This is an automated email from the ASF dual-hosted git repository.
pkdotson 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 06ec88eb99 feat: add empty states to sqlab editor and select (#19598)
06ec88eb99 is described below
commit 06ec88eb9934e90c93c9ee90a7871ceaf5abde06
Author: Phillip Kelley-Dotson <[email protected]>
AuthorDate: Fri Apr 15 15:09:07 2022 -0700
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
<[email protected]>
* Update superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
Co-authored-by: Michael S. Molina
<[email protected]>
* Update superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
Co-authored-by: Michael S. Molina
<[email protected]>
* Update superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
Co-authored-by: Michael S. Molina
<[email protected]>
* remove handlerror and unused code
Co-authored-by: Michael S. Molina
<[email protected]>
---
.../SqlLab/components/SqlEditor/SqlEditor.test.jsx | 21 +++++++++++-
.../src/SqlLab/components/SqlEditor/index.jsx | 25 +++++++++++++-
.../SqlLab/components/SqlEditorLeftBar/index.tsx | 39 +++++++++++++++++++++-
superset-frontend/src/SqlLab/types.ts | 1 +
superset-frontend/src/assets/images/vector.svg | 21 ++++++++++++
.../DatabaseSelector/DatabaseSelector.test.tsx | 33 +++++++++++++-----
.../src/components/DatabaseSelector/index.tsx | 16 ++++++---
superset-frontend/src/components/Select/Select.tsx | 2 +-
.../src/components/TableSelector/index.tsx | 6 ++++
9 files changed, 147 insertions(+), 17 deletions(-)
diff --git
a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx
b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx
index f3549b547f..d946c675cc 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 c6708b266c..df1a9a77c5 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';
@@ -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)}
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
index a50e3a3f62..f742494654 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
@@ -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}
diff --git a/superset-frontend/src/SqlLab/types.ts
b/superset-frontend/src/SqlLab/types.ts
index 6693089574..e171479163 100644
--- a/superset-frontend/src/SqlLab/types.ts
+++ b/superset-frontend/src/SqlLab/types.ts
@@ -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[];
diff --git a/superset-frontend/src/assets/images/vector.svg
b/superset-frontend/src/assets/images/vector.svg
new file mode 100644
index 0000000000..0bf9c39c6c
--- /dev/null
+++ b/superset-frontend/src/assets/images/vector.svg
@@ -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 [...]
+</svg>
diff --git
a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx
b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx
index 2387c2e251..272249b549 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(
+ <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 });
diff --git a/superset-frontend/src/components/DatabaseSelector/index.tsx
b/superset-frontend/src/components/DatabaseSelector/index.tsx
index 531a7a9e71..718177a139 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<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}
diff --git a/superset-frontend/src/components/Select/Select.tsx
b/superset-frontend/src/components/Select/Select.tsx
index 20bab6bcfc..3624f4a2b1 100644
--- a/superset-frontend/src/components/Select/Select.tsx
+++ b/superset-frontend/src/components/Select/Select.tsx
@@ -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';
diff --git a/superset-frontend/src/components/TableSelector/index.tsx
b/superset-frontend/src/components/TableSelector/index.tsx
index 84696f9391..fcc5dbe10d 100644
--- a/superset-frontend/src/components/TableSelector/index.tsx
+++ b/superset-frontend/src/components/TableSelector/index.tsx
@@ -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}