/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import React, { FunctionComponent, useState, useEffect } from 'react'; import { styled, t } from '@superset-ui/core'; import InfoTooltip from 'src/common/components/InfoTooltip'; import { useSingleViewResource, testDatabaseConnection, } from 'src/views/CRUD/hooks'; import withToasts from 'src/messageToasts/enhancers/withToasts'; import Icon from 'src/components/Icon'; import Modal from 'src/common/components/Modal'; import Tabs from 'src/common/components/Tabs'; import Button from 'src/components/Button'; import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox'; import { JsonEditor } from 'src/components/AsyncAceEditor'; import { DatabaseObject } from './types'; import { useCommonConf } from './state'; interface DatabaseModalProps { addDangerToast: (msg: string) => void; addSuccessToast: (msg: string) => void; onDatabaseAdd?: (database?: DatabaseObject) => void; // TODO: should we add a separate function for edit? onHide: () => void; show: boolean; database?: DatabaseObject | null; // If included, will go into edit mode } const DEFAULT_TAB_KEY = '1'; const EXPOSE_SQLLAB_FORM_HEIGHT = '270px'; const CTAS_CVAS_SCHEMA_FORM_HEIGHT = '94px'; const StyledIcon = styled(Icon)` margin: auto ${({ theme }) => theme.gridUnit * 2}px auto 0; `; const StyledInputContainer = styled.div` margin-bottom: ${({ theme }) => theme.gridUnit * 2}px; &.extra-container { padding-top: 8px; } &.expandable { height: 0; overflow: hidden; transition: height 0.25s; margin-left: ${({ theme }) => theme.gridUnit * 8}px; padding: 0; &.open { height: ${CTAS_CVAS_SCHEMA_FORM_HEIGHT}; } } .helper { display: block; padding: ${({ theme }) => theme.gridUnit}px 0; color: ${({ theme }) => theme.colors.grayscale.base}; font-size: ${({ theme }) => theme.typography.sizes.s - 1}px; text-align: left; .required { margin-left: ${({ theme }) => theme.gridUnit / 2}px; color: ${({ theme }) => theme.colors.error.base}; } } .input-container { display: flex; align-items: top; label { display: flex; margin-left: ${({ theme }) => theme.gridUnit * 2}px; margin-top: ${({ theme }) => theme.gridUnit * 0.75}px; font-family: ${({ theme }) => theme.typography.families.sansSerif}; font-size: ${({ theme }) => theme.typography.sizes.m}px; } i { margin: 0 ${({ theme }) => theme.gridUnit}px; } } input, textarea { flex: 1 1 auto; } textarea { height: 160px; resize: none; } input::placeholder, textarea::placeholder { color: ${({ theme }) => theme.colors.grayscale.light1}; } textarea, input[type='text'], input[type='number'] { padding: ${({ theme }) => theme.gridUnit * 1.5}px ${({ theme }) => theme.gridUnit * 2}px; border-style: none; border: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; border-radius: ${({ theme }) => theme.gridUnit}px; &[name='name'] { flex: 0 1 auto; width: 40%; } &[name='sqlalchemy_uri'] { margin-right: ${({ theme }) => theme.gridUnit * 3}px; } } `; const StyledJsonEditor = styled(JsonEditor)` flex: 1 1 auto; border: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; border-radius: ${({ theme }) => theme.gridUnit}px; `; const StyledExpandableForm = styled.div` padding-top: ${({ theme }) => theme.gridUnit}px; .input-container { padding-top: ${({ theme }) => theme.gridUnit}px; padding-bottom: ${({ theme }) => theme.gridUnit}px; } &.expandable { height: 0; overflow: hidden; transition: height 0.25s; margin-left: ${({ theme }) => theme.gridUnit * 7}px; &.open { height: ${EXPOSE_SQLLAB_FORM_HEIGHT}; } } `; const DatabaseModal: FunctionComponent = ({ addDangerToast, addSuccessToast, onDatabaseAdd, onHide, show, database = null, }) => { const [disableSave, setDisableSave] = useState(true); const [db, setDB] = useState(null); const [isHidden, setIsHidden] = useState(true); const [tabKey, setTabKey] = useState(DEFAULT_TAB_KEY); const conf = useCommonConf(); const isEditMode = database !== null; const defaultExtra = '{\n "metadata_params": {},\n "engine_params": {},' + '\n "metadata_cache_timeout": {},\n "schemas_allowed_for_csv_upload": [] \n}'; // Database fetch logic const { state: { loading: dbLoading, resource: dbFetched }, fetchResource, createResource, updateResource, } = useSingleViewResource( 'database', t('database'), addDangerToast, ); // Test Connection logic const testConnection = () => { if (!db || !db.sqlalchemy_uri || !db.sqlalchemy_uri.length) { addDangerToast(t('Please enter a SQLAlchemy URI to test')); return; } const connection = { sqlalchemy_uri: db ? db.sqlalchemy_uri : '', database_name: db && db.database_name.trim().length ? db.database_name.trim() : undefined, impersonate_user: db ? db.impersonate_user || undefined : undefined, extra: db && db.extra && db.extra.length ? db.extra : undefined, encrypted_extra: db ? db.encrypted_extra || undefined : undefined, server_cert: db ? db.server_cert || undefined : undefined, }; testDatabaseConnection(connection, addDangerToast, addSuccessToast); }; // Functions const hide = () => { setIsHidden(true); onHide(); }; const onSave = () => { if (isEditMode) { // Edit const update: DatabaseObject = { database_name: db ? db.database_name.trim() : '', sqlalchemy_uri: db ? db.sqlalchemy_uri : '', ...db, }; // Need to clean update object if (update.id) { delete update.id; } if (db && db.id) { updateResource(db.id, update).then(result => { if (result) { if (onDatabaseAdd) { onDatabaseAdd(); } hide(); } }); } } else if (db) { // Create db.database_name = db.database_name.trim(); createResource(db).then(dbId => { if (dbId) { if (onDatabaseAdd) { onDatabaseAdd(); } hide(); } }); } }; const onInputChange = (event: React.ChangeEvent) => { const { target } = event; const { checked, name, value, type } = target; const data = { database_name: db?.database_name || '', sqlalchemy_uri: db?.sqlalchemy_uri || '', ...db, }; if (type === 'checkbox') { data[name] = checked; } else { data[name] = value; } setDB(data); }; const onTextChange = (event: React.ChangeEvent) => { const { target } = event; const { name, value } = target; const data = { database_name: db?.database_name || '', sqlalchemy_uri: db?.sqlalchemy_uri || '', ...db, }; data[name] = value; setDB(data); }; const onEditorChange = (json: string, name: string) => { const data = { database_name: db?.database_name || '', sqlalchemy_uri: db?.sqlalchemy_uri || '', ...db, }; data[name] = json; setDB(data); }; const validate = () => { if ( db && db.database_name.trim().length && db.sqlalchemy_uri && db.sqlalchemy_uri.length ) { setDisableSave(false); } else { setDisableSave(true); } }; // Initialize if ( isEditMode && (!db || !db.id || database?.id !== db.id || (isHidden && show)) ) { if (database?.id && !dbLoading) { const id = database.id || 0; setTabKey(DEFAULT_TAB_KEY); fetchResource(id) .then(() => { setDB(dbFetched); }) .catch(e => addDangerToast( t( 'Sorry there was an error fetching database information: %s', e.message, ), ), ); } } else if (!isEditMode && (!db || db.id || (isHidden && show))) { setTabKey(DEFAULT_TAB_KEY); setDB({ database_name: '', sqlalchemy_uri: '', }); } // Validation useEffect(() => { validate(); }, [db?.database_name || null, db?.sqlalchemy_uri || null]); // Show/hide if (isHidden && show) { setIsHidden(false); } const tabChange = (key: string) => { setTabKey(key); }; const expandableModalIsOpen = !!db?.expose_in_sqllab; const createAsOpen = !!(db?.allow_ctas || db?.allow_cvas); return ( {isEditMode ? t('Edit database') : t('Add database')} } > {t('Connection')} * } key="1" >
{t('Database name')} *
{t('SQLAlchemy URI')} *
{t('Refer to the ')} {conf?.SQLALCHEMY_DISPLAY_TEXT ?? ''} {t(' for more information on how to structure your URI.')}
{t('Performance')}} key="2">
{t('Chart cache timeout')}
{t( 'Duration (in seconds) of the caching timeout for charts of this database.' + ' A timeout of 0 indicates that the cache never expires.' + ' Note this defaults to the global timeout if undefined.', )}
{t('SQL Lab settings')}} key="3">
{t('CTAS & CVAS SCHEMA')}
{t( 'When allowing CREATE TABLE AS option in SQL Lab, this option ' + 'forces the table to be created in this schema.', )}
{t('Security')}} key="4">
{t('Secure extra')}
onEditorChange(json, 'encrypted_extra') } width="100%" height="160px" />
{t( 'JSON string containing additional connection configuration.', )}
{t( 'This is used to provide connection information for systems like Hive, ' + 'Presto, and BigQuery, which do not conform to the username:password syntax ' + 'normally used by SQLAlchemy.', )}
{t('Root certificate')}