mirror of https://github.com/apache/superset.git
feat: add/edit database modal form sections UI (#10745)
This commit is contained in:
parent
7b2200437e
commit
7cd96edcdf
|
@ -23,13 +23,32 @@ import { styledMount as mount } from 'spec/helpers/theming';
|
|||
|
||||
import DatabaseModal from 'src/views/CRUD/data/database/DatabaseModal';
|
||||
import Modal from 'src/common/components/Modal';
|
||||
import Tabs from 'src/common/components/Tabs';
|
||||
|
||||
// store needed for withToasts(DatabaseModal)
|
||||
const mockStore = configureStore([thunk]);
|
||||
const store = mockStore({});
|
||||
|
||||
const mockedProps = {
|
||||
show: true,
|
||||
};
|
||||
|
||||
const dbProps = {
|
||||
show: true,
|
||||
database: {
|
||||
id: 10,
|
||||
database_name: 'test',
|
||||
sqlalchemy_uri: 'sqllite:///user:pw/test',
|
||||
},
|
||||
};
|
||||
|
||||
describe('DatabaseModal', () => {
|
||||
const wrapper = mount(<DatabaseModal />, { context: { store } });
|
||||
const wrapper = mount(<DatabaseModal {...mockedProps} />, {
|
||||
context: { store },
|
||||
});
|
||||
const editWrapper = mount(<DatabaseModal {...dbProps} />, {
|
||||
context: { store },
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.find(DatabaseModal)).toExist();
|
||||
|
@ -38,4 +57,26 @@ describe('DatabaseModal', () => {
|
|||
it('renders a Modal', () => {
|
||||
expect(wrapper.find(Modal)).toExist();
|
||||
});
|
||||
|
||||
it('renders "Add Database" header when no database is included', () => {
|
||||
expect(wrapper.find('h4').text()).toEqual('Add Database');
|
||||
});
|
||||
|
||||
it('renders "Edit Database" header when database prop is included', () => {
|
||||
expect(editWrapper.find('h4').text()).toEqual('Edit Database');
|
||||
});
|
||||
|
||||
it('renders a Tabs menu', () => {
|
||||
expect(wrapper.find(Tabs)).toExist();
|
||||
});
|
||||
|
||||
it('renders five TabPanes', () => {
|
||||
expect(wrapper.find(Tabs.TabPane)).toExist();
|
||||
expect(wrapper.find(Tabs.TabPane)).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('renders input elements for Connection section', () => {
|
||||
expect(wrapper.find('input[name="database_name"]')).toExist();
|
||||
expect(wrapper.find('input[name="sqlalchemy_uri"]')).toExist();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -107,13 +107,15 @@ export default function Modal({
|
|||
</span>
|
||||
}
|
||||
footer={[
|
||||
<Button key="back" onClick={onHide}>
|
||||
<Button key="back" onClick={onHide} cta>
|
||||
{t('Cancel')}
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
buttonStyle={primaryButtonType}
|
||||
disabled={disablePrimaryButton}
|
||||
onClick={onHandledPrimaryAction}
|
||||
cta
|
||||
>
|
||||
{primaryButtonName}
|
||||
</Button>,
|
||||
|
|
|
@ -40,6 +40,7 @@ const StyledTabs = styled(AntdTabs)`
|
|||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
user-select: none;
|
||||
|
||||
.required {
|
||||
margin-left: ${({ theme }) => theme.gridUnit / 2}px;
|
||||
|
|
|
@ -100,6 +100,9 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
|
|||
() => {
|
||||
refreshData();
|
||||
addSuccessToast(t('Deleted: %s', dbName));
|
||||
|
||||
// Close delete modal
|
||||
setDatabaseCurrentlyDeleting(null);
|
||||
},
|
||||
createErrorHandler(errMsg =>
|
||||
addDangerToast(t('There was an issue deleting %s: %s', dbName, errMsg)),
|
||||
|
@ -107,7 +110,14 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
|
|||
);
|
||||
}
|
||||
|
||||
function handleDatabaseEdit(database: DatabaseObject) {
|
||||
// Set database and open modal
|
||||
setCurrentDatabase(database);
|
||||
setDatabaseModalOpen(true);
|
||||
}
|
||||
|
||||
const canCreate = hasPerm('can_add');
|
||||
const canEdit = hasPerm('can_edit');
|
||||
const canDelete = hasPerm('can_delete');
|
||||
|
||||
const menuData: SubMenuProps = {
|
||||
|
@ -224,12 +234,29 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
|
|||
},
|
||||
{
|
||||
Cell: ({ row: { original } }: any) => {
|
||||
const handleEdit = () => handleDatabaseEdit(original);
|
||||
const handleDelete = () => openDatabaseDeleteModal(original);
|
||||
if (!canDelete) {
|
||||
if (!canEdit && !canDelete) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span className="actions">
|
||||
{canEdit && (
|
||||
<TooltipWrapper
|
||||
label="edit-action"
|
||||
tooltip={t('Edit')}
|
||||
placement="bottom"
|
||||
>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="action-button"
|
||||
onClick={handleEdit}
|
||||
>
|
||||
<Icon name="pencil" />
|
||||
</span>
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
{canDelete && (
|
||||
<span
|
||||
role="button"
|
||||
|
@ -308,7 +335,7 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
|
|||
show={databaseModalOpen}
|
||||
onHide={() => setDatabaseModalOpen(false)}
|
||||
onDatabaseAdd={() => {
|
||||
/* TODO: add database logic here */
|
||||
refreshData();
|
||||
}}
|
||||
/>
|
||||
{databaseCurrentlyDeleting && (
|
||||
|
|
|
@ -16,12 +16,16 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { FunctionComponent, useState } from 'react';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import React, { FunctionComponent, useState, useEffect } from 'react';
|
||||
import { styled, t, SupersetClient } from '@superset-ui/core';
|
||||
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
||||
import { useSingleViewResource } 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 { DatabaseObject } from './types';
|
||||
|
||||
interface DatabaseModalProps {
|
||||
|
@ -45,7 +49,7 @@ const StyledInputContainer = styled.div`
|
|||
display: block;
|
||||
padding: ${({ theme }) => theme.gridUnit}px 0;
|
||||
color: ${({ theme }) => theme.colors.grayscale.light1};
|
||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||
font-size: ${({ theme }) => theme.typography.sizes.s - 1}px;
|
||||
text-align: left;
|
||||
|
||||
.required {
|
||||
|
@ -56,10 +60,41 @@ const StyledInputContainer = styled.div`
|
|||
|
||||
.input-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
margin-right: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
}
|
||||
|
||||
i {
|
||||
margin: 0 ${({ theme }) => theme.gridUnit}px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
height: 36px;
|
||||
font-size: ${({ theme }) => theme.typography.sizes.s - 1}px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
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;
|
||||
|
@ -70,6 +105,10 @@ const StyledInputContainer = styled.div`
|
|||
flex: 0 1 auto;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
&[name='sqlalchemy_uri'] {
|
||||
margin-right: ${({ theme }) => theme.gridUnit * 3}px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -81,11 +120,55 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||
show,
|
||||
database = null,
|
||||
}) => {
|
||||
// const [disableSave, setDisableSave] = useState(true);
|
||||
const [disableSave] = useState<boolean>(true);
|
||||
const [db, setDB] = useState<Partial<DatabaseObject> | null>(null);
|
||||
const [disableSave, setDisableSave] = useState<boolean>(true);
|
||||
const [db, setDB] = useState<DatabaseObject | null>(null);
|
||||
const [isHidden, setIsHidden] = useState<boolean>(true);
|
||||
|
||||
const isEditMode = database !== null;
|
||||
|
||||
// Database fetch logic
|
||||
const {
|
||||
state: { loading: dbLoading, resource: dbFetched },
|
||||
fetchResource,
|
||||
createResource,
|
||||
updateResource,
|
||||
} = useSingleViewResource<DatabaseObject>(
|
||||
'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.length ? db.database_name : 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,
|
||||
};
|
||||
|
||||
SupersetClient.post({
|
||||
endpoint: 'api/v1/database/test_connection',
|
||||
postPayload: JSON.stringify(connection),
|
||||
})
|
||||
.then(() => {
|
||||
addSuccessToast(t('Connection looks good!'));
|
||||
})
|
||||
.catch(() => {
|
||||
addDangerToast(
|
||||
t('ERROR: Connection failed, please check your connection settings'),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Functions
|
||||
const hide = () => {
|
||||
setIsHidden(true);
|
||||
|
@ -93,41 +176,107 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||
};
|
||||
|
||||
const onSave = () => {
|
||||
if (onDatabaseAdd) {
|
||||
onDatabaseAdd();
|
||||
}
|
||||
if (isEditMode) {
|
||||
// Edit
|
||||
const update: DatabaseObject = {
|
||||
database_name: db ? db.database_name : '',
|
||||
sqlalchemy_uri: db ? db.sqlalchemy_uri : '',
|
||||
...db,
|
||||
};
|
||||
|
||||
hide();
|
||||
// Need to clean update object
|
||||
if (update.id) {
|
||||
delete update.id;
|
||||
}
|
||||
|
||||
if (db && db.id) {
|
||||
updateResource(db.id, update).then(() => {
|
||||
if (onDatabaseAdd) {
|
||||
onDatabaseAdd();
|
||||
}
|
||||
|
||||
hide();
|
||||
});
|
||||
}
|
||||
} else if (db) {
|
||||
// Create
|
||||
createResource(db).then(() => {
|
||||
if (onDatabaseAdd) {
|
||||
onDatabaseAdd();
|
||||
}
|
||||
|
||||
hide();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const target = event.target;
|
||||
const data = {
|
||||
database_name: db ? db.database_name : '',
|
||||
uri: db ? db.uri : '',
|
||||
sqlalchemy_uri: db ? db.sqlalchemy_uri : '',
|
||||
...db,
|
||||
};
|
||||
|
||||
data[target.name] = target.value;
|
||||
if (target.type === 'checkbox') {
|
||||
data[target.name] = target.checked;
|
||||
} else {
|
||||
data[target.name] =
|
||||
typeof target.value === 'string' ? target.value.trim() : target.value;
|
||||
}
|
||||
|
||||
setDB(data);
|
||||
};
|
||||
|
||||
const isEditMode = database !== null;
|
||||
const onTextChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const target = event.target;
|
||||
const data = {
|
||||
database_name: db ? db.database_name : '',
|
||||
sqlalchemy_uri: db ? db.sqlalchemy_uri : '',
|
||||
...db,
|
||||
};
|
||||
|
||||
data[target.name] = target.value;
|
||||
setDB(data);
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
if (
|
||||
db &&
|
||||
db.database_name.length &&
|
||||
db.sqlalchemy_uri &&
|
||||
db.sqlalchemy_uri.length
|
||||
) {
|
||||
setDisableSave(false);
|
||||
} else {
|
||||
setDisableSave(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize
|
||||
if (
|
||||
isEditMode &&
|
||||
(!db || !db.id || (database && database.id !== db.id) || (isHidden && show))
|
||||
) {
|
||||
setDB(database);
|
||||
if (database && database.id !== null && !dbLoading) {
|
||||
const id = database.id || 0;
|
||||
|
||||
fetchResource(id).then(() => {
|
||||
setDB(dbFetched);
|
||||
});
|
||||
}
|
||||
} else if (!isEditMode && (!db || db.id || (isHidden && show))) {
|
||||
setDB({
|
||||
database_name: '',
|
||||
uri: '',
|
||||
sqlalchemy_uri: '',
|
||||
});
|
||||
}
|
||||
|
||||
// Validation
|
||||
useEffect(() => {
|
||||
validate();
|
||||
}, [db ? db.database_name : null, db ? db.sqlalchemy_uri : null]);
|
||||
|
||||
// Show/hide
|
||||
if (isHidden && show) {
|
||||
setIsHidden(false);
|
||||
|
@ -167,7 +316,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||
<div className="input-container">
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
name="database_name"
|
||||
value={db ? db.database_name : ''}
|
||||
placeholder={t('Name your datasource')}
|
||||
onChange={onInputChange}
|
||||
|
@ -182,11 +331,14 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||
<div className="input-container">
|
||||
<input
|
||||
type="text"
|
||||
name="uri"
|
||||
value={db ? db.uri : ''}
|
||||
name="sqlalchemy_uri"
|
||||
value={db ? db.sqlalchemy_uri : ''}
|
||||
placeholder={t('SQLAlchemy URI')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<Button buttonStyle="primary" onClick={testConnection} cta>
|
||||
{t('Test Connection')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="helper">
|
||||
{t('Refer to the ')}
|
||||
|
@ -202,16 +354,288 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||
</StyledInputContainer>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={<span>{t('Performance')}</span>} key="2">
|
||||
Performance Form Data
|
||||
<StyledInputContainer>
|
||||
<div className="label">{t('Chart Cache Timeout')}</div>
|
||||
<div className="input-container">
|
||||
<input
|
||||
type="number"
|
||||
name="cache_timeout"
|
||||
value={db ? db.cache_timeout || '' : ''}
|
||||
placeholder={t('Chart Cache Timeout')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="helper">
|
||||
{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.',
|
||||
)}
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
<StyledInputContainer>
|
||||
<div className="input-container">
|
||||
<IndeterminateCheckbox
|
||||
id="allow_run_async"
|
||||
indeterminate={false}
|
||||
checked={db ? !!db.allow_run_async : false}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<div>{t('Asynchronous Query Execution')}</div>
|
||||
<InfoTooltipWithTrigger
|
||||
label="aqe"
|
||||
tooltip={t(
|
||||
'Operate the database in asynchronous mode, meaning that the queries ' +
|
||||
'are executed on remote workers as opposed to on the web server itself. ' +
|
||||
'This assumes that you have a Celery worker setup as well as a results ' +
|
||||
'backend. Refer to the installation docs for more information.',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={<span>{t('SQL Lab Settings')}</span>} key="3">
|
||||
SQL Lab Settings Form Data
|
||||
<StyledInputContainer>
|
||||
<StyledInputContainer>
|
||||
<div className="input-container">
|
||||
<IndeterminateCheckbox
|
||||
id="expose_in_sqllab"
|
||||
indeterminate={false}
|
||||
checked={db ? !!db.expose_in_sqllab : false}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<div>{t('Expose in SQL Lab')}</div>
|
||||
<InfoTooltipWithTrigger
|
||||
label="sql-expose"
|
||||
tooltip={t('Expose this DB in SQL Lab')}
|
||||
/>
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
<StyledInputContainer>
|
||||
<div className="input-container">
|
||||
<IndeterminateCheckbox
|
||||
id="allow_ctas"
|
||||
indeterminate={false}
|
||||
checked={db ? !!db.allow_ctas : false}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<div>{t('Allow CREATE TABLE AS')}</div>
|
||||
<InfoTooltipWithTrigger
|
||||
label="allow-cta"
|
||||
tooltip={t('Allow CREATE TABLE AS option in SQL Lab')}
|
||||
/>
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
<StyledInputContainer>
|
||||
<div className="input-container">
|
||||
<IndeterminateCheckbox
|
||||
id="allow_cvas"
|
||||
indeterminate={false}
|
||||
checked={db ? !!db.allow_cvas : false}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<div>{t('Allow CREATE VIEW AS')}</div>
|
||||
<InfoTooltipWithTrigger
|
||||
label="allow-cva"
|
||||
tooltip={t('Allow CREATE VIEW AS option in SQL Lab')}
|
||||
/>
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
<StyledInputContainer>
|
||||
<div className="input-container">
|
||||
<IndeterminateCheckbox
|
||||
id="allow_dml"
|
||||
indeterminate={false}
|
||||
checked={db ? !!db.allow_dml : false}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<div>{t('Allow DML')}</div>
|
||||
<InfoTooltipWithTrigger
|
||||
label="allow-dml"
|
||||
tooltip={t(
|
||||
'Allow users to run non-SELECT statements (UPDATE, DELETE, CREATE, ...)',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
<StyledInputContainer>
|
||||
<div className="input-container">
|
||||
<IndeterminateCheckbox
|
||||
id="allow_multi_schema_metadata_fetch"
|
||||
indeterminate={false}
|
||||
checked={db ? !!db.allow_multi_schema_metadata_fetch : false}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<div>{t('Allow Multi Schema Metadata Fetch')}</div>
|
||||
<InfoTooltipWithTrigger
|
||||
label="allow-msmf"
|
||||
tooltip={t(
|
||||
'Allow SQL Lab to fetch a list of all tables and all views across all database ' +
|
||||
'schemas. For large data warehouse with thousands of tables, this can be ' +
|
||||
'expensive and put strain on the system.',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
</StyledInputContainer>
|
||||
<StyledInputContainer>
|
||||
<div className="label">{t('CTAS Schema')}</div>
|
||||
<div className="input-container">
|
||||
<input
|
||||
type="text"
|
||||
name="force_ctas_schema"
|
||||
value={db ? db.force_ctas_schema || '' : ''}
|
||||
placeholder={t('CTAS Schema')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="helper">
|
||||
{t(
|
||||
'When allowing CREATE TABLE AS option in SQL Lab, this option ' +
|
||||
'forces the table to be created in this schema.',
|
||||
)}
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={<span>{t('Security')}</span>} key="4">
|
||||
Security Form Data
|
||||
<StyledInputContainer>
|
||||
<div className="label">{t('Secure Extra')}</div>
|
||||
<div className="input-container">
|
||||
<textarea
|
||||
name="encrypted_extra"
|
||||
value={db ? db.encrypted_extra || '' : ''}
|
||||
placeholder={t('Secure Extra')}
|
||||
onChange={onTextChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="helper">
|
||||
<div>
|
||||
{t(
|
||||
'JSON string containing additional connection configuration.',
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{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.',
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
<StyledInputContainer>
|
||||
<div className="label">{t('Root Certificate')}</div>
|
||||
<div className="input-container">
|
||||
<textarea
|
||||
name="server_cert"
|
||||
value={db ? db.server_cert || '' : ''}
|
||||
placeholder={t('Root Certificate')}
|
||||
onChange={onTextChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="helper">
|
||||
{t(
|
||||
'Optional CA_BUNDLE contents to validate HTTPS requests. Only available on ' +
|
||||
'certain database engines.',
|
||||
)}
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={<span>{t('Extra')}</span>} key="5">
|
||||
Extra Form Data
|
||||
<StyledInputContainer>
|
||||
<div className="input-container">
|
||||
<IndeterminateCheckbox
|
||||
id="impersonate_user"
|
||||
indeterminate={false}
|
||||
checked={db ? !!db.impersonate_user : false}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<div>{t('Impersonate Logged In User (Presto & Hive')}</div>
|
||||
<InfoTooltipWithTrigger
|
||||
label="impersonate"
|
||||
tooltip={t(
|
||||
'If Presto, all the queries in SQL Lab are going to be executed as the ' +
|
||||
'currently logged on user who must have permission to run them. If Hive ' +
|
||||
'and hive.server2.enable.doAs is enabled, will run the queries as ' +
|
||||
'service account, but impersonate the currently logged on user via ' +
|
||||
'hive.server2.proxy.user property.',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
<StyledInputContainer>
|
||||
<div className="input-container">
|
||||
<IndeterminateCheckbox
|
||||
id="allow_csv_upload"
|
||||
indeterminate={false}
|
||||
checked={db ? !!db.allow_csv_upload : false}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<div>{t('Allow CSV Upload')}</div>
|
||||
<InfoTooltipWithTrigger
|
||||
label="allow-csv"
|
||||
tooltip={t(
|
||||
'If selected, please set the schemas allowed for csv upload in Extra.',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
<StyledInputContainer>
|
||||
<div className="label">{t('Extra')}</div>
|
||||
<div className="input-container">
|
||||
<textarea
|
||||
name="extra"
|
||||
value={db ? db.extra || '' : ''}
|
||||
placeholder={
|
||||
'{\n "metadata_params": {},\n "engine_params": {},' +
|
||||
'\n "metadata_cache_timeout": {},\n "schemas_allowed_for_csv_upload": [] \n}'
|
||||
}
|
||||
onChange={onTextChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="helper">
|
||||
<div>
|
||||
{t('JSON string containing extra configuration elements.')}
|
||||
</div>
|
||||
<div>
|
||||
{t(
|
||||
'1. The engine_params object gets unpacked into the sqlalchemy.create_engine ' +
|
||||
'call, while the metadata_params gets unpacked into the sqlalchemy.MetaData ' +
|
||||
'call.',
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{t(
|
||||
'2. The metadata_cache_timeout is a cache timeout setting in seconds for ' +
|
||||
'metadata fetch of this database. Specify it as "metadata_cache_timeout": ' +
|
||||
'{"schema_cache_timeout": 600, "table_cache_timeout": 600}. If unset, cache ' +
|
||||
'will not be enabled for the functionality. A timeout of 0 indicates that ' +
|
||||
'the cache never expires.',
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{t(
|
||||
'3. The schemas_allowed_for_csv_upload is a comma separated list of schemas ' +
|
||||
'that CSVs are allowed to upload to. Specify it as ' +
|
||||
'"schemas_allowed_for_csv_upload": ["public", "csv_upload"]. If database ' +
|
||||
'flavor does not support schema or any schema is allowed to be accessed, ' +
|
||||
'just leave the list empty.',
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{t(
|
||||
"4. The version field is a string specifying this db's version. This " +
|
||||
'should be used with Presto DBs so that the syntax is correct.',
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{t(
|
||||
'5. The allows_virtual_table_explore field is a boolean specifying whether ' +
|
||||
'or not the Explore button in SQL Lab results is shown.',
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Modal>
|
||||
|
|
|
@ -22,17 +22,33 @@ type DatabaseUser = {
|
|||
};
|
||||
|
||||
export type DatabaseObject = {
|
||||
id: number;
|
||||
// Connection + general
|
||||
id?: number;
|
||||
database_name: string;
|
||||
backend: string;
|
||||
allow_run_async: boolean;
|
||||
allow_dml: boolean;
|
||||
allow_csv_upload: boolean;
|
||||
expose_in_sqllab: boolean;
|
||||
created_by: null | DatabaseUser;
|
||||
changed_on_delta_humanized: string;
|
||||
changed_on: string;
|
||||
sqlalchemy_uri?: string;
|
||||
backend?: string;
|
||||
created_by?: null | DatabaseUser;
|
||||
changed_on_delta_humanized?: string;
|
||||
changed_on?: string;
|
||||
|
||||
uri: string;
|
||||
// TODO: add more props
|
||||
// Performance
|
||||
cache_timeout?: string;
|
||||
allow_run_async?: boolean;
|
||||
|
||||
// SQL Lab
|
||||
expose_in_sqllab?: boolean;
|
||||
allow_ctas?: boolean;
|
||||
allow_cvas?: boolean;
|
||||
allow_dml?: boolean;
|
||||
allow_multi_schema_metadata_fetch?: boolean;
|
||||
force_ctas_schema?: string;
|
||||
|
||||
// Security
|
||||
encrypted_extra?: string;
|
||||
server_cert?: string;
|
||||
|
||||
// Extra
|
||||
impersonate_user?: boolean;
|
||||
allow_csv_upload?: boolean;
|
||||
extra?: string;
|
||||
};
|
||||
|
|
|
@ -167,6 +167,135 @@ export function useListViewResource<D extends object = any>(
|
|||
};
|
||||
}
|
||||
|
||||
// In the same vein as above, a hook for viewing a single instance of a resource (given id)
|
||||
interface SingleViewResourceState<D extends object = any> {
|
||||
loading: boolean;
|
||||
resource: D | null;
|
||||
}
|
||||
|
||||
export function useSingleViewResource<D extends object = any>(
|
||||
resourceName: string,
|
||||
resourceLabel: string, // resourceLabel for translations
|
||||
handleErrorMsg: (errorMsg: string) => void,
|
||||
) {
|
||||
const [state, setState] = useState<SingleViewResourceState<D>>({
|
||||
loading: false,
|
||||
resource: null,
|
||||
});
|
||||
|
||||
function updateState(update: Partial<SingleViewResourceState<D>>) {
|
||||
setState(currentState => ({ ...currentState, ...update }));
|
||||
}
|
||||
|
||||
const fetchResource = useCallback((resourceID: number) => {
|
||||
// Set loading state
|
||||
updateState({
|
||||
loading: true,
|
||||
});
|
||||
|
||||
return SupersetClient.get({
|
||||
endpoint: `/api/v1/${resourceName}/${resourceID}`,
|
||||
})
|
||||
.then(
|
||||
({ json = {} }) => {
|
||||
updateState({
|
||||
resource: json.result,
|
||||
});
|
||||
},
|
||||
createErrorHandler(errMsg =>
|
||||
handleErrorMsg(
|
||||
t(
|
||||
'An error occurred while fetching %ss: %s',
|
||||
resourceLabel,
|
||||
errMsg,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.finally(() => {
|
||||
updateState({ loading: false });
|
||||
});
|
||||
}, []);
|
||||
|
||||
const createResource = useCallback((resource: D) => {
|
||||
// Set loading state
|
||||
updateState({
|
||||
loading: true,
|
||||
});
|
||||
|
||||
return SupersetClient.post({
|
||||
endpoint: `/api/v1/${resourceName}/`,
|
||||
body: JSON.stringify(resource),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
.then(
|
||||
({ json = {} }) => {
|
||||
updateState({
|
||||
resource: json.result,
|
||||
});
|
||||
},
|
||||
createErrorHandler(errMsg =>
|
||||
handleErrorMsg(
|
||||
t(
|
||||
'An error occurred while fetching %ss: %s',
|
||||
resourceLabel,
|
||||
errMsg,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.finally(() => {
|
||||
updateState({ loading: false });
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateResource = useCallback((resourceID: number, resource: D) => {
|
||||
// Set loading state
|
||||
updateState({
|
||||
loading: true,
|
||||
});
|
||||
|
||||
return SupersetClient.put({
|
||||
endpoint: `/api/v1/${resourceName}/${resourceID}`,
|
||||
body: JSON.stringify(resource),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
.then(
|
||||
({ json = {} }) => {
|
||||
updateState({
|
||||
resource: json.result,
|
||||
});
|
||||
},
|
||||
createErrorHandler(errMsg =>
|
||||
handleErrorMsg(
|
||||
t(
|
||||
'An error occurred while fetching %ss: %s',
|
||||
resourceLabel,
|
||||
errMsg,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.finally(() => {
|
||||
updateState({ loading: false });
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
state: {
|
||||
loading: state.loading,
|
||||
resource: state.resource,
|
||||
},
|
||||
setResource: (update: D) =>
|
||||
updateState({
|
||||
resource: update,
|
||||
}),
|
||||
fetchResource,
|
||||
createResource,
|
||||
updateResource,
|
||||
};
|
||||
}
|
||||
|
||||
// the hooks api has some known limitations around stale state in closures.
|
||||
// See https://github.com/reactjs/rfcs/blob/master/text/0068-react-hooks.md#drawbacks
|
||||
// the useRef hook is a way of getting around these limitations by having a consistent ref
|
||||
|
|
Loading…
Reference in New Issue