refactor: serialize extra json in state (#21523)

This commit is contained in:
Elizabeth Thompson 2022-10-14 14:55:20 -07:00 committed by GitHub
parent df3b5a8305
commit 196c3671e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 535 additions and 147 deletions

View File

@ -29,7 +29,7 @@ import {
antdCollapseStyles, antdCollapseStyles,
no_margin_bottom, no_margin_bottom,
} from './styles'; } from './styles';
import { DatabaseObject } from '../types'; import { DatabaseObject, ExtraJson } from '../types';
const ExtraOptions = ({ const ExtraOptions = ({
db, db,
@ -50,6 +50,7 @@ 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 || '{}');
return ( return (
<Collapse <Collapse
@ -155,7 +156,7 @@ const ExtraOptions = ({
<IndeterminateCheckbox <IndeterminateCheckbox
id="cost_estimate_enabled" id="cost_estimate_enabled"
indeterminate={false} indeterminate={false}
checked={!!db?.extra_json?.cost_estimate_enabled} checked={!!extraJson?.cost_estimate_enabled}
onChange={onExtraInputChange} onChange={onExtraInputChange}
labelText={t('Enable query cost estimation')} labelText={t('Enable query cost estimation')}
/> />
@ -171,7 +172,7 @@ const ExtraOptions = ({
<IndeterminateCheckbox <IndeterminateCheckbox
id="allows_virtual_table_explore" id="allows_virtual_table_explore"
indeterminate={false} indeterminate={false}
checked={!!db?.extra_json?.allows_virtual_table_explore} checked={!!extraJson?.allows_virtual_table_explore}
onChange={onExtraInputChange} onChange={onExtraInputChange}
labelText={t('Allow this database to be explored')} labelText={t('Allow this database to be explored')}
/> />
@ -187,7 +188,7 @@ const ExtraOptions = ({
<IndeterminateCheckbox <IndeterminateCheckbox
id="disable_data_preview" id="disable_data_preview"
indeterminate={false} indeterminate={false}
checked={!!db?.extra_json?.disable_data_preview} checked={!!extraJson?.disable_data_preview}
onChange={onExtraInputChange} onChange={onExtraInputChange}
labelText={t('Disable SQL Lab data preview queries')} labelText={t('Disable SQL Lab data preview queries')}
/> />
@ -240,8 +241,7 @@ const ExtraOptions = ({
type="number" type="number"
name="schema_cache_timeout" name="schema_cache_timeout"
value={ value={
db?.extra_json?.metadata_cache_timeout?.schema_cache_timeout || extraJson?.metadata_cache_timeout?.schema_cache_timeout || ''
''
} }
placeholder={t('Enter duration in seconds')} placeholder={t('Enter duration in seconds')}
onChange={onExtraInputChange} onChange={onExtraInputChange}
@ -262,8 +262,7 @@ const ExtraOptions = ({
type="number" type="number"
name="table_cache_timeout" name="table_cache_timeout"
value={ value={
db?.extra_json?.metadata_cache_timeout?.table_cache_timeout || extraJson?.metadata_cache_timeout?.table_cache_timeout || ''
''
} }
placeholder={t('Enter duration in seconds')} placeholder={t('Enter duration in seconds')}
onChange={onExtraInputChange} onChange={onExtraInputChange}
@ -301,7 +300,7 @@ const ExtraOptions = ({
<IndeterminateCheckbox <IndeterminateCheckbox
id="cancel_query_on_windows_unload" id="cancel_query_on_windows_unload"
indeterminate={false} indeterminate={false}
checked={!!db?.extra_json?.cancel_query_on_windows_unload} checked={!!extraJson?.cancel_query_on_windows_unload}
onChange={onExtraInputChange} onChange={onExtraInputChange}
labelText={t('Cancel query on window unload event')} labelText={t('Cancel query on window unload event')}
/> />
@ -414,9 +413,9 @@ const ExtraOptions = ({
<input <input
type="text" type="text"
name="schemas_allowed_for_file_upload" name="schemas_allowed_for_file_upload"
value={( value={(extraJson?.schemas_allowed_for_file_upload || []).join(
db?.extra_json?.schemas_allowed_for_file_upload || [] ',',
).join(',')} )}
placeholder="schema1,schema2" placeholder="schema1,schema2"
onChange={onExtraInputChange} onChange={onExtraInputChange}
/> />
@ -443,7 +442,11 @@ const ExtraOptions = ({
<div className="input-container"> <div className="input-container">
<StyledJsonEditor <StyledJsonEditor
name="metadata_params" name="metadata_params"
value={db?.extra_json?.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' })
@ -465,7 +468,11 @@ const ExtraOptions = ({
<div className="input-container"> <div className="input-container">
<StyledJsonEditor <StyledJsonEditor
name="engine_params" name="engine_params"
value={db?.extra_json?.engine_params || ''} value={
!Object.keys(extraJson?.engine_params || {}).length
? ''
: JSON.stringify(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' })
@ -490,7 +497,7 @@ const ExtraOptions = ({
<input <input
type="number" type="number"
name="version" name="version"
value={db?.extra_json?.version || ''} value={extraJson?.version || ''}
placeholder={t('Version number')} placeholder={t('Version number')}
onChange={onExtraInputChange} onChange={onExtraInputChange}
/> />

View File

@ -27,12 +27,21 @@ import {
act, act,
} from 'spec/helpers/testing-library'; } from 'spec/helpers/testing-library';
import * as hooks from 'src/views/CRUD/hooks'; import * as hooks from 'src/views/CRUD/hooks';
import DatabaseModal from './index'; import {
DatabaseObject,
CONFIGURATION_METHOD,
} from 'src/views/CRUD/data/database/types';
import DatabaseModal, {
dbReducer,
DBReducerActionType,
ActionType,
} from './index';
const dbProps = { const dbProps = {
show: true, show: true,
database_name: 'my database', database_name: 'my database',
sqlalchemy_uri: 'postgres://superset:superset@something:1234/superset', sqlalchemy_uri: 'postgres://superset:superset@something:1234/superset',
onHide: () => {},
}; };
const DATABASE_FETCH_ENDPOINT = 'glob:*/api/v1/database/10'; const DATABASE_FETCH_ENDPOINT = 'glob:*/api/v1/database/10';
@ -223,6 +232,14 @@ fetchMock.post(VALIDATE_PARAMS_ENDPOINT, {
message: 'OK', message: 'OK',
}); });
const databaseFixture: DatabaseObject = {
backend: 'postgres',
configuration_method: CONFIGURATION_METHOD.DYNAMIC_FORM,
database_name: 'Postgres',
name: 'PostgresDB',
is_managed_externally: false,
};
describe('DatabaseModal', () => { describe('DatabaseModal', () => {
const renderAndWait = async () => { const renderAndWait = async () => {
const mounted = act(async () => { const mounted = act(async () => {
@ -640,8 +657,6 @@ describe('DatabaseModal', () => {
checkboxOffSVGs[2], checkboxOffSVGs[2],
checkboxOffSVGs[3], checkboxOffSVGs[3],
checkboxOffSVGs[4], checkboxOffSVGs[4],
checkboxOffSVGs[5],
checkboxOffSVGs[6],
tooltipIcons[0], tooltipIcons[0],
tooltipIcons[1], tooltipIcons[1],
tooltipIcons[2], tooltipIcons[2],
@ -670,14 +685,13 @@ describe('DatabaseModal', () => {
allowDbExplorationCheckbox, allowDbExplorationCheckbox,
disableSQLLabDataPreviewQueriesCheckbox, disableSQLLabDataPreviewQueriesCheckbox,
]; ];
visibleComponents.forEach(component => { visibleComponents.forEach(component => {
expect(component).toBeVisible(); expect(component).toBeVisible();
}); });
invisibleComponents.forEach(component => { invisibleComponents.forEach(component => {
expect(component).not.toBeVisible(); expect(component).not.toBeVisible();
}); });
expect(checkboxOffSVGs).toHaveLength(7); expect(checkboxOffSVGs).toHaveLength(5);
expect(tooltipIcons).toHaveLength(7); expect(tooltipIcons).toHaveLength(7);
}); });
@ -1169,7 +1183,9 @@ describe('DatabaseModal', () => {
describe('Import database flow', () => { describe('Import database flow', () => {
test('imports a file', async () => { test('imports a file', async () => {
const importDbButton = screen.getByTestId('import-database-btn'); const importDbButton = screen.getByTestId(
'import-database-btn',
) as HTMLInputElement;
expect(importDbButton).toBeVisible(); expect(importDbButton).toBeVisible();
const testFile = new File([new ArrayBuffer(1)], 'model_export.zip'); const testFile = new File([new ArrayBuffer(1)], 'model_export.zip');
@ -1177,8 +1193,8 @@ describe('DatabaseModal', () => {
userEvent.click(importDbButton); userEvent.click(importDbButton);
userEvent.upload(importDbButton, testFile); userEvent.upload(importDbButton, testFile);
expect(importDbButton.files[0]).toStrictEqual(testFile); expect(importDbButton.files?.[0]).toStrictEqual(testFile);
expect(importDbButton.files.item(0)).toStrictEqual(testFile); expect(importDbButton.files?.item(0)).toStrictEqual(testFile);
expect(importDbButton.files).toHaveLength(1); expect(importDbButton.files).toHaveLength(1);
}); });
}); });
@ -1291,6 +1307,7 @@ describe('DatabaseModal', () => {
createResource: jest.fn(), createResource: jest.fn(),
updateResource: jest.fn(), updateResource: jest.fn(),
clearError: jest.fn(), clearError: jest.fn(),
setResource: jest.fn(),
}); });
const renderAndWait = async () => { const renderAndWait = async () => {
@ -1335,6 +1352,7 @@ describe('DatabaseModal', () => {
createResource: jest.fn(), createResource: jest.fn(),
updateResource: jest.fn(), updateResource: jest.fn(),
clearError: jest.fn(), clearError: jest.fn(),
setResource: jest.fn(),
}); });
const renderAndWait = async () => { const renderAndWait = async () => {
@ -1361,3 +1379,374 @@ describe('DatabaseModal', () => {
}); });
}); });
}); });
describe('dbReducer', () => {
test('it will reset state to null', () => {
const action: DBReducerActionType = { type: ActionType.reset };
const currentState = dbReducer(databaseFixture, action);
expect(currentState).toBeNull();
});
test('it will set state to payload from fetched', () => {
const action: DBReducerActionType = {
type: ActionType.fetched,
payload: databaseFixture,
};
const currentState = dbReducer({}, action);
expect(currentState).toEqual({
...databaseFixture,
engine: 'postgres',
masked_encrypted_extra: '',
parameters: undefined,
query_input: '',
});
});
test('it will set state to payload from extra editor', () => {
const action: DBReducerActionType = {
type: ActionType.extraEditorChange,
payload: { name: 'foo', json: { bar: 1 } },
};
const currentState = dbReducer(databaseFixture, action);
// extra should be serialized
expect(currentState).toEqual({
...databaseFixture,
extra: '{"foo":{"bar":1}}',
});
});
test('it will set state to payload from editor', () => {
const action: DBReducerActionType = {
type: ActionType.editorChange,
payload: { name: 'foo', json: { bar: 1 } },
};
const currentState = dbReducer(databaseFixture, action);
// extra should be serialized
expect(currentState).toEqual({
...databaseFixture,
foo: { bar: 1 },
});
});
test('it will add extra payload to existing extra data', () => {
const action: DBReducerActionType = {
type: ActionType.extraEditorChange,
payload: { name: 'foo', json: { bar: 1 } },
};
// extra should be a string
const currentState = dbReducer(
{
...databaseFixture,
extra: JSON.stringify({ name: 'baz', json: { fiz: 2 } }),
},
action,
);
// extra should be serialized
expect(currentState).toEqual({
...databaseFixture,
extra: '{"name":"baz","json":{"fiz":2},"foo":{"bar":1}}',
});
});
test('it will set state to payload from extra input change', () => {
const action: DBReducerActionType = {
type: ActionType.extraInputChange,
payload: { name: 'foo', value: 'bar' },
};
const currentState = dbReducer(databaseFixture, action);
// extra should be serialized
expect(currentState).toEqual({
...databaseFixture,
extra: '{"foo":"bar"}',
});
});
test('it will set state to payload from extra input change when checkbox', () => {
const action: DBReducerActionType = {
type: ActionType.extraInputChange,
payload: { name: 'foo', type: 'checkbox', checked: true },
};
const currentState = dbReducer(databaseFixture, action);
// extra should be serialized
expect(currentState).toEqual({
...databaseFixture,
extra: '{"foo":true}',
});
});
test('it will set state to payload from extra input change when schema_cache_timeout', () => {
const action: DBReducerActionType = {
type: ActionType.extraInputChange,
payload: { name: 'schema_cache_timeout', value: 'bar' },
};
const currentState = dbReducer(databaseFixture, action);
// extra should be serialized
expect(currentState).toEqual({
...databaseFixture,
extra: '{"metadata_cache_timeout":{"schema_cache_timeout":"bar"}}',
});
});
test('it will set state to payload from extra input change when table_cache_timeout', () => {
const action: DBReducerActionType = {
type: ActionType.extraInputChange,
payload: { name: 'table_cache_timeout', value: 'bar' },
};
const currentState = dbReducer(databaseFixture, action);
// extra should be serialized
expect(currentState).toEqual({
...databaseFixture,
extra: '{"metadata_cache_timeout":{"table_cache_timeout":"bar"}}',
});
});
test('it will overwrite state to payload from extra input change when table_cache_timeout', () => {
const action: DBReducerActionType = {
type: ActionType.extraInputChange,
payload: { name: 'table_cache_timeout', value: 'bar' },
};
const currentState = dbReducer(
{
...databaseFixture,
extra: '{"metadata_cache_timeout":{"table_cache_timeout":"foo"}}',
},
action,
);
// extra should be serialized
expect(currentState).toEqual({
...databaseFixture,
extra: '{"metadata_cache_timeout":{"table_cache_timeout":"bar"}}',
});
});
test(`it will set state to payload from extra
input change when schemas_allowed_for_file_upload`, () => {
const action: DBReducerActionType = {
type: ActionType.extraInputChange,
payload: { name: 'schemas_allowed_for_file_upload', value: 'bar' },
};
const currentState = dbReducer(databaseFixture, action);
// extra should be serialized
expect(currentState).toEqual({
...databaseFixture,
extra: '{"schemas_allowed_for_file_upload":["bar"]}',
});
});
test(`it will overwrite state to payload from extra
input change when schemas_allowed_for_file_upload`, () => {
const action: DBReducerActionType = {
type: ActionType.extraInputChange,
payload: { name: 'schemas_allowed_for_file_upload', value: 'bar' },
};
const currentState = dbReducer(
{
...databaseFixture,
extra: '{"schemas_allowed_for_file_upload":["foo"]}',
},
action,
);
// extra should be serialized
expect(currentState).toEqual({
...databaseFixture,
extra: '{"schemas_allowed_for_file_upload":["bar"]}',
});
});
test(`it will set state to payload from extra
input change when schemas_allowed_for_file_upload
with blank list`, () => {
const action: DBReducerActionType = {
type: ActionType.extraInputChange,
payload: { name: 'schemas_allowed_for_file_upload', value: 'bar,' },
};
const currentState = dbReducer(databaseFixture, action);
// extra should be serialized
expect(currentState).toEqual({
...databaseFixture,
extra: '{"schemas_allowed_for_file_upload":["bar"]}',
});
});
test('it will set state to payload from input change', () => {
const action: DBReducerActionType = {
type: ActionType.inputChange,
payload: { name: 'foo', value: 'bar' },
};
const currentState = dbReducer(databaseFixture, action);
expect(currentState).toEqual({
...databaseFixture,
foo: 'bar',
});
});
test('it will set state to payload from input change for checkbox', () => {
const action: DBReducerActionType = {
type: ActionType.inputChange,
payload: { name: 'foo', type: 'checkbox', checked: true },
};
const currentState = dbReducer(databaseFixture, action);
expect(currentState).toEqual({
...databaseFixture,
foo: true,
});
});
test('it will change state to payload from input change for checkbox', () => {
const action: DBReducerActionType = {
type: ActionType.inputChange,
payload: { name: 'allow_ctas', type: 'checkbox', checked: false },
};
const currentState = dbReducer(
{
...databaseFixture,
allow_ctas: true,
},
action,
);
expect(currentState).toEqual({
...databaseFixture,
allow_ctas: false,
});
});
test('it will add a parameter', () => {
const action: DBReducerActionType = {
type: ActionType.parametersChange,
payload: { name: 'host', value: '127.0.0.1' },
};
const currentState = dbReducer(databaseFixture, action);
expect(currentState).toEqual({
...databaseFixture,
parameters: {
host: '127.0.0.1',
},
});
});
test('it will add a parameter with existing parameters', () => {
const action: DBReducerActionType = {
type: ActionType.parametersChange,
payload: { name: 'port', value: '1234' },
};
const currentState = dbReducer(
{
...databaseFixture,
parameters: {
host: '127.0.0.1',
},
},
action,
);
expect(currentState).toEqual({
...databaseFixture,
parameters: {
host: '127.0.0.1',
port: '1234',
},
});
});
test('it will change a parameter with existing parameters', () => {
const action: DBReducerActionType = {
type: ActionType.parametersChange,
payload: { name: 'host', value: 'localhost' },
};
const currentState = dbReducer(
{
...databaseFixture,
parameters: {
host: '127.0.0.1',
},
},
action,
);
expect(currentState).toEqual({
...databaseFixture,
parameters: {
host: 'localhost',
},
});
});
test('it will set state to payload from parametersChange with catalog', () => {
const action: DBReducerActionType = {
type: ActionType.parametersChange,
payload: { name: 'name', type: 'catalog-0', value: 'bar' },
};
const currentState = dbReducer(
{ ...databaseFixture, catalog: [{ name: 'foo', value: 'baz' }] },
action,
);
expect(currentState).toEqual({
...databaseFixture,
catalog: [{ name: 'bar', value: 'baz' }],
parameters: {
catalog: {
bar: 'baz',
},
},
});
});
test('it will add a new catalog array when empty', () => {
const action: DBReducerActionType = {
type: ActionType.addTableCatalogSheet,
};
const currentState = dbReducer(databaseFixture, action);
expect(currentState).toEqual({
...databaseFixture,
catalog: [{ name: '', value: '' }],
});
});
test('it will add a new catalog array when one exists', () => {
const action: DBReducerActionType = {
type: ActionType.addTableCatalogSheet,
};
const currentState = dbReducer(
{ ...databaseFixture, catalog: [{ name: 'foo', value: 'baz' }] },
action,
);
expect(currentState).toEqual({
...databaseFixture,
catalog: [
{ name: 'foo', value: 'baz' },
{ name: '', value: '' },
],
});
});
test('it will remove a catalog when one exists', () => {
const action: DBReducerActionType = {
type: ActionType.removeTableCatalogSheet,
payload: { indexToDelete: 0 },
};
const currentState = dbReducer(
{ ...databaseFixture, catalog: [{ name: 'foo', value: 'baz' }] },
action,
);
expect(currentState).toEqual({
...databaseFixture,
catalog: [],
});
});
});

View File

@ -60,6 +60,7 @@ import {
CONFIGURATION_METHOD, CONFIGURATION_METHOD,
CatalogObject, CatalogObject,
Engines, Engines,
ExtraJson,
} from 'src/views/CRUD/data/database/types'; } from 'src/views/CRUD/data/database/types';
import Loading from 'src/components/Loading'; import Loading from 'src/components/Loading';
import ExtraOptions from './ExtraOptions'; import ExtraOptions from './ExtraOptions';
@ -88,6 +89,8 @@ import {
} from './styles'; } from './styles';
import ModalHeader, { DOCUMENTATION_LINK } from './ModalHeader'; import ModalHeader, { DOCUMENTATION_LINK } from './ModalHeader';
const DEFAULT_EXTRA = JSON.stringify({ allows_virtual_table_explore: true });
const engineSpecificAlertMapping = { const engineSpecificAlertMapping = {
[Engines.GSheet]: { [Engines.GSheet]: {
message: 'Why do I need to create a database?', message: 'Why do I need to create a database?',
@ -121,14 +124,14 @@ const ErrorAlertContainer = styled.div`
interface DatabaseModalProps { interface DatabaseModalProps {
addDangerToast: (msg: string) => void; addDangerToast: (msg: string) => void;
addSuccessToast: (msg: string) => void; addSuccessToast: (msg: string) => void;
onDatabaseAdd?: (database?: DatabaseObject) => void; // TODO: should we add a separate function for edit? onDatabaseAdd?: (database?: DatabaseObject) => void;
onHide: () => void; onHide: () => void;
show: boolean; show: boolean;
databaseId: number | undefined; // If included, will go into edit mode databaseId: number | undefined; // If included, will go into edit mode
dbEngine: string | undefined; // if included goto step 2 with engine already set dbEngine: string | undefined; // if included goto step 2 with engine already set
} }
enum ActionType { export enum ActionType {
configMethodChange, configMethodChange,
dbSelected, dbSelected,
editorChange, editorChange,
@ -153,7 +156,7 @@ interface DBReducerPayloadType {
value?: string; value?: string;
} }
type DBReducerActionType = export type DBReducerActionType =
| { | {
type: type:
| ActionType.extraEditorChange | ActionType.extraEditorChange
@ -201,7 +204,7 @@ const StyledBtns = styled.div`
margin-left: ${({ theme }) => theme.gridUnit * 3}px; margin-left: ${({ theme }) => theme.gridUnit * 3}px;
`; `;
function dbReducer( export function dbReducer(
state: Partial<DatabaseObject> | null, state: Partial<DatabaseObject> | null,
action: DBReducerActionType, action: DBReducerActionType,
): Partial<DatabaseObject> | null { ): Partial<DatabaseObject> | null {
@ -210,54 +213,56 @@ function dbReducer(
}; };
let query = {}; let query = {};
let query_input = ''; let query_input = '';
let deserializeExtraJSON = { allows_virtual_table_explore: true }; let parametersCatalog;
let extra_json: DatabaseObject['extra_json']; 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
return { return {
...trimmedState, ...trimmedState,
extra_json: { extra: JSON.stringify({
...trimmedState.extra_json, ...extraJson,
[action.payload.name]: action.payload.json, [action.payload.name]: action.payload.json,
}, }),
}; };
case ActionType.extraInputChange: case ActionType.extraInputChange:
// "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'
) { ) {
return { return {
...trimmedState, ...trimmedState,
extra_json: { extra: JSON.stringify({
...trimmedState.extra_json, ...extraJson,
metadata_cache_timeout: { metadata_cache_timeout: {
...trimmedState.extra_json?.metadata_cache_timeout, ...extraJson?.metadata_cache_timeout,
[action.payload.name]: action.payload.value, [action.payload.name]: action.payload.value,
}, },
}, }),
}; };
} }
if (action.payload.name === 'schemas_allowed_for_file_upload') { if (action.payload.name === 'schemas_allowed_for_file_upload') {
return { return {
...trimmedState, ...trimmedState,
extra_json: { extra: JSON.stringify({
...trimmedState.extra_json, ...extraJson,
schemas_allowed_for_file_upload: (action.payload.value || '').split( schemas_allowed_for_file_upload: (action.payload.value || '')
',', .split(',')
), .filter(schema => schema !== ''),
}, }),
}; };
} }
return { return {
...trimmedState, ...trimmedState,
extra_json: { extra: JSON.stringify({
...trimmedState.extra_json, ...extraJson,
[action.payload.name]: [action.payload.name]:
action.payload.type === 'checkbox' action.payload.type === 'checkbox'
? action.payload.checked ? action.payload.checked
: action.payload.value, : action.payload.value,
}, }),
}; };
case ActionType.inputChange: case ActionType.inputChange:
if (action.payload.type === 'checkbox') { if (action.payload.type === 'checkbox') {
@ -271,26 +276,36 @@ function dbReducer(
[action.payload.name]: action.payload.value, [action.payload.name]: action.payload.value,
}; };
case ActionType.parametersChange: case ActionType.parametersChange:
// catalog params will always have a catalog state for
// dbs that use a catalog, i.e., gsheets, even if the
// fields are empty strings
if ( if (
trimmedState.catalog !== undefined && action.payload.type?.startsWith('catalog') &&
action.payload.type?.startsWith('catalog') trimmedState.catalog !== undefined
) { ) {
// Formatting wrapping google sheets table catalog // Formatting wrapping google sheets table catalog
const catalogCopy: CatalogObject[] = [...trimmedState.catalog];
const idx = action.payload.type?.split('-')[1]; const idx = action.payload.type?.split('-')[1];
const catalogToUpdate = trimmedState?.catalog[idx] || {}; const catalogToUpdate: CatalogObject = catalogCopy[idx] || {};
catalogToUpdate[action.payload.name] = action.payload.value; catalogToUpdate[action.payload.name] = action.payload.value;
const paramatersCatalog = {}; // insert updated catalog to existing state
catalogCopy.splice(parseInt(idx, 10), 1, catalogToUpdate);
// format catalog for state
// eslint-disable-next-line array-callback-return // eslint-disable-next-line array-callback-return
trimmedState.catalog?.map((item: CatalogObject) => { parametersCatalog = catalogCopy.reduce((obj, item: any) => {
paramatersCatalog[item.name] = item.value; const catalog = { ...obj };
}); catalog[item.name] = item.value;
return catalog;
}, {});
return { return {
...trimmedState, ...trimmedState,
catalog: catalogCopy,
parameters: { parameters: {
...trimmedState.parameters, ...trimmedState.parameters,
catalog: paramatersCatalog, catalog: parametersCatalog,
}, },
}; };
} }
@ -301,6 +316,7 @@ function dbReducer(
[action.payload.name]: action.payload.value, [action.payload.name]: action.payload.value,
}, },
}; };
case ActionType.addTableCatalogSheet: case ActionType.addTableCatalogSheet:
if (trimmedState.catalog !== undefined) { if (trimmedState.catalog !== undefined) {
return { return {
@ -337,22 +353,6 @@ function dbReducer(
[action.payload.name]: action.payload.value, [action.payload.name]: action.payload.value,
}; };
case ActionType.fetched: case ActionType.fetched:
// convert all the keys in this payload into strings
if (action.payload.extra) {
extra_json = {
...JSON.parse(action.payload.extra || ''),
} as DatabaseObject['extra_json'];
deserializeExtraJSON = {
...deserializeExtraJSON,
...JSON.parse(action.payload.extra || ''),
metadata_params: JSON.stringify(extra_json?.metadata_params),
engine_params: JSON.stringify(extra_json?.engine_params),
schemas_allowed_for_file_upload:
extra_json?.schemas_allowed_for_file_upload,
};
}
// convert query to a string and store in query_input // convert query to a string and store in query_input
query = action.payload?.parameters?.query || {}; query = action.payload?.parameters?.query || {};
query_input = Object.entries(query) query_input = Object.entries(query)
@ -364,19 +364,26 @@ function dbReducer(
action.payload.configuration_method === action.payload.configuration_method ===
CONFIGURATION_METHOD.DYNAMIC_FORM CONFIGURATION_METHOD.DYNAMIC_FORM
) { ) {
const engineParamsCatalog = Object.entries( // "extra" payload from the api is a string
extra_json?.engine_params?.catalog || {}, const extraJsonPayload: ExtraJson = {
).map(([key, value]) => ({ ...JSON.parse((action.payload.extra as string) || '{}'),
name: key, };
value,
})); const payloadCatalog = extraJsonPayload.engine_params?.catalog;
const engineRootCatalog = Object.entries(payloadCatalog || {}).map(
([name, value]: string[]) => ({ name, value }),
);
return { return {
...action.payload, ...action.payload,
engine: action.payload.backend || trimmedState.engine, engine: action.payload.backend || trimmedState.engine,
configuration_method: action.payload.configuration_method, configuration_method: action.payload.configuration_method,
extra_json: deserializeExtraJSON, catalog: engineRootCatalog,
catalog: engineParamsCatalog, parameters: {
parameters: action.payload.parameters || trimmedState.parameters, ...(action.payload.parameters || trimmedState.parameters),
catalog: payloadCatalog,
},
query_input, query_input,
}; };
} }
@ -385,16 +392,17 @@ function dbReducer(
masked_encrypted_extra: action.payload.masked_encrypted_extra || '', masked_encrypted_extra: action.payload.masked_encrypted_extra || '',
engine: action.payload.backend || trimmedState.engine, engine: action.payload.backend || trimmedState.engine,
configuration_method: action.payload.configuration_method, configuration_method: action.payload.configuration_method,
extra_json: deserializeExtraJSON,
parameters: action.payload.parameters || trimmedState.parameters, parameters: action.payload.parameters || trimmedState.parameters,
query_input, query_input,
}; };
case ActionType.dbSelected: case ActionType.dbSelected:
// set initial state for blank form
return { return {
...action.payload, ...action.payload,
extra: DEFAULT_EXTRA,
expose_in_sqllab: true,
}; };
case ActionType.configMethodChange: case ActionType.configMethodChange:
return { return {
...action.payload, ...action.payload,
@ -408,16 +416,6 @@ function dbReducer(
const DEFAULT_TAB_KEY = '1'; const DEFAULT_TAB_KEY = '1';
const serializeExtra = (extraJson: DatabaseObject['extra_json']) =>
JSON.stringify({
...extraJson,
metadata_params: JSON.parse((extraJson?.metadata_params as string) || '{}'),
engine_params: JSON.parse((extraJson?.engine_params as string) || '{}'),
schemas_allowed_for_file_upload: (
extraJson?.schemas_allowed_for_file_upload || []
).filter(schema => schema !== ''),
});
const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
addDangerToast, addDangerToast,
addSuccessToast, addSuccessToast,
@ -498,7 +496,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
sqlalchemy_uri: db?.sqlalchemy_uri || '', sqlalchemy_uri: db?.sqlalchemy_uri || '',
database_name: db?.database_name?.trim() || undefined, database_name: db?.database_name?.trim() || undefined,
impersonate_user: db?.impersonate_user || undefined, impersonate_user: db?.impersonate_user || undefined,
extra: serializeExtra(db?.extra_json) || undefined, extra: db?.extra,
masked_encrypted_extra: db?.masked_encrypted_extra || '', masked_encrypted_extra: db?.masked_encrypted_extra || '',
server_cert: db?.server_cert || undefined, server_cert: db?.server_cert || undefined,
}; };
@ -567,28 +565,25 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
const onSave = async () => { const onSave = async () => {
// Clone DB object // Clone DB object
const dbToUpdate = JSON.parse(JSON.stringify(db || {})); const dbToUpdate = { ...(db || {}) };
if (dbToUpdate.catalog) {
// convert catalog to fit /validate_parameters endpoint
dbToUpdate.catalog = Object.assign(
{},
...dbToUpdate.catalog.map((x: { name: string; value: string }) => ({
[x.name]: x.value,
})),
);
} else {
dbToUpdate.catalog = {};
}
if (dbToUpdate.configuration_method === CONFIGURATION_METHOD.DYNAMIC_FORM) { if (dbToUpdate.configuration_method === CONFIGURATION_METHOD.DYNAMIC_FORM) {
// Validate DB before saving // Validate DB before saving
if (dbToUpdate?.parameters?.catalog) {
// need to stringify gsheets catalog to allow it to be serialized
dbToUpdate.extra = JSON.stringify({
...JSON.parse(dbToUpdate.extra || '{}'),
engine_params: {
catalog: dbToUpdate.parameters.catalog,
},
});
}
const errors = await getValidation(dbToUpdate, true); const errors = await getValidation(dbToUpdate, true);
if ((validationErrors && !isEmpty(validationErrors)) || errors) { if ((validationErrors && !isEmpty(validationErrors)) || errors) {
return; return;
} }
const parameters_schema = isEditMode const parameters_schema = isEditMode
? dbToUpdate.parameters_schema.properties ? dbToUpdate.parameters_schema?.properties
: dbModel?.parameters.properties; : dbModel?.parameters.properties;
const additionalEncryptedExtra = JSON.parse( const additionalEncryptedExtra = JSON.parse(
dbToUpdate.masked_encrypted_extra || '{}', dbToUpdate.masked_encrypted_extra || '{}',
@ -633,16 +628,12 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
if (dbToUpdate?.parameters?.catalog) { if (dbToUpdate?.parameters?.catalog) {
// need to stringify gsheets catalog to allow it to be seralized // need to stringify gsheets catalog to allow it to be seralized
dbToUpdate.extra_json = { dbToUpdate.extra = JSON.stringify({
engine_params: JSON.stringify({ ...JSON.parse(dbToUpdate.extra || '{}'),
engine_params: {
catalog: dbToUpdate.parameters.catalog, catalog: dbToUpdate.parameters.catalog,
}), },
}; });
}
if (dbToUpdate?.extra_json) {
// convert extra_json to back to string
dbToUpdate.extra = serializeExtra(dbToUpdate?.extra_json);
} }
setLoading(true); setLoading(true);

View File

@ -28,14 +28,18 @@ export type CatalogObject = {
export type DatabaseObject = { export type DatabaseObject = {
// Connection + general // Connection + general
id?: number;
database_name: string;
name: string; // synonym to database_name
sqlalchemy_uri?: string;
backend?: string; backend?: string;
created_by?: null | DatabaseUser;
changed_on_delta_humanized?: string;
changed_on?: string; changed_on?: string;
changed_on_delta_humanized?: string;
configuration_method: CONFIGURATION_METHOD;
created_by?: null | DatabaseUser;
database_name: string;
engine?: string;
extra?: string;
id?: number;
name: string; // synonym to database_name
paramProperties?: Record<string, any>;
sqlalchemy_uri?: string;
parameters?: { parameters?: {
database_name?: string; database_name?: string;
host?: string; host?: string;
@ -47,52 +51,30 @@ export type DatabaseObject = {
credentials_info?: string; credentials_info?: string;
service_account_info?: string; service_account_info?: string;
query?: Record<string, string>; query?: Record<string, string>;
catalog?: Record<string, string>; catalog?: Record<string, string | undefined>;
properties?: Record<string, any>; properties?: Record<string, any>;
warehouse?: string; warehouse?: string;
role?: string; role?: string;
account?: string; account?: string;
}; };
configuration_method: CONFIGURATION_METHOD;
engine?: string;
paramProperties?: Record<string, any>;
// Performance // Performance
cache_timeout?: string; cache_timeout?: string;
allow_run_async?: boolean; allow_run_async?: boolean;
// SQL Lab // SQL Lab
expose_in_sqllab?: boolean;
allow_ctas?: boolean; allow_ctas?: boolean;
allow_cvas?: boolean; allow_cvas?: boolean;
allow_dml?: boolean; allow_dml?: boolean;
expose_in_sqllab?: boolean;
force_ctas_schema?: string; force_ctas_schema?: string;
// Security // Security
masked_encrypted_extra?: string;
server_cert?: string;
allow_file_upload?: boolean; allow_file_upload?: boolean;
impersonate_user?: boolean; impersonate_user?: boolean;
masked_encrypted_extra?: string;
parameters_schema?: Record<string, any>; parameters_schema?: Record<string, any>;
server_cert?: string;
// Extra
extra_json?: {
engine_params?: {
catalog?: Record<any, any> | string;
};
metadata_params?: {} | string;
metadata_cache_timeout?: {
schema_cache_timeout?: number; // in Performance
table_cache_timeout?: number; // in Performance
}; // No field, holds schema and table timeout
allows_virtual_table_explore?: boolean; // in SQL Lab
schemas_allowed_for_file_upload?: string[]; // in Security
cancel_query_on_windows_unload?: boolean; // in Performance
version?: string;
cost_estimate_enabled?: boolean; // in SQL Lab
disable_data_preview?: boolean; // in SQL Lab
};
// External management // External management
is_managed_externally: boolean; is_managed_externally: boolean;
@ -100,7 +82,6 @@ export type DatabaseObject = {
// Temporary storage // Temporary storage
catalog?: Array<CatalogObject>; catalog?: Array<CatalogObject>;
query_input?: string; query_input?: string;
extra?: string;
// DB Engine Spec information // DB Engine Spec information
engine_information?: { engine_information?: {
@ -170,3 +151,23 @@ export enum Engines {
GSheet = 'gsheets', GSheet = 'gsheets',
Snowflake = 'snowflake', Snowflake = 'snowflake',
} }
export interface ExtraJson {
allows_virtual_table_explore?: boolean; // in SQL Lab
cancel_query_on_windows_unload?: boolean; // in Performance
cost_estimate_enabled?: boolean; // in SQL Lab
disable_data_preview?: boolean; // in SQL Lab
engine_params?: {
catalog?: Record<string, string>;
connect_args?: {
http_path?: string;
};
};
metadata_params?: {};
metadata_cache_timeout?: {
schema_cache_timeout?: number; // in Performance
table_cache_timeout?: number; // in Performance
}; // No field, holds schema and table timeout
schemas_allowed_for_file_upload?: string[]; // in Security
version?: string;
}