feat: add empty states to sqlab editor and select (#19598)

* feat: add empty states to sqlab editor and select

* add suggestions and test

* update type

* lint fix and add suggestions

* fix typo

* run lint

* remove unused code

* fix test

* remove redux for propagation and other suggestions

* add t

* lint

* fix text and remove code

* ts and fix t in p

* fix spelling

* remove unused prop

* add fn to prop change state

* remove unused code

* remove unused types

* update code and test

* fix lint

* fix ts

* update ts

* add type export and fix test

* Update superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx

Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>

* Update superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx

Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>

* Update superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx

Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>

* Update superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx

Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>

* remove handlerror and unused code

Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
This commit is contained in:
Phillip Kelley-Dotson 2022-04-15 15:09:07 -07:00 committed by GitHub
parent 154f1ea8c9
commit 06ec88eb99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 147 additions and 17 deletions

View File

@ -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);

View File

@ -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';
@ -180,6 +182,7 @@ class SqlEditor extends React.PureComponent {
),
showCreateAsModal: false,
createAs: '',
showEmptyState: false,
};
this.sqlEditorRef = React.createRef();
this.northPaneRef = React.createRef();
@ -189,6 +192,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);
@ -228,7 +232,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);
@ -369,6 +377,10 @@ class SqlEditor extends React.PureComponent {
return base;
}
setEmptyState(bool) {
this.setState({ showEmptyState: bool });
}
setQueryEditorSql(sql) {
this.props.queryEditorSetSql(this.props.queryEditor, sql);
}
@ -760,10 +772,21 @@ class SqlEditor extends React.PureComponent {
queryEditor={this.props.queryEditor}
tables={this.props.tables}
actions={this.props.actions}
setEmptyState={this.setEmptyState}
/>
</div>
</CSSTransition>
{this.queryPane()}
{this.state.showEmptyState ? (
<EmptyStateBig
image="vector.svg"
title={t('Select a database to write a query')}
description={t(
'Choose one of the available databases from the panel on the left.',
)}
/>
) : (
this.queryPane()
)}
<StyledModal
visible={this.state.showCreateAsModal}
title={t(createViewModalTitle)}

View File

@ -16,7 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useRef, useCallback, useMemo } from 'react';
import React, {
useEffect,
useRef,
useCallback,
useMemo,
useState,
Dispatch,
SetStateAction,
} from 'react';
import Button from 'src/components/Button';
import { t, styled, css, SupersetTheme } from '@superset-ui/core';
import Collapse from 'src/components/Collapse';
@ -25,6 +33,7 @@ import { TableSelectorMultiple } from 'src/components/TableSelector';
import { IconTooltip } from 'src/components/IconTooltip';
import { QueryEditor } from 'src/SqlLab/types';
import { DatabaseObject } from 'src/components/DatabaseSelector';
import { EmptyStateSmall } from 'src/components/EmptyState';
import TableElement, { Table, TableElementProps } from '../TableElement';
interface ExtendedTable extends Table {
@ -54,6 +63,8 @@ interface SqlEditorLeftBarProps {
tables?: ExtendedTable[];
actions: actionsTypes & TableElementProps['actions'];
database: DatabaseObject;
setEmptyState: Dispatch<SetStateAction<boolean>>;
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>(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 = (
<EmptyStateSmall
image="empty.svg"
title={
emptyResultsWithSearch
? t('No databases match your search')
: t('There are no databases available')
}
description={
<p>
{t('Manage your databases')}{' '}
<a href="/databaseview/list">{t('here')}</a>
</p>
}
/>
);
const handleSchemaChange = useCallback(
(schema: string) => {
if (queryEditorRef.current) {
@ -185,6 +220,8 @@ export default function SqlEditorLeftBar({
return (
<div className="SqlEditorLeftBar">
<TableSelectorMultiple
onEmptyResults={onEmptyResults}
emptyState={emptyStateComponent}
database={database}
getDbList={actions.setDatabases}
handleError={actions.addDangerToast}

View File

@ -114,6 +114,7 @@ export type RootState = {
activeSouthPaneTab: string | number; // default is string; action.newQuery.id is number
alerts: any[];
databases: Record<string, any>;
dbConnect: boolean;
offline: boolean;
queries: Query[];
queryEditors: QueryEditor[];

View File

@ -0,0 +1,21 @@
<!--
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.
-->
<svg width="118" height="150" viewBox="0 0 118 150" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.1212 11.5536H11.6212V12.0536V46.875V47.375H12.1212H105.871H106.371V46.875V12.0536V11.5536H105.871H12.1212ZM105.871 92.9107H106.371V92.4107V57.5893V57.0893H105.871H12.1212H11.6212V57.5893V92.4107V92.9107H12.1212H105.871ZM105.871 138.446H106.371V137.946V103.125V102.625H105.871H12.1212H11.6212V103.125V137.946V138.446H12.1212H105.871ZM5.42477 0.5H112.568C115.255 0.5 117.425 2.67012 117.425 5.35714V144.643C117.425 147.33 115.255 149.5 112.568 149.5H5.42477C2.73774 149.5 0.567627 147.33 0.567627 144.643V5.35714C0.567627 2.67012 2.73774 0.5 5.42477 0.5ZM26.4897 33.8458C25.3276 32.6838 24.6748 31.1077 24.6748 29.4643C24.6748 27.8209 25.3276 26.2448 26.4897 25.0828C27.6517 23.9207 29.2278 23.2679 30.8712 23.2679C32.5146 23.2679 34.0907 23.9207 35.2527 25.0828C36.4148 26.2448 37.0676 27.8209 37.0676 29.4643C37.0676 31.1077 36.4148 32.6838 35.2527 33.8458C34.0907 35.0079 32.5146 35.6607 30.8712 35.6607C29.2278 35.6607 27.6517 35.0079 26.4897 33.8458ZM26.4897 79.3815C25.3276 78.2195 24.6748 76.6434 24.6748 75C24.6748 73.3566 25.3276 71.7805 26.4897 70.6185C27.6517 69.4564 29.2278 68.8036 30.8712 68.8036C32.5146 68.8036 34.0907 69.4564 35.2527 70.6185C36.4148 71.7805 37.0676 73.3566 37.0676 75C37.0676 76.6434 36.4148 78.2195 35.2527 79.3815C34.0907 80.5436 32.5146 81.1964 30.8712 81.1964C29.2278 81.1964 27.6517 80.5436 26.4897 79.3815ZM26.4897 124.917C25.3276 123.755 24.6748 122.179 24.6748 120.536C24.6748 118.892 25.3276 117.316 26.4897 116.154C27.6517 114.992 29.2278 114.339 30.8712 114.339C32.5146 114.339 34.0907 114.992 35.2527 116.154C36.4148 117.316 37.0676 118.892 37.0676 120.536C37.0676 122.179 36.4148 123.755 35.2527 124.917C34.0907 126.079 32.5146 126.732 30.8712 126.732C29.2278 126.732 27.6517 126.079 26.4897 124.917Z" fill="#F7F7F7" stroke="#D9D9D9"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -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(
<DatabaseSelector
{...props}
db={undefined}
emptyState={<EmptyStateSmall title="empty" image="" />}
/>,
{ 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(<DatabaseSelector {...props} />, { useRedux: true });

View File

@ -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<object>) => 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={<FormLabel>{t('Database')}</FormLabel>}
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(
<Select
ariaLabel={t('Select schema or type schema name')}
disabled={readOnly}
disabled={!currentDb || readOnly}
header={<FormLabel>{t('Schema')}</FormLabel>}
labelInValue
lazyLoading={false}

View File

@ -35,9 +35,9 @@ import AntdSelect, {
LabeledValue as AntdLabeledValue,
} from 'antd/lib/select';
import { DownOutlined, SearchOutlined } from '@ant-design/icons';
import { Spin } from 'antd';
import debounce from 'lodash/debounce';
import { isEqual } from 'lodash';
import { Spin } from 'antd';
import Icons from 'src/components/Icons';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
import { SLOW_DEBOUNCE } from 'src/constants';

View File

@ -82,6 +82,7 @@ const TableLabel = styled.span`
interface TableSelectorProps {
clearable?: boolean;
database?: DatabaseObject;
emptyState?: ReactNode;
formMode?: boolean;
getDbList?: (arg0: any) => {};
handleError: (msg: string) => void;
@ -92,6 +93,7 @@ interface TableSelectorProps {
onTablesLoad?: (options: Array<any>) => void;
readOnly?: boolean;
schema?: string;
onEmptyResults?: (searchText?: string) => void;
sqlLabMode?: boolean;
tableValue?: string | string[];
onTableSelectChange?: (value?: string | string[], schema?: string) => void;
@ -146,6 +148,7 @@ const TableOption = ({ table }: { table: Table }) => {
const TableSelector: FunctionComponent<TableSelectorProps> = ({
database,
emptyState,
formMode = false,
getDbList,
handleError,
@ -155,6 +158,7 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
onSchemasLoad,
onTablesLoad,
readOnly = false,
onEmptyResults,
schema,
sqlLabMode = true,
tableSelectMode = 'single',
@ -286,10 +290,12 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
<DatabaseSelector
key={currentDatabase?.id}
db={currentDatabase}
emptyState={emptyState}
formMode={formMode}
getDbList={getDbList}
handleError={handleError}
onDbChange={readOnly ? undefined : internalDbChange}
onEmptyResults={onEmptyResults}
onSchemaChange={readOnly ? undefined : internalSchemaChange}
onSchemasLoad={onSchemasLoad}
schema={currentSchema}