diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx index f3549b547f8b1..d946c675cc8c4 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx @@ -38,6 +38,7 @@ import { queryEditorSetSelectedText, queryEditorSetSchemaOptions, } from 'src/SqlLab/actions/sqlLab'; +import { EmptyStateBig } from 'src/components/EmptyState'; import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; import { initialState, queries, table } from 'src/SqlLab/fixtures'; @@ -57,7 +58,19 @@ describe('SqlEditor', () => { queryEditorSetSchemaOptions, addDangerToast: jest.fn(), }, - database: {}, + database: { + allow_ctas: false, + allow_cvas: false, + allow_dml: false, + allow_file_upload: false, + allow_multi_schema_metadata_fetch: false, + allow_run_async: false, + backend: 'postgresql', + database_name: 'examples', + expose_in_sqllab: true, + force_ctas_schema: null, + id: 1, + }, queryEditorId: initialState.sqlLab.queryEditors[0].id, latestQuery: queries[0], tables: [table], @@ -80,6 +93,12 @@ describe('SqlEditor', () => { }, ); + it('does not render SqlEditor if no db selected', () => { + const database = {}; + const updatedProps = { ...mockedProps, database }; + const wrapper = buildWrapper(updatedProps); + expect(wrapper.find(EmptyStateBig)).toExist(); + }); it('render a SqlEditorLeftBar', async () => { const wrapper = buildWrapper(); await waitForComponentToPaint(wrapper); diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx index 7899cbf71908a..baef09848a2e2 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx @@ -66,6 +66,8 @@ import { setItem, } from 'src/utils/localStorageHelpers'; import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; +import { EmptyStateBig } from 'src/components/EmptyState'; +import { isEmpty } from 'lodash'; import TemplateParamsEditor from '../TemplateParamsEditor'; import ConnectedSouthPane from '../SouthPane/state'; import SaveQuery from '../SaveQuery'; @@ -179,6 +181,7 @@ class SqlEditor extends React.PureComponent { ), showCreateAsModal: false, createAs: '', + showEmptyState: false, }; this.sqlEditorRef = React.createRef(); this.northPaneRef = React.createRef(); @@ -188,6 +191,7 @@ class SqlEditor extends React.PureComponent { this.onResizeEnd = this.onResizeEnd.bind(this); this.canValidateQuery = this.canValidateQuery.bind(this); this.runQuery = this.runQuery.bind(this); + this.setEmptyState = this.setEmptyState.bind(this); this.stopQuery = this.stopQuery.bind(this); this.saveQuery = this.saveQuery.bind(this); this.onSqlChanged = this.onSqlChanged.bind(this); @@ -227,7 +231,11 @@ class SqlEditor extends React.PureComponent { // We need to measure the height of the sql editor post render to figure the height of // the south pane so it gets rendered properly // eslint-disable-next-line react/no-did-mount-set-state + const db = this.props.database; this.setState({ height: this.getSqlEditorHeight() }); + if (!db || isEmpty(db)) { + this.setEmptyState(true); + } window.addEventListener('resize', this.handleWindowResize); window.addEventListener('beforeunload', this.onBeforeUnload); @@ -362,6 +370,10 @@ class SqlEditor extends React.PureComponent { return base; } + setEmptyState(bool) { + this.setState({ showEmptyState: bool }); + } + setQueryEditorSql(sql) { this.props.queryEditorSetSql(this.props.queryEditor, sql); } @@ -753,10 +765,21 @@ class SqlEditor extends React.PureComponent { queryEditor={this.props.queryEditor} tables={this.props.tables} actions={this.props.actions} + setEmptyState={this.setEmptyState} /> - {this.queryPane()} + {this.state.showEmptyState ? ( + + ) : ( + this.queryPane() + )} >; + showDisabled: boolean; } const StyledScrollbarContainer = styled.div` @@ -88,15 +99,23 @@ export default function SqlEditorLeftBar({ queryEditor, tables = [], height = 500, + setEmptyState, }: SqlEditorLeftBarProps) { // Ref needed to avoid infinite rerenders on handlers // that require and modify the queryEditor const queryEditorRef = useRef(queryEditor); + const [emptyResultsWithSearch, setEmptyResultsWithSearch] = useState(false); + useEffect(() => { queryEditorRef.current = queryEditor; }, [queryEditor]); + const onEmptyResults = (searchText?: string) => { + setEmptyResultsWithSearch(!!searchText); + }; + const onDbChange = ({ id: dbId }: { id: number }) => { + setEmptyState(false); actions.queryEditorSetDb(queryEditor, dbId); actions.queryEditorSetFunctionNames(queryEditor, dbId); }; @@ -164,6 +183,22 @@ export default function SqlEditorLeftBar({ const shouldShowReset = window.location.search === '?reset=1'; const tableMetaDataHeight = height - 130; // 130 is the height of the selects above + const emptyStateComponent = ( + + {t('Manage your databases')}{' '} + {t('here')} +

+ } + /> + ); const handleSchemaChange = useCallback( (schema: string) => { if (queryEditorRef.current) { @@ -185,6 +220,8 @@ export default function SqlEditorLeftBar({ return (
; + dbConnect: boolean; offline: boolean; queries: Query[]; queryEditors: QueryEditor[]; diff --git a/superset-frontend/src/assets/images/vector.svg b/superset-frontend/src/assets/images/vector.svg new file mode 100644 index 0000000000000..0bf9c39c6ccb0 --- /dev/null +++ b/superset-frontend/src/assets/images/vector.svg @@ -0,0 +1,21 @@ + + + + diff --git a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx index 2387c2e2517fe..272249b549600 100644 --- a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx +++ b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx @@ -21,11 +21,12 @@ 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 DatabaseSelector from '.'; +import DatabaseSelector, { DatabaseSelectorProps } from '.'; +import { EmptyStateSmall } from '../EmptyState'; const SupersetClientGet = jest.spyOn(SupersetClient, 'get'); -const createProps = () => ({ +const createProps = (): DatabaseSelectorProps => ({ db: { id: 1, database_name: 'test', @@ -38,12 +39,10 @@ const createProps = () => ({ schema: undefined, sqlLabMode: true, getDbList: jest.fn(), - getTableList: jest.fn(), handleError: jest.fn(), onDbChange: jest.fn(), onSchemaChange: jest.fn(), onSchemasLoad: jest.fn(), - onUpdate: jest.fn(), }); beforeEach(() => { @@ -191,12 +190,10 @@ test('Refresh should work', async () => { await waitFor(() => { expect(SupersetClientGet).toBeCalledTimes(2); 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(0); - expect(props.onUpdate).toBeCalledTimes(0); }); userEvent.click(screen.getByRole('button', { name: 'refresh' })); @@ -204,12 +201,10 @@ test('Refresh should work', async () => { await waitFor(() => { expect(SupersetClientGet).toBeCalledTimes(3); expect(props.getDbList).toBeCalledTimes(1); - expect(props.getTableList).toBeCalledTimes(0); expect(props.handleError).toBeCalledTimes(0); expect(props.onDbChange).toBeCalledTimes(0); expect(props.onSchemaChange).toBeCalledTimes(0); expect(props.onSchemasLoad).toBeCalledTimes(2); - expect(props.onUpdate).toBeCalledTimes(0); }); }); @@ -224,6 +219,28 @@ test('Should database select display options', async () => { expect(await screen.findByText('test-mysql')).toBeInTheDocument(); }); +test('should show empty state if there are no options', async () => { + SupersetClientGet.mockImplementation( + async () => ({ json: { result: [] } } as any), + ); + const props = createProps(); + render( + } + />, + { useRedux: true }, + ); + const select = screen.getByRole('combobox', { + name: 'Select database or type database name', + }); + userEvent.click(select); + const emptystate = await screen.findByText('empty'); + expect(emptystate).toBeInTheDocument(); + expect(screen.queryByText('test-mysql')).not.toBeInTheDocument(); +}); + test('Should schema select display options', async () => { const props = createProps(); render(, { useRedux: true }); diff --git a/superset-frontend/src/components/DatabaseSelector/index.tsx b/superset-frontend/src/components/DatabaseSelector/index.tsx index 531a7a9e7194c..718177a13956f 100644 --- a/superset-frontend/src/components/DatabaseSelector/index.tsx +++ b/superset-frontend/src/components/DatabaseSelector/index.tsx @@ -86,13 +86,15 @@ export type DatabaseObject = { type SchemaValue = { label: string; value: string }; -interface DatabaseSelectorProps { +export interface DatabaseSelectorProps { db?: DatabaseObject; + emptyState?: ReactNode; formMode?: boolean; getDbList?: (arg0: any) => {}; handleError: (msg: string) => void; isDatabaseSelectEnabled?: boolean; onDbChange?: (db: DatabaseObject) => void; + onEmptyResults?: (searchText?: string) => void; onSchemaChange?: (schema?: string) => void; onSchemasLoad?: (schemas: Array) => void; readOnly?: boolean; @@ -118,10 +120,12 @@ const SelectLabel = ({ export default function DatabaseSelector({ db, formMode = false, + emptyState, getDbList, handleError, isDatabaseSelectEnabled = true, onDbChange, + onEmptyResults, onSchemaChange, onSchemasLoad, readOnly = false, @@ -146,6 +150,7 @@ export default function DatabaseSelector({ ); const [refresh, setRefresh] = useState(0); const { addSuccessToast } = useToasts(); + const loadDatabases = useMemo( () => async ( @@ -181,7 +186,7 @@ export default function DatabaseSelector({ getDbList(result); } if (result.length === 0) { - handleError(t("It seems you don't have access to any database")); + if (onEmptyResults) onEmptyResults(search); } const options = result.map((row: DatabaseObject) => ({ label: ( @@ -197,13 +202,14 @@ export default function DatabaseSelector({ allow_multi_schema_metadata_fetch: row.allow_multi_schema_metadata_fetch, })); + return { data: options, totalCount: options.length, }; }); }, - [formMode, getDbList, handleError, sqlLabMode], + [formMode, getDbList, sqlLabMode], ); useEffect(() => { @@ -272,6 +278,7 @@ export default function DatabaseSelector({ data-test="select-database" header={{t('Database')}} lazyLoading={false} + notFoundContent={emptyState} onChange={changeDataBase} value={currentDb} placeholder={t('Select database or type database name')} @@ -289,11 +296,10 @@ export default function DatabaseSelector({ tooltipContent={t('Force refresh schema list')} /> ); - return renderSelectRow(