fix: make database connection modal ace fields uncontrolled (#22350)

This commit is contained in:
Elizabeth Thompson
2022-12-12 12:36:41 -08:00
committed by GitHub
parent c3a6327ff0
commit 608ffcbfb9
3 changed files with 57 additions and 50 deletions

View File

@@ -50,7 +50,16 @@ const ExtraOptions = ({
const createAsOpen = !!(db?.allow_ctas || db?.allow_cvas); const createAsOpen = !!(db?.allow_ctas || db?.allow_cvas);
const isFileUploadSupportedByEngine = const isFileUploadSupportedByEngine =
db?.engine_information?.supports_file_upload; db?.engine_information?.supports_file_upload;
const extraJson: ExtraJson = JSON.parse(db?.extra || '{}');
// JSON.parse will deep parse engine_params
// if it's an object, and we want to keep it a string
const extraJson: ExtraJson = JSON.parse(db?.extra || '{}', (key, value) => {
if (key === 'engine_params' && typeof value === 'object') {
// keep this as a string
return JSON.stringify(value);
}
return value;
});
return ( return (
<Collapse <Collapse
@@ -123,9 +132,9 @@ const ExtraOptions = ({
<input <input
type="text" type="text"
name="force_ctas_schema" name="force_ctas_schema"
value={db?.force_ctas_schema || ''}
placeholder={t('Create or select schema...')} placeholder={t('Create or select schema...')}
onChange={onInputChange} onChange={onInputChange}
value={db?.force_ctas_schema || ''}
/> />
</div> </div>
<div className="helper"> <div className="helper">
@@ -442,17 +451,17 @@ const ExtraOptions = ({
<div className="input-container"> <div className="input-container">
<StyledJsonEditor <StyledJsonEditor
name="metadata_params" name="metadata_params"
value={
!Object.keys(extraJson?.metadata_params || {}).length
? ''
: extraJson?.metadata_params
}
placeholder={t('Metadata Parameters')} placeholder={t('Metadata Parameters')}
onChange={(json: string) => onChange={(json: string) =>
onExtraEditorChange({ json, name: 'metadata_params' }) onExtraEditorChange({ json, name: 'metadata_params' })
} }
width="100%" width="100%"
height="160px" height="160px"
defaultValue={
!Object.keys(extraJson?.metadata_params || {}).length
? ''
: extraJson?.metadata_params
}
/> />
</div> </div>
<div className="helper"> <div className="helper">
@@ -468,17 +477,17 @@ const ExtraOptions = ({
<div className="input-container"> <div className="input-container">
<StyledJsonEditor <StyledJsonEditor
name="engine_params" name="engine_params"
value={
!Object.keys(extraJson?.engine_params || {}).length
? ''
: extraJson?.engine_params
}
placeholder={t('Engine Parameters')} placeholder={t('Engine Parameters')}
onChange={(json: string) => onChange={(json: string) =>
onExtraEditorChange({ json, name: 'engine_params' }) onExtraEditorChange({ json, name: 'engine_params' })
} }
width="100%" width="100%"
height="160px" height="160px"
defaultValue={
!Object.keys(extraJson?.engine_params || {}).length
? ''
: extraJson?.engine_params
}
/> />
</div> </div>
<div className="helper"> <div className="helper">
@@ -497,9 +506,9 @@ const ExtraOptions = ({
<input <input
type="number" type="number"
name="version" name="version"
value={extraJson?.version || ''}
placeholder={t('Version number')} placeholder={t('Version number')}
onChange={onExtraInputChange} onChange={onExtraInputChange}
value={extraJson?.version || ''}
/> />
</div> </div>
<div className="helper"> <div className="helper">

View File

@@ -25,12 +25,13 @@ import {
within, within,
cleanup, cleanup,
act, act,
waitFor,
} from 'spec/helpers/testing-library'; } from 'spec/helpers/testing-library';
import * as hooks from 'src/views/CRUD/hooks';
import { import {
DatabaseObject, DatabaseObject,
CONFIGURATION_METHOD, CONFIGURATION_METHOD,
} from 'src/views/CRUD/data/database/types'; } from 'src/views/CRUD/data/database/types';
import * as hooks from 'src/views/CRUD/hooks';
import DatabaseModal, { import DatabaseModal, {
dbReducer, dbReducer,
DBReducerActionType, DBReducerActionType,
@@ -47,6 +48,20 @@ const dbProps = {
const DATABASE_FETCH_ENDPOINT = 'glob:*/api/v1/database/10'; const DATABASE_FETCH_ENDPOINT = 'glob:*/api/v1/database/10';
const AVAILABLE_DB_ENDPOINT = 'glob:*/api/v1/database/available*'; const AVAILABLE_DB_ENDPOINT = 'glob:*/api/v1/database/available*';
const VALIDATE_PARAMS_ENDPOINT = 'glob:*/api/v1/database/validate_parameters*'; const VALIDATE_PARAMS_ENDPOINT = 'glob:*/api/v1/database/validate_parameters*';
const DATABASE_CONNECT_ENDPOINT = 'glob:*/api/v1/database/';
fetchMock.post(DATABASE_CONNECT_ENDPOINT, {
id: 10,
result: {
configuration_method: 'sqlalchemy_form',
database_name: 'Other2',
driver: 'apsw',
expose_in_sqllab: true,
extra: '{"allows_virtual_table_explore":true}',
sqlalchemy_uri: 'gsheets://',
},
json: 'foo',
});
fetchMock.config.overwriteRoutes = true; fetchMock.config.overwriteRoutes = true;
fetchMock.get(DATABASE_FETCH_ENDPOINT, { fetchMock.get(DATABASE_FETCH_ENDPOINT, {
@@ -1178,6 +1193,7 @@ describe('DatabaseModal', () => {
const databaseNameField = textboxes[1]; const databaseNameField = textboxes[1];
const usernameField = textboxes[2]; const usernameField = textboxes[2];
const passwordField = textboxes[3]; const passwordField = textboxes[3];
const connectButton = screen.getByRole('button', { name: 'Connect' });
expect(hostField).toHaveValue(''); expect(hostField).toHaveValue('');
expect(portField).toHaveValue(null); expect(portField).toHaveValue(null);
@@ -1198,19 +1214,10 @@ describe('DatabaseModal', () => {
expect(usernameField).toHaveValue('testdb'); expect(usernameField).toHaveValue('testdb');
expect(passwordField).toHaveValue('demoPassword'); expect(passwordField).toHaveValue('demoPassword');
/* ---------- 🐞 TODO (lyndsiWilliams): function mock is not currently working 🐞 ---------- userEvent.click(connectButton);
await waitFor(() => {
// Mock useSingleViewResource expect(fetchMock.calls(VALIDATE_PARAMS_ENDPOINT).length).toEqual(6);
const mockUseSingleViewResource = jest.fn(); });
mockUseSingleViewResource.mockImplementation(useSingleViewResource);
const { fetchResource } = mockUseSingleViewResource('database');
// Invalid hook call?
userEvent.click(screen.getByRole('button', { name: 'Connect' }));
expect(fetchResource).toHaveBeenCalled();
*/
}); });
}); });
@@ -1325,23 +1332,6 @@ describe('DatabaseModal', () => {
...jest.requireActual('src/views/CRUD/hooks'), ...jest.requireActual('src/views/CRUD/hooks'),
useSingleViewResource: jest.fn(), useSingleViewResource: jest.fn(),
})); }));
const useSingleViewResourceMock = jest.spyOn(
hooks,
'useSingleViewResource',
);
useSingleViewResourceMock.mockReturnValue({
state: {
loading: false,
resource: null,
error: { _schema: 'Test Error With Object' },
},
fetchResource: jest.fn(),
createResource: jest.fn(),
updateResource: jest.fn(),
clearError: jest.fn(),
setResource: jest.fn(),
});
const renderAndWait = async () => { const renderAndWait = async () => {
const mounted = act(async () => { const mounted = act(async () => {
@@ -1442,7 +1432,7 @@ describe('dbReducer', () => {
test('it will set state to payload from extra editor', () => { test('it will set state to payload from extra editor', () => {
const action: DBReducerActionType = { const action: DBReducerActionType = {
type: ActionType.extraEditorChange, type: ActionType.extraEditorChange,
payload: { name: 'foo', json: { bar: 1 } }, payload: { name: 'foo', json: JSON.stringify({ bar: 1 }) },
}; };
const currentState = dbReducer(databaseFixture, action); const currentState = dbReducer(databaseFixture, action);
// extra should be serialized // extra should be serialized
@@ -1455,20 +1445,20 @@ describe('dbReducer', () => {
test('it will set state to payload from editor', () => { test('it will set state to payload from editor', () => {
const action: DBReducerActionType = { const action: DBReducerActionType = {
type: ActionType.editorChange, type: ActionType.editorChange,
payload: { name: 'foo', json: { bar: 1 } }, payload: { name: 'foo', json: JSON.stringify({ bar: 1 }) },
}; };
const currentState = dbReducer(databaseFixture, action); const currentState = dbReducer(databaseFixture, action);
// extra should be serialized // extra should be serialized
expect(currentState).toEqual({ expect(currentState).toEqual({
...databaseFixture, ...databaseFixture,
foo: { bar: 1 }, foo: JSON.stringify({ bar: 1 }),
}); });
}); });
test('it will add extra payload to existing extra data', () => { test('it will add extra payload to existing extra data', () => {
const action: DBReducerActionType = { const action: DBReducerActionType = {
type: ActionType.extraEditorChange, type: ActionType.extraEditorChange,
payload: { name: 'foo', json: { bar: 1 } }, payload: { name: 'foo', json: JSON.stringify({ bar: 1 }) },
}; };
// extra should be a string // extra should be a string
const currentState = dbReducer( const currentState = dbReducer(

View File

@@ -150,7 +150,7 @@ export enum ActionType {
interface DBReducerPayloadType { interface DBReducerPayloadType {
target?: string; target?: string;
name: string; name: string;
json?: {}; json?: string;
type?: string; type?: string;
checked?: boolean; checked?: boolean;
value?: string; value?: string;
@@ -215,20 +215,28 @@ export function dbReducer(
let query = {}; let query = {};
let query_input = ''; let query_input = '';
let parametersCatalog; let parametersCatalog;
let actionPayloadJson;
const extraJson: ExtraJson = JSON.parse(trimmedState.extra || '{}'); const extraJson: ExtraJson = JSON.parse(trimmedState.extra || '{}');
switch (action.type) { switch (action.type) {
case ActionType.extraEditorChange: case ActionType.extraEditorChange:
// "extra" payload in state is a string // "extra" payload in state is a string
try {
// we don't want to stringify encoded strings twice
actionPayloadJson = JSON.parse(action.payload.json || '{}');
} catch (e) {
actionPayloadJson = action.payload.json;
}
return { return {
...trimmedState, ...trimmedState,
extra: JSON.stringify({ extra: JSON.stringify({
...extraJson, ...extraJson,
[action.payload.name]: action.payload.json, [action.payload.name]: actionPayloadJson,
}), }),
}; };
case ActionType.extraInputChange: case ActionType.extraInputChange:
// "extra" payload in state is a string // "extra" payload in state is a string
if ( if (
action.payload.name === 'schema_cache_timeout' || action.payload.name === 'schema_cache_timeout' ||
action.payload.name === 'table_cache_timeout' action.payload.name === 'table_cache_timeout'