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}

Reply via email to