From 47d259ca017aca92624c43596ca16fc64355e97b Mon Sep 17 00:00:00 2001
From: Phillip Kelley-Dotson
Date: Fri, 15 Apr 2022 15:09:07 -0700
Subject: [PATCH] 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>
---
.../components/SqlEditor/SqlEditor.test.jsx | 21 +++++++++++-
.../src/SqlLab/components/SqlEditor/index.jsx | 25 +++++++++++++-
.../components/SqlEditorLeftBar/index.tsx | 31 ++++++++++++++++-
superset-frontend/src/SqlLab/types.ts | 1 +
.../src/assets/images/vector.svg | 21 ++++++++++++
.../DatabaseSelector.test.tsx | 33 ++++++++++++++-----
.../src/components/DatabaseSelector/index.tsx | 16 ++++++---
.../src/components/Select/Select.tsx | 2 +-
.../src/components/TableSelector/index.tsx | 6 ++++
9 files changed, 139 insertions(+), 17 deletions(-)
create mode 100644 superset-frontend/src/assets/images/vector.svg
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 +91,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);
};
@@ -142,6 +153,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) {
@@ -164,8 +191,10 @@ export default function SqlEditorLeftBar({
;
+ 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