database modal should close on connect with tab layout (#14771)

This commit is contained in:
Elizabeth Thompson 2021-05-24 10:13:56 -07:00 committed by GitHub
parent e9657afe4b
commit fbe6f16052
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 278 additions and 285 deletions

View File

@ -44,6 +44,7 @@ const SqlAlchemyTab = ({
<input
type="text"
name="database_name"
data-test="database-name-input"
value={db?.database_name || ''}
placeholder={t('Name your database')}
onChange={onInputChange}
@ -62,6 +63,7 @@ const SqlAlchemyTab = ({
<input
type="text"
name="sqlalchemy_uri"
data-test="sqlalchemy-uri-input"
value={db?.sqlalchemy_uri || ''}
autoComplete="off"
placeholder={t(

View File

@ -17,28 +17,11 @@
* under the License.
*/
import React from 'react';
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import * as redux from 'react-redux';
import fetchMock from 'fetch-mock';
import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
import { initialState } from 'spec/javascripts/sqllab/fixtures';
import { styledMount as mount } from 'spec/helpers/theming';
import { render, screen } from 'spec/helpers/testing-library';
import Modal from 'src/components/Modal';
import Tabs from 'src/components/Tabs';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import DatabaseModal from './index';
// store needed for withToasts(DatabaseModal)
const mockStore = configureStore([thunk]);
const store = mockStore({});
const mockedProps = {
show: true,
};
const dbProps = {
show: true,
databaseId: 10,
@ -46,10 +29,11 @@ const dbProps = {
sqlalchemy_uri: 'postgres://superset:superset@something:1234/superset',
};
const DATABASE_ENDPOINT = 'glob:*/api/v1/database/*';
const AVAILABLE_DB_ENDPOINT = 'glob:*/api/v1/database/available/*';
const DATABASE_FETCH_ENDPOINT = 'glob:*/api/v1/database/10';
const DATABASE_POST_ENDPOINT = 'glob:*/api/v1/database/';
const AVAILABLE_DB_ENDPOINT = 'glob:*/api/v1/database/available*';
fetchMock.config.overwriteRoutes = true;
fetchMock.get(DATABASE_ENDPOINT, {
fetchMock.get(DATABASE_FETCH_ENDPOINT, {
result: {
id: 10,
database_name: 'my database',
@ -59,7 +43,7 @@ fetchMock.get(DATABASE_ENDPOINT, {
configuration_method: 'sqlalchemy_form',
},
});
fetchMock.get(AVAILABLE_DB_ENDPOINT, {
fetchMock.mock(AVAILABLE_DB_ENDPOINT, {
databases: [
{
engine: 'mysql',
@ -70,268 +54,270 @@ fetchMock.get(AVAILABLE_DB_ENDPOINT, {
});
describe('DatabaseModal', () => {
afterEach(fetchMock.reset);
describe('enzyme', () => {
let wrapper;
let spyOnUseSelector;
beforeAll(() => {
spyOnUseSelector = jest.spyOn(redux, 'useSelector');
spyOnUseSelector.mockReturnValue(initialState.common.conf);
afterEach(fetchMock.restore);
describe('initial load', () => {
it('hides the forms from the db when not selected', () => {
render(<DatabaseModal show databaseId={1} />, { useRedux: true });
// Select Advanced tab
const advancedTab = screen.getByRole('tab', {
name: /advanced/i,
});
userEvent.click(advancedTab);
// Select SQL Lab tab
const sqlLabSettingsTab = screen.getByRole('tab', {
name: /sql lab/i,
});
userEvent.click(sqlLabSettingsTab);
const exposeInSqlLab = screen.getByText('Expose in SQL Lab');
const exposeChoicesForm = exposeInSqlLab.parentElement.nextSibling;
const schemaField = screen.getByText('CTAS & CVAS SCHEMA').parentElement;
expect(exposeChoicesForm).not.toHaveClass('open');
expect(schemaField).not.toHaveClass('open');
});
});
it('renders all settings when "Expose in SQL Lab" is checked', () => {
render(<DatabaseModal {...dbProps} />, { useRedux: true });
// Select Advanced tab
const advancedTab = screen.getByRole('tab', {
name: /advanced/i,
});
userEvent.click(advancedTab);
// Select SQL Lab tab
const sqlLabSettingsTab = screen.getByRole('tab', {
name: /sql lab/i,
});
userEvent.click(sqlLabSettingsTab);
// Grab all SQL Lab settings by their labels
// const exposeInSqlLab = screen.getByText('Expose in SQL Lab');
const exposeInSqlLab = screen.getByRole('checkbox', {
name: /expose in sql lab/i,
});
expect(exposeInSqlLab).not.toBeChecked();
userEvent.click(exposeInSqlLab);
// While checked make sure all checkboxes are showing
expect(exposeInSqlLab).toBeChecked();
const checkboxes = screen
.getAllByRole('checkbox')
.filter(checkbox => !checkbox.checked);
expect(checkboxes.length).toEqual(4);
});
it('renders the schema field when allowCTAS is checked', () => {
render(<DatabaseModal {...dbProps} />, { useRedux: true });
// Select Advanced tab
const advancedTab = screen.getByRole('tab', {
name: /advanced/i,
});
userEvent.click(advancedTab);
// Select SQL Lab tab
const sqlLabSettingsTab = screen.getByRole('tab', {
name: /sql lab/i,
});
userEvent.click(sqlLabSettingsTab);
// Grab CTAS & schema field by their labels
const allowCTAS = screen.getByLabelText('Allow CREATE TABLE AS');
const schemaField = screen.getByText('CTAS & CVAS SCHEMA').parentElement;
// While CTAS & CVAS are unchecked, schema field is not visible
expect(schemaField).not.toHaveClass('open');
// Check "Allow CTAS" to reveal schema field
userEvent.click(allowCTAS);
expect(schemaField).toHaveClass('open');
// Uncheck "Allow CTAS" to hide schema field again
userEvent.click(allowCTAS);
expect(schemaField).not.toHaveClass('open');
});
it('renders the schema field when allowCVAS is checked', () => {
render(<DatabaseModal {...dbProps} />, { useRedux: true });
// Select Advanced tab
const advancedTab = screen.getByRole('tab', {
name: /advanced/i,
});
userEvent.click(advancedTab);
// Select SQL Lab tab
const sqlLabSettingsTab = screen.getByRole('tab', {
name: /sql lab/i,
});
userEvent.click(sqlLabSettingsTab);
// Grab CVAS by it's label & schema field
const allowCVAS = screen.getByText('Allow CREATE VIEW AS');
const schemaField = screen.getByText('CTAS & CVAS SCHEMA').parentElement;
// While CTAS & CVAS are unchecked, schema field is not visible
expect(schemaField).not.toHaveClass('open');
// Check "Allow CVAS" to reveal schema field
userEvent.click(allowCVAS);
expect(schemaField).toHaveClass('open');
// Uncheck "Allow CVAS" to hide schema field again
userEvent.click(allowCVAS);
expect(schemaField).not.toHaveClass('open');
});
it('renders the schema field when both allowCTAS and allowCVAS are checked', () => {
render(<DatabaseModal {...dbProps} />, { useRedux: true });
// Select Advanced tab
const advancedTab = screen.getByRole('tab', {
name: /advanced/i,
});
userEvent.click(advancedTab);
// Select SQL Lab tab
const sqlLabSettingsTab = screen.getByRole('tab', {
name: /sql lab/i,
});
userEvent.click(sqlLabSettingsTab);
// Grab CTAS and CVAS by their labels, & schema field
const allowCTAS = screen.getByText('Allow CREATE TABLE AS');
const allowCVAS = screen.getByText('Allow CREATE VIEW AS');
const schemaField = screen.getByText('CTAS & CVAS SCHEMA').parentElement;
// While CTAS & CVAS are unchecked, schema field is not visible
expect(schemaField).not.toHaveClass('open');
// Check both "Allow CTAS" and "Allow CVAS" to reveal schema field
userEvent.click(allowCTAS);
userEvent.click(allowCVAS);
expect(schemaField).toHaveClass('open');
// Uncheck both "Allow CTAS" and "Allow CVAS" to hide schema field again
userEvent.click(allowCTAS);
userEvent.click(allowCVAS);
// Both checkboxes go unchecked, so the field should no longer render
expect(schemaField).not.toHaveClass('open');
});
describe('create database', () => {
beforeEach(() => {
wrapper = mount(
<Provider store={store}>
<DatabaseModal store={store} {...mockedProps} />
</Provider>,
);
fetchMock.post(DATABASE_POST_ENDPOINT, {
id: 10,
});
fetchMock.mock(AVAILABLE_DB_ENDPOINT, {
databases: [
{
engine: 'mysql',
name: 'MySQL',
preferred: false,
},
],
});
});
afterEach(() => {
wrapper.unmount();
const props = {
...dbProps,
databaseId: null,
database_name: null,
sqlalchemy_uri: null,
};
it('should show a form when dynamic_form is selected', async () => {
render(<DatabaseModal {...props} />, { useRedux: true });
// it should have the correct header text
const headerText = screen.getByText(/connect a database/i);
expect(headerText).toBeVisible();
await screen.findByText(/display name/i);
// it does not fetch any databases if no id is passed in
expect(fetchMock.calls(DATABASE_FETCH_ENDPOINT).length).toEqual(0);
// todo we haven't hooked this up to load dynamically yet so
// we can't currently test it
});
it('renders', () => {
expect(wrapper.find(DatabaseModal)).toExist();
});
it('renders a Modal', () => {
expect(wrapper.find(Modal)).toExist();
});
it('renders "Connect a database" header when no database is included', () => {
expect(wrapper.find('h4').text()).toEqual('Connect a database');
});
it('renders "Edit database" header when database prop is included', () => {
const editWrapper = mount(<DatabaseModal store={store} {...dbProps} />);
waitForComponentToPaint(editWrapper);
expect(editWrapper.find('h4').text()).toEqual('Edit database');
editWrapper.unmount();
});
it('renders a Tabs menu', () => {
expect(wrapper.find(Tabs)).toExist();
});
it('renders two TabPanes', () => {
expect(wrapper.find('.ant-tabs-tab')).toExist();
expect(wrapper.find('.ant-tabs-tab')).toHaveLength(2);
});
it('renders input elements for Connection section', () => {
expect(wrapper.find('input[name="database_name"]')).toExist();
expect(wrapper.find('input[name="sqlalchemy_uri"]')).toExist();
it('should close the modal on save if using the sqlalchemy form', async () => {
const onHideMock = jest.fn();
render(<DatabaseModal {...props} onHide={onHideMock} />, {
useRedux: true,
});
// button should be disabled by default
const submitButton = screen.getByTestId('modal-confirm-button');
expect(submitButton).toBeDisabled();
const displayName = screen.getByTestId('database-name-input');
userEvent.type(displayName, 'MyTestDB');
expect(displayName.value).toBe('MyTestDB');
const sqlalchemyInput = screen.getByTestId('sqlalchemy-uri-input');
userEvent.type(sqlalchemyInput, 'some_url');
expect(sqlalchemyInput.value).toBe('some_url');
// button should not be disabled now
expect(submitButton).toBeEnabled();
await waitFor(() => {
userEvent.click(submitButton);
});
expect(fetchMock.calls(DATABASE_POST_ENDPOINT)).toHaveLength(1);
expect(onHideMock).toHaveBeenCalled();
});
});
describe('RTL', () => {
describe('initial load', () => {
it('hides the forms from the db when not selected', () => {
render(<DatabaseModal show databaseId={1} />, { useRedux: true });
// Select Advanced tab
const advancedTab = screen.getByRole('tab', {
name: /advanced/i,
});
userEvent.click(advancedTab);
// Select SQL Lab tab
const sqlLabSettingsTab = screen.getByRole('tab', {
name: /sql lab/i,
});
userEvent.click(sqlLabSettingsTab);
const exposeInSqlLab = screen.getByText('Expose in SQL Lab');
const exposeChoicesForm = exposeInSqlLab.parentElement.nextSibling;
const schemaField = screen.getByText('CTAS & CVAS SCHEMA')
.parentElement;
expect(exposeChoicesForm).not.toHaveClass('open');
expect(schemaField).not.toHaveClass('open');
});
});
it('renders all settings when "Expose in SQL Lab" is checked', () => {
render(<DatabaseModal {...dbProps} />, { useRedux: true });
// Select Advanced tab
const advancedTab = screen.getByRole('tab', {
name: /advanced/i,
});
userEvent.click(advancedTab);
// Select SQL Lab tab
const sqlLabSettingsTab = screen.getByRole('tab', {
name: /sql lab/i,
});
userEvent.click(sqlLabSettingsTab);
// Grab all SQL Lab settings by their labels
// const exposeInSqlLab = screen.getByText('Expose in SQL Lab');
const exposeInSqlLab = screen.getByRole('checkbox', {
name: /expose in sql lab/i,
});
expect(exposeInSqlLab).not.toBeChecked();
userEvent.click(exposeInSqlLab);
// While checked make sure all checkboxes are showing
expect(exposeInSqlLab).toBeChecked();
const checkboxes = screen
.getAllByRole('checkbox')
.filter(checkbox => !checkbox.checked);
expect(checkboxes.length).toEqual(4);
});
it('renders the schema field when allowCTAS is checked', () => {
render(<DatabaseModal {...dbProps} />, { useRedux: true });
// Select Advanced tab
const advancedTab = screen.getByRole('tab', {
name: /advanced/i,
});
userEvent.click(advancedTab);
// Select SQL Lab tab
const sqlLabSettingsTab = screen.getByRole('tab', {
name: /sql lab/i,
});
userEvent.click(sqlLabSettingsTab);
// Grab CTAS & schema field by their labels
const allowCTAS = screen.getByLabelText('Allow CREATE TABLE AS');
const schemaField = screen.getByText('CTAS & CVAS SCHEMA').parentElement;
// While CTAS & CVAS are unchecked, schema field is not visible
expect(schemaField).not.toHaveClass('open');
// Check "Allow CTAS" to reveal schema field
userEvent.click(allowCTAS);
expect(schemaField).toHaveClass('open');
// Uncheck "Allow CTAS" to hide schema field again
userEvent.click(allowCTAS);
expect(schemaField).not.toHaveClass('open');
});
it('renders the schema field when allowCVAS is checked', () => {
render(<DatabaseModal {...dbProps} />, { useRedux: true });
// Select Advanced tab
const advancedTab = screen.getByRole('tab', {
name: /advanced/i,
});
userEvent.click(advancedTab);
// Select SQL Lab tab
const sqlLabSettingsTab = screen.getByRole('tab', {
name: /sql lab/i,
});
userEvent.click(sqlLabSettingsTab);
// Grab CVAS by it's label & schema field
const allowCVAS = screen.getByText('Allow CREATE VIEW AS');
const schemaField = screen.getByText('CTAS & CVAS SCHEMA').parentElement;
// While CTAS & CVAS are unchecked, schema field is not visible
expect(schemaField).not.toHaveClass('open');
// Check "Allow CVAS" to reveal schema field
userEvent.click(allowCVAS);
expect(schemaField).toHaveClass('open');
// Uncheck "Allow CVAS" to hide schema field again
userEvent.click(allowCVAS);
expect(schemaField).not.toHaveClass('open');
});
it('renders the schema field when both allowCTAS and allowCVAS are checked', () => {
render(<DatabaseModal {...dbProps} />, { useRedux: true });
// Select Advanced tab
const advancedTab = screen.getByRole('tab', {
name: /advanced/i,
});
userEvent.click(advancedTab);
// Select SQL Lab tab
const sqlLabSettingsTab = screen.getByRole('tab', {
name: /sql lab/i,
});
userEvent.click(sqlLabSettingsTab);
// Grab CTAS and CVAS by their labels, & schema field
const allowCTAS = screen.getByText('Allow CREATE TABLE AS');
const allowCVAS = screen.getByText('Allow CREATE VIEW AS');
const schemaField = screen.getByText('CTAS & CVAS SCHEMA').parentElement;
// While CTAS & CVAS are unchecked, schema field is not visible
expect(schemaField).not.toHaveClass('open');
// Check both "Allow CTAS" and "Allow CVAS" to reveal schema field
userEvent.click(allowCTAS);
userEvent.click(allowCVAS);
expect(schemaField).toHaveClass('open');
// Uncheck both "Allow CTAS" and "Allow CVAS" to hide schema field again
userEvent.click(allowCTAS);
userEvent.click(allowCVAS);
// Both checkboxes go unchecked, so the field should no longer render
expect(schemaField).not.toHaveClass('open');
});
describe('create database', () => {
it('should show a form when dynamic_form is selected', async () => {
const props = {
...dbProps,
databaseId: null,
database_name: null,
sqlalchemy_uri: null,
};
render(<DatabaseModal {...props} />, { useRedux: true });
// it should have the correct header text
const headerText = screen.getByText(/connect a database/i);
expect(headerText).toBeVisible();
await screen.findByText(/display name/i);
// it does not fetch any databases if no id is passed in
expect(fetchMock.calls().length).toEqual(0);
// todo we haven't hooked this up to load dynamically yet so
// we can't currently test it
});
});
describe('edit database', () => {
it('renders the sqlalchemy form when the sqlalchemy_form configuration method is set', async () => {
render(<DatabaseModal {...dbProps} />, { useRedux: true });
// it should have tabs
const tabs = screen.getAllByRole('tab');
expect(tabs.length).toEqual(2);
expect(tabs[0]).toHaveTextContent('Basic');
expect(tabs[1]).toHaveTextContent('Advanced');
// it should have the correct header text
const headerText = screen.getByText(/edit database/i);
expect(headerText).toBeVisible();
// todo add more when this form is built out
});
it('renders the dynamic form when the dynamic_form configuration method is set', async () => {
fetchMock.get(DATABASE_ENDPOINT, {
result: {
id: 10,
database_name: 'my database',
expose_in_sqllab: false,
allow_ctas: false,
allow_cvas: false,
configuration_method: 'dynamic_form',
parameters: {
database: 'mydatabase',
},
describe('edit database', () => {
beforeEach(() => {
fetchMock.mock(AVAILABLE_DB_ENDPOINT, {
databases: [
{
engine: 'mysql',
name: 'MySQL',
preferred: false,
},
});
render(<DatabaseModal {...dbProps} />, { useRedux: true });
await screen.findByText(/todo/i);
// // it should have tabs
const tabs = screen.getAllByRole('tab');
expect(tabs.length).toEqual(2);
// it should show a TODO for now
const todoText = screen.getAllByText(/todo/i);
expect(todoText[0]).toBeVisible();
],
});
});
it('renders the sqlalchemy form when the sqlalchemy_form configuration method is set', async () => {
render(<DatabaseModal {...dbProps} />, { useRedux: true });
// it should have tabs
const tabs = screen.getAllByRole('tab');
expect(tabs.length).toEqual(2);
expect(tabs[0]).toHaveTextContent('Basic');
expect(tabs[1]).toHaveTextContent('Advanced');
// it should have the correct header text
const headerText = screen.getByText(/edit database/i);
expect(headerText).toBeVisible();
// todo add more when this form is built out
});
it('renders the dynamic form when the dynamic_form configuration method is set', async () => {
fetchMock.get(DATABASE_FETCH_ENDPOINT, {
result: {
id: 10,
database_name: 'my database',
expose_in_sqllab: false,
allow_ctas: false,
allow_cvas: false,
configuration_method: 'dynamic_form',
parameters: {
database: 'mydatabase',
},
},
});
render(<DatabaseModal {...dbProps} />, { useRedux: true });
await screen.findByText(/todo/i);
// // it should have tabs
const tabs = screen.getAllByRole('tab');
expect(tabs.length).toEqual(2);
// it should show a TODO for now
const todoText = screen.getAllByText(/todo/i);
expect(todoText[0]).toBeVisible();
});
});
});

View File

@ -202,7 +202,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
const isEditMode = !!databaseId;
const useSqlAlchemyForm =
db?.configuration_method === CONFIGURATION_METHOD.SQLALCHEMY_URI;
const useTabLayout = isEditMode || useSqlAlchemyForm;
// Database fetch logic
const {
state: { loading: dbLoading, resource: dbFetched },
@ -240,7 +240,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
onHide();
};
const onSave = () => {
const onSave = async () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { id, ...update } = db || {};
if (db?.id) {
@ -258,14 +258,18 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
});
} else if (db) {
// Create
createResource(update as DatabaseObject).then(dbId => {
if (dbId) {
setHasConnectedDb(true);
if (onDatabaseAdd) {
onDatabaseAdd();
}
const dbId = await createResource(update as DatabaseObject);
if (dbId) {
setHasConnectedDb(true);
if (onDatabaseAdd) {
onDatabaseAdd();
}
});
if (useTabLayout) {
// tab layout only has one step
// so it should close immediately on save
onClose();
}
}
}
};
@ -336,7 +340,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
FALSY_FORM_VALUES.includes(db?.parameters?.[field]),
).length);
return isEditMode || useSqlAlchemyForm ? (
return useTabLayout ? (
<Modal
css={(theme: SupersetTheme) => [
antDTabsStyles,
@ -346,6 +350,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
]}
name="database"
disablePrimaryButton={disableSave}
data-test="database-modal"
height="600px"
onHandledPrimaryAction={onSave}
onHide={onClose}