diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ExtraOptions.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ExtraOptions.tsx index 349264d6819fd..243ee27fe2241 100644 --- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ExtraOptions.tsx +++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ExtraOptions.tsx @@ -29,7 +29,7 @@ import { antdCollapseStyles, no_margin_bottom, } from './styles'; -import { DatabaseObject } from '../types'; +import { DatabaseObject, ExtraJson } from '../types'; const ExtraOptions = ({ db, @@ -50,6 +50,7 @@ const ExtraOptions = ({ const createAsOpen = !!(db?.allow_ctas || db?.allow_cvas); const isFileUploadSupportedByEngine = db?.engine_information?.supports_file_upload; + const extraJson: ExtraJson = JSON.parse(db?.extra || '{}'); return ( @@ -171,7 +172,7 @@ const ExtraOptions = ({ @@ -187,7 +188,7 @@ const ExtraOptions = ({ @@ -240,8 +241,7 @@ const ExtraOptions = ({ type="number" name="schema_cache_timeout" value={ - db?.extra_json?.metadata_cache_timeout?.schema_cache_timeout || - '' + extraJson?.metadata_cache_timeout?.schema_cache_timeout || '' } placeholder={t('Enter duration in seconds')} onChange={onExtraInputChange} @@ -262,8 +262,7 @@ const ExtraOptions = ({ type="number" name="table_cache_timeout" value={ - db?.extra_json?.metadata_cache_timeout?.table_cache_timeout || - '' + extraJson?.metadata_cache_timeout?.table_cache_timeout || '' } placeholder={t('Enter duration in seconds')} onChange={onExtraInputChange} @@ -301,7 +300,7 @@ const ExtraOptions = ({ @@ -414,9 +413,9 @@ const ExtraOptions = ({ @@ -443,7 +442,11 @@ const ExtraOptions = ({
onExtraEditorChange({ json, name: 'metadata_params' }) @@ -465,7 +468,11 @@ const ExtraOptions = ({
onExtraEditorChange({ json, name: 'engine_params' }) @@ -490,7 +497,7 @@ const ExtraOptions = ({ diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.test.jsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.test.tsx similarity index 80% rename from superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.test.jsx rename to superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.test.tsx index 4fb5d60cdd3d8..6a0173fd56c2b 100644 --- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.test.jsx +++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.test.tsx @@ -27,12 +27,21 @@ import { act, } from 'spec/helpers/testing-library'; 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 = { show: true, database_name: 'my database', sqlalchemy_uri: 'postgres://superset:superset@something:1234/superset', + onHide: () => {}, }; const DATABASE_FETCH_ENDPOINT = 'glob:*/api/v1/database/10'; @@ -223,6 +232,14 @@ fetchMock.post(VALIDATE_PARAMS_ENDPOINT, { message: 'OK', }); +const databaseFixture: DatabaseObject = { + backend: 'postgres', + configuration_method: CONFIGURATION_METHOD.DYNAMIC_FORM, + database_name: 'Postgres', + name: 'PostgresDB', + is_managed_externally: false, +}; + describe('DatabaseModal', () => { const renderAndWait = async () => { const mounted = act(async () => { @@ -640,8 +657,6 @@ describe('DatabaseModal', () => { checkboxOffSVGs[2], checkboxOffSVGs[3], checkboxOffSVGs[4], - checkboxOffSVGs[5], - checkboxOffSVGs[6], tooltipIcons[0], tooltipIcons[1], tooltipIcons[2], @@ -670,14 +685,13 @@ describe('DatabaseModal', () => { allowDbExplorationCheckbox, disableSQLLabDataPreviewQueriesCheckbox, ]; - visibleComponents.forEach(component => { expect(component).toBeVisible(); }); invisibleComponents.forEach(component => { expect(component).not.toBeVisible(); }); - expect(checkboxOffSVGs).toHaveLength(7); + expect(checkboxOffSVGs).toHaveLength(5); expect(tooltipIcons).toHaveLength(7); }); @@ -1169,7 +1183,9 @@ describe('DatabaseModal', () => { describe('Import database flow', () => { test('imports a file', async () => { - const importDbButton = screen.getByTestId('import-database-btn'); + const importDbButton = screen.getByTestId( + 'import-database-btn', + ) as HTMLInputElement; expect(importDbButton).toBeVisible(); const testFile = new File([new ArrayBuffer(1)], 'model_export.zip'); @@ -1177,8 +1193,8 @@ describe('DatabaseModal', () => { userEvent.click(importDbButton); userEvent.upload(importDbButton, testFile); - expect(importDbButton.files[0]).toStrictEqual(testFile); - expect(importDbButton.files.item(0)).toStrictEqual(testFile); + expect(importDbButton.files?.[0]).toStrictEqual(testFile); + expect(importDbButton.files?.item(0)).toStrictEqual(testFile); expect(importDbButton.files).toHaveLength(1); }); }); @@ -1291,6 +1307,7 @@ describe('DatabaseModal', () => { createResource: jest.fn(), updateResource: jest.fn(), clearError: jest.fn(), + setResource: jest.fn(), }); const renderAndWait = async () => { @@ -1335,6 +1352,7 @@ describe('DatabaseModal', () => { createResource: jest.fn(), updateResource: jest.fn(), clearError: jest.fn(), + setResource: jest.fn(), }); 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: [], + }); + }); +}); diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx index 37c13f17e52d8..003f9d64be856 100644 --- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx +++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx @@ -60,6 +60,7 @@ import { CONFIGURATION_METHOD, CatalogObject, Engines, + ExtraJson, } from 'src/views/CRUD/data/database/types'; import Loading from 'src/components/Loading'; import ExtraOptions from './ExtraOptions'; @@ -88,6 +89,8 @@ import { } from './styles'; import ModalHeader, { DOCUMENTATION_LINK } from './ModalHeader'; +const DEFAULT_EXTRA = JSON.stringify({ allows_virtual_table_explore: true }); + const engineSpecificAlertMapping = { [Engines.GSheet]: { message: 'Why do I need to create a database?', @@ -121,14 +124,14 @@ const ErrorAlertContainer = styled.div` interface DatabaseModalProps { addDangerToast: (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; show: boolean; databaseId: number | undefined; // If included, will go into edit mode dbEngine: string | undefined; // if included goto step 2 with engine already set } -enum ActionType { +export enum ActionType { configMethodChange, dbSelected, editorChange, @@ -153,7 +156,7 @@ interface DBReducerPayloadType { value?: string; } -type DBReducerActionType = +export type DBReducerActionType = | { type: | ActionType.extraEditorChange @@ -201,7 +204,7 @@ const StyledBtns = styled.div` margin-left: ${({ theme }) => theme.gridUnit * 3}px; `; -function dbReducer( +export function dbReducer( state: Partial | null, action: DBReducerActionType, ): Partial | null { @@ -210,54 +213,56 @@ function dbReducer( }; let query = {}; let query_input = ''; - let deserializeExtraJSON = { allows_virtual_table_explore: true }; - let extra_json: DatabaseObject['extra_json']; + let parametersCatalog; + const extraJson: ExtraJson = JSON.parse(trimmedState.extra || '{}'); switch (action.type) { case ActionType.extraEditorChange: + // "extra" payload in state is a string return { ...trimmedState, - extra_json: { - ...trimmedState.extra_json, + extra: JSON.stringify({ + ...extraJson, [action.payload.name]: action.payload.json, - }, + }), }; case ActionType.extraInputChange: + // "extra" payload in state is a string if ( action.payload.name === 'schema_cache_timeout' || action.payload.name === 'table_cache_timeout' ) { return { ...trimmedState, - extra_json: { - ...trimmedState.extra_json, + extra: JSON.stringify({ + ...extraJson, metadata_cache_timeout: { - ...trimmedState.extra_json?.metadata_cache_timeout, + ...extraJson?.metadata_cache_timeout, [action.payload.name]: action.payload.value, }, - }, + }), }; } if (action.payload.name === 'schemas_allowed_for_file_upload') { return { ...trimmedState, - extra_json: { - ...trimmedState.extra_json, - schemas_allowed_for_file_upload: (action.payload.value || '').split( - ',', - ), - }, + extra: JSON.stringify({ + ...extraJson, + schemas_allowed_for_file_upload: (action.payload.value || '') + .split(',') + .filter(schema => schema !== ''), + }), }; } return { ...trimmedState, - extra_json: { - ...trimmedState.extra_json, + extra: JSON.stringify({ + ...extraJson, [action.payload.name]: action.payload.type === 'checkbox' ? action.payload.checked : action.payload.value, - }, + }), }; case ActionType.inputChange: if (action.payload.type === 'checkbox') { @@ -271,26 +276,36 @@ function dbReducer( [action.payload.name]: action.payload.value, }; 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 ( - trimmedState.catalog !== undefined && - action.payload.type?.startsWith('catalog') + action.payload.type?.startsWith('catalog') && + trimmedState.catalog !== undefined ) { // Formatting wrapping google sheets table catalog + const catalogCopy: CatalogObject[] = [...trimmedState.catalog]; const idx = action.payload.type?.split('-')[1]; - const catalogToUpdate = trimmedState?.catalog[idx] || {}; + const catalogToUpdate: CatalogObject = catalogCopy[idx] || {}; 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 - trimmedState.catalog?.map((item: CatalogObject) => { - paramatersCatalog[item.name] = item.value; - }); + parametersCatalog = catalogCopy.reduce((obj, item: any) => { + const catalog = { ...obj }; + catalog[item.name] = item.value; + return catalog; + }, {}); return { ...trimmedState, + catalog: catalogCopy, parameters: { ...trimmedState.parameters, - catalog: paramatersCatalog, + catalog: parametersCatalog, }, }; } @@ -301,6 +316,7 @@ function dbReducer( [action.payload.name]: action.payload.value, }, }; + case ActionType.addTableCatalogSheet: if (trimmedState.catalog !== undefined) { return { @@ -337,22 +353,6 @@ function dbReducer( [action.payload.name]: action.payload.value, }; 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 query = action.payload?.parameters?.query || {}; query_input = Object.entries(query) @@ -364,19 +364,26 @@ function dbReducer( action.payload.configuration_method === CONFIGURATION_METHOD.DYNAMIC_FORM ) { - const engineParamsCatalog = Object.entries( - extra_json?.engine_params?.catalog || {}, - ).map(([key, value]) => ({ - name: key, - value, - })); + // "extra" payload from the api is a string + const extraJsonPayload: ExtraJson = { + ...JSON.parse((action.payload.extra as string) || '{}'), + }; + + const payloadCatalog = extraJsonPayload.engine_params?.catalog; + + const engineRootCatalog = Object.entries(payloadCatalog || {}).map( + ([name, value]: string[]) => ({ name, value }), + ); + return { ...action.payload, engine: action.payload.backend || trimmedState.engine, configuration_method: action.payload.configuration_method, - extra_json: deserializeExtraJSON, - catalog: engineParamsCatalog, - parameters: action.payload.parameters || trimmedState.parameters, + catalog: engineRootCatalog, + parameters: { + ...(action.payload.parameters || trimmedState.parameters), + catalog: payloadCatalog, + }, query_input, }; } @@ -385,16 +392,17 @@ function dbReducer( masked_encrypted_extra: action.payload.masked_encrypted_extra || '', engine: action.payload.backend || trimmedState.engine, configuration_method: action.payload.configuration_method, - extra_json: deserializeExtraJSON, parameters: action.payload.parameters || trimmedState.parameters, query_input, }; case ActionType.dbSelected: + // set initial state for blank form return { ...action.payload, + extra: DEFAULT_EXTRA, + expose_in_sqllab: true, }; - case ActionType.configMethodChange: return { ...action.payload, @@ -408,16 +416,6 @@ function dbReducer( 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 = ({ addDangerToast, addSuccessToast, @@ -498,7 +496,7 @@ const DatabaseModal: FunctionComponent = ({ sqlalchemy_uri: db?.sqlalchemy_uri || '', database_name: db?.database_name?.trim() || undefined, impersonate_user: db?.impersonate_user || undefined, - extra: serializeExtra(db?.extra_json) || undefined, + extra: db?.extra, masked_encrypted_extra: db?.masked_encrypted_extra || '', server_cert: db?.server_cert || undefined, }; @@ -567,28 +565,25 @@ const DatabaseModal: FunctionComponent = ({ const onSave = async () => { // Clone DB object - const dbToUpdate = JSON.parse(JSON.stringify(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 = {}; - } + const dbToUpdate = { ...(db || {}) }; if (dbToUpdate.configuration_method === CONFIGURATION_METHOD.DYNAMIC_FORM) { // 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); if ((validationErrors && !isEmpty(validationErrors)) || errors) { return; } const parameters_schema = isEditMode - ? dbToUpdate.parameters_schema.properties + ? dbToUpdate.parameters_schema?.properties : dbModel?.parameters.properties; const additionalEncryptedExtra = JSON.parse( dbToUpdate.masked_encrypted_extra || '{}', @@ -633,16 +628,12 @@ const DatabaseModal: FunctionComponent = ({ if (dbToUpdate?.parameters?.catalog) { // need to stringify gsheets catalog to allow it to be seralized - dbToUpdate.extra_json = { - engine_params: JSON.stringify({ + dbToUpdate.extra = JSON.stringify({ + ...JSON.parse(dbToUpdate.extra || '{}'), + engine_params: { catalog: dbToUpdate.parameters.catalog, - }), - }; - } - - if (dbToUpdate?.extra_json) { - // convert extra_json to back to string - dbToUpdate.extra = serializeExtra(dbToUpdate?.extra_json); + }, + }); } setLoading(true); diff --git a/superset-frontend/src/views/CRUD/data/database/types.ts b/superset-frontend/src/views/CRUD/data/database/types.ts index 9a9386035eebc..373e0dbf83981 100644 --- a/superset-frontend/src/views/CRUD/data/database/types.ts +++ b/superset-frontend/src/views/CRUD/data/database/types.ts @@ -28,14 +28,18 @@ export type CatalogObject = { export type DatabaseObject = { // Connection + general - id?: number; + backend?: 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; sqlalchemy_uri?: string; - backend?: string; - created_by?: null | DatabaseUser; - changed_on_delta_humanized?: string; - changed_on?: string; parameters?: { database_name?: string; host?: string; @@ -47,52 +51,30 @@ export type DatabaseObject = { credentials_info?: string; service_account_info?: string; query?: Record; - catalog?: Record; + catalog?: Record; properties?: Record; warehouse?: string; role?: string; account?: string; }; - configuration_method: CONFIGURATION_METHOD; - engine?: string; - paramProperties?: Record; // Performance cache_timeout?: string; allow_run_async?: boolean; // SQL Lab - expose_in_sqllab?: boolean; allow_ctas?: boolean; allow_cvas?: boolean; allow_dml?: boolean; + expose_in_sqllab?: boolean; force_ctas_schema?: string; // Security - masked_encrypted_extra?: string; - server_cert?: string; allow_file_upload?: boolean; impersonate_user?: boolean; + masked_encrypted_extra?: string; parameters_schema?: Record; - - // Extra - extra_json?: { - engine_params?: { - catalog?: Record | 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 - }; + server_cert?: string; // External management is_managed_externally: boolean; @@ -100,7 +82,6 @@ export type DatabaseObject = { // Temporary storage catalog?: Array; query_input?: string; - extra?: string; // DB Engine Spec information engine_information?: { @@ -170,3 +151,23 @@ export enum Engines { GSheet = 'gsheets', 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; + 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; +}