blob: 48cdccdb55e37eafd4a0b710a9542ecb3e7fce6b [file] [log] [blame]
/**
* 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 { ChangeEvent, EventHandler, useState, useEffect } from 'react';
import cx from 'classnames';
import {
t,
DatabaseConnectionExtension,
isFeatureEnabled,
useTheme,
FeatureFlag,
} from '@superset-ui/core';
import {
Input,
Checkbox,
Collapse,
InfoTooltip,
CollapseLabelInModal,
type CheckboxChangeEvent,
} from '@superset-ui/core/components';
import { useJsonValidation } from '@superset-ui/core/components/AsyncAceEditor';
import {
StyledInputContainer,
StyledJsonEditor,
StyledExpandableForm,
no_margin_bottom,
} from './styles';
import { DatabaseObject, ExtraJson } from '../types';
const ExtraOptions = ({
db,
onInputChange,
onTextChange,
onEditorChange,
onExtraInputChange,
onExtraEditorChange,
extraExtension,
}: {
db: DatabaseObject | null;
onInputChange: (
e: CheckboxChangeEvent | React.ChangeEvent<HTMLInputElement>,
) => void;
onTextChange: EventHandler<ChangeEvent<HTMLTextAreaElement>>;
onEditorChange: Function;
onExtraInputChange: (
e: CheckboxChangeEvent | ChangeEvent<HTMLInputElement>,
) => void;
onExtraEditorChange: Function;
extraExtension: DatabaseConnectionExtension | undefined;
}) => {
const expandableModalIsOpen = !!db?.expose_in_sqllab;
const createAsOpen = !!(db?.allow_ctas || db?.allow_cvas);
const isFileUploadSupportedByEngine =
db?.engine_information?.supports_file_upload;
const supportsDynamicCatalog =
db?.engine_information?.supports_dynamic_catalog;
// JSON.parse will deep parse engine_params
// if it's an object, and we want to keep it a string
const extraJson: ExtraJson = JSON.parse(db?.extra || '{}', (key, value) => {
if (key === 'engine_params' && typeof value === 'object') {
// keep this as a string
return JSON.stringify(value);
}
return value;
});
// JSON validation hooks for the three editors
const secureExtraAnnotations = useJsonValidation(db?.masked_encrypted_extra, {
errorPrefix: 'Invalid secure extra JSON',
});
const metadataParamsValue = !Object.keys(extraJson?.metadata_params || {})
.length
? ''
: typeof extraJson?.metadata_params === 'string'
? extraJson?.metadata_params
: JSON.stringify(extraJson?.metadata_params);
const metadataParamsAnnotations = useJsonValidation(metadataParamsValue, {
errorPrefix: 'Invalid metadata parameters JSON',
});
const engineParamsValue = !Object.keys(extraJson?.engine_params || {}).length
? ''
: typeof extraJson?.engine_params === 'string'
? extraJson?.engine_params
: JSON.stringify(extraJson?.engine_params);
const engineParamsAnnotations = useJsonValidation(engineParamsValue, {
errorPrefix: 'Invalid engine parameters JSON',
});
const theme = useTheme();
const ExtraExtensionComponent = extraExtension?.component;
const ExtraExtensionLogo = extraExtension?.logo;
const ExtensionDescription = extraExtension?.description;
const allowRunAsync = isFeatureEnabled(FeatureFlag.ForceSqlLabRunAsync)
? true
: !!db?.allow_run_async;
const isAllowRunAsyncDisabled = isFeatureEnabled(
FeatureFlag.ForceSqlLabRunAsync,
);
const [activeKey, setActiveKey] = useState<string[] | undefined>();
useEffect(() => {
if (!expandableModalIsOpen && activeKey !== undefined) {
setActiveKey(undefined);
}
// See issue #34630 for why we omit `activeKey` from the dependency array
}, [expandableModalIsOpen]);
return (
<Collapse
expandIconPosition="end"
accordion
modalMode
activeKey={activeKey}
onChange={key => setActiveKey(key)}
items={[
{
key: 'sql-lab',
label: (
<CollapseLabelInModal
title={t('SQL Lab')}
subtitle={t(
'Adjust how this database will interact with SQL Lab.',
)}
testId="sql-lab-label-test"
/>
),
children: (
<>
<StyledInputContainer css={no_margin_bottom}>
<div className="input-container">
<Checkbox
id="expose_in_sqllab"
name="expose_in_sqllab"
indeterminate={false}
checked={!!db?.expose_in_sqllab}
onChange={onInputChange}
>
{t('Expose database in SQL Lab')}
</Checkbox>
<InfoTooltip
tooltip={t('Allow this database to be queried in SQL Lab')}
/>
</div>
<StyledExpandableForm
className={cx('expandable', {
open: expandableModalIsOpen,
'ctas-open': createAsOpen,
})}
>
<StyledInputContainer css={no_margin_bottom}>
<div className="input-container">
<Checkbox
id="allow_ctas"
name="allow_ctas"
indeterminate={false}
checked={!!db?.allow_ctas}
onChange={onInputChange}
>
{t('Allow CREATE TABLE AS')}
</Checkbox>
<InfoTooltip
tooltip={t(
'Allow creation of new tables based on queries',
)}
/>
</div>
</StyledInputContainer>
<StyledInputContainer css={no_margin_bottom}>
<div className="input-container">
<Checkbox
id="allow_cvas"
name="allow_cvas"
indeterminate={false}
checked={!!db?.allow_cvas}
onChange={onInputChange}
>
{t('Allow CREATE VIEW AS')}
</Checkbox>
<InfoTooltip
tooltip={t(
'Allow creation of new views based on queries',
)}
/>
</div>
<StyledInputContainer
className={cx('expandable', { open: createAsOpen })}
>
<div className="control-label">
{t('CTAS & CVAS SCHEMA')}
</div>
<div className="input-container">
<Input
type="text"
name="force_ctas_schema"
placeholder={t('Create or select schema...')}
onChange={onInputChange}
value={db?.force_ctas_schema || ''}
/>
</div>
<div className="helper">
{t(
'Force all tables and views to be created in this schema when clicking CTAS or CVAS in SQL Lab.',
)}
</div>
</StyledInputContainer>
</StyledInputContainer>
<StyledInputContainer css={no_margin_bottom}>
<div className="input-container">
<Checkbox
id="allow_dml"
name="allow_dml"
indeterminate={false}
checked={!!db?.allow_dml}
onChange={onInputChange}
>
{t('Allow DDL and DML')}
</Checkbox>
<InfoTooltip
tooltip={t(
'Allow the execution of DDL (Data Definition Language: CREATE, DROP, TRUNCATE, etc.) and DML (Data Modification Language: INSERT, UPDATE, DELETE, etc)',
)}
/>
</div>
</StyledInputContainer>
<StyledInputContainer css={no_margin_bottom}>
<div className="input-container">
<Checkbox
id="cost_estimate_enabled"
name="cost_estimate_enabled"
indeterminate={false}
checked={!!extraJson?.cost_estimate_enabled}
onChange={onExtraInputChange}
>
{t('Enable query cost estimation')}
</Checkbox>
<InfoTooltip
tooltip={t(
'For Bigquery, Presto and Postgres, shows a button to compute cost before running a query.',
)}
/>
</div>
</StyledInputContainer>
<StyledInputContainer css={no_margin_bottom}>
<div className="input-container">
<Checkbox
id="allows_virtual_table_explore"
name="allows_virtual_table_explore"
indeterminate={false}
// when `allows_virtual_table_explore` is not present in `extra` it defaults to true
checked={
extraJson?.allows_virtual_table_explore !== false
}
onChange={onExtraInputChange}
>
{t('Allow this database to be explored')}
</Checkbox>
<InfoTooltip
tooltip={t(
'When enabled, users are able to visualize SQL Lab results in Explore.',
)}
/>
</div>
</StyledInputContainer>
<StyledInputContainer css={no_margin_bottom}>
<div className="input-container">
<Checkbox
id="disable_data_preview"
name="disable_data_preview"
indeterminate={false}
checked={!!extraJson?.disable_data_preview}
onChange={onExtraInputChange}
>
{t('Disable SQL Lab data preview queries')}
</Checkbox>
<InfoTooltip
tooltip={t(
'Disable data preview when fetching table metadata in SQL Lab. ' +
' Useful to avoid browser performance issues when using ' +
' databases with very wide tables.',
)}
/>
</div>
</StyledInputContainer>
<StyledInputContainer>
<div className="input-container">
<Checkbox
id="expand_rows"
name="expand_rows"
indeterminate={false}
checked={!!extraJson?.schema_options?.expand_rows}
onChange={onExtraInputChange}
>
{t('Enable row expansion in schemas')}
</Checkbox>
<InfoTooltip
tooltip={t(
'For Trino, describe full schemas of nested ROW types, expanding them with dotted paths',
)}
/>
</div>
</StyledInputContainer>
</StyledExpandableForm>
</StyledInputContainer>
</>
),
},
{
key: 'performance',
label: (
<CollapseLabelInModal
title={t('Performance')}
subtitle={t('Adjust performance settings of this database.')}
testId="performance-label-test"
/>
),
children: (
<>
<StyledInputContainer className="mb-8">
<div className="control-label">{t('Chart cache timeout')}</div>
<div className="input-container">
<Input
type="number"
name="cache_timeout"
value={db?.cache_timeout || ''}
placeholder={t('Enter duration in seconds')}
onChange={onInputChange}
data-test="cache-timeout-test"
/>
</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, and -1 bypasses the cache.' +
' Note this defaults to the global timeout if undefined.',
)}
</div>
</StyledInputContainer>
<StyledInputContainer>
<div className="control-label">{t('Schema cache timeout')}</div>
<div className="input-container">
<Input
type="number"
name="schema_cache_timeout"
value={
extraJson?.metadata_cache_timeout?.schema_cache_timeout ||
''
}
placeholder={t('Enter duration in seconds')}
onChange={onExtraInputChange}
data-test="schema-cache-timeout-test"
/>
</div>
<div className="helper">
{t(
'Duration (in seconds) of the metadata caching timeout for schemas of ' +
'this database. If left unset, the cache never expires.',
)}
</div>
</StyledInputContainer>
<StyledInputContainer>
<div className="control-label">{t('Table cache timeout')}</div>
<div className="input-container">
<Input
type="number"
name="table_cache_timeout"
value={
extraJson?.metadata_cache_timeout?.table_cache_timeout ||
''
}
placeholder={t('Enter duration in seconds')}
onChange={onExtraInputChange}
data-test="table-cache-timeout-test"
/>
</div>
<div className="helper">
{t(
'Duration (in seconds) of the metadata caching timeout for tables of ' +
'this database. If left unset, the cache never expires. ',
)}
</div>
</StyledInputContainer>
<StyledInputContainer css={{ no_margin_bottom }}>
<div className="input-container">
<Checkbox
id="allow_run_async"
name="allow_run_async"
indeterminate={false}
checked={allowRunAsync}
onChange={onInputChange}
>
{t('Asynchronous query execution')}
</Checkbox>
<InfoTooltip
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.',
)}
/>
{isAllowRunAsyncDisabled && (
<InfoTooltip
iconStyle={{ color: theme.colorError }}
tooltip={t(
'This option has been disabled by the administrator.',
)}
/>
)}
</div>
</StyledInputContainer>
<StyledInputContainer css={{ no_margin_bottom }}>
<div className="input-container">
<Checkbox
id="cancel_query_on_windows_unload"
name="cancel_query_on_windows_unload"
indeterminate={false}
checked={!!extraJson?.cancel_query_on_windows_unload}
onChange={onExtraInputChange}
>
{t('Cancel query on window unload event')}
</Checkbox>
<InfoTooltip
tooltip={t(
'Terminate running queries when browser window closed or navigated ' +
'to another page. Available for Presto, Hive, MySQL, Postgres and ' +
'Snowflake databases.',
)}
/>
</div>
</StyledInputContainer>
</>
),
},
{
key: 'security',
label: (
<CollapseLabelInModal
title={t('Security')}
testId="security-label-test"
subtitle={t('Add extra connection information.')}
/>
),
children: (
<>
<StyledInputContainer
css={!isFileUploadSupportedByEngine ? no_margin_bottom : {}}
>
<div className="input-container">
<Checkbox
id="per_user_caching"
name="per_user_caching"
indeterminate={false}
checked={!!extraJson?.per_user_caching}
onChange={onExtraInputChange}
>
{t('Per user caching')}
</Checkbox>
<InfoTooltip
tooltip={t(
'Cache data separately for each user based on their data access roles and permissions. ' +
'When disabled, a single cache will be used for all users.',
)}
/>
</div>
</StyledInputContainer>
<StyledInputContainer
css={!isFileUploadSupportedByEngine ? no_margin_bottom : {}}
>
<div className="input-container">
<Checkbox
id="impersonate_user"
name="impersonate_user"
indeterminate={false}
checked={!!db?.impersonate_user}
onChange={onInputChange}
>
{t(
'Impersonate logged in user (Presto, Trino, Drill, Hive, and Google Sheets)',
)}
</Checkbox>
<InfoTooltip
tooltip={t(
'If Presto or Trino, 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>
{isFileUploadSupportedByEngine && (
<StyledInputContainer
css={!db?.allow_file_upload ? no_margin_bottom : {}}
>
<div className="input-container">
<Checkbox
id="allow_file_upload"
name="allow_file_upload"
indeterminate={false}
checked={!!db?.allow_file_upload}
onChange={onInputChange}
>
{t('Allow file uploads to database')}
</Checkbox>
</div>
</StyledInputContainer>
)}
{isFileUploadSupportedByEngine && !!db?.allow_file_upload && (
<StyledInputContainer css={no_margin_bottom}>
<div className="control-label">
{t('Schemas allowed for File upload')}
</div>
<div className="input-container">
<Input
type="text"
name="schemas_allowed_for_file_upload"
value={(
extraJson?.schemas_allowed_for_file_upload || []
).join(',')}
placeholder="schema1,schema2"
onChange={onExtraInputChange}
/>
</div>
<div className="helper">
{t(
'A comma-separated list of schemas that files are allowed to upload to.',
)}
</div>
</StyledInputContainer>
)}
<StyledInputContainer>
<div className="control-label">{t('Secure extra')}</div>
<div className="input-container">
<StyledJsonEditor
name="masked_encrypted_extra"
value={db?.masked_encrypted_extra || ''}
placeholder={t('Secure extra')}
onChange={(json: string) =>
onEditorChange({ json, name: 'masked_encrypted_extra' })
}
width="100%"
height="160px"
annotations={secureExtraAnnotations}
/>
</div>
<div className="helper">
<div>
{t(
'JSON string containing additional connection configuration. ' +
'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="control-label">{t('Root certificate')}</div>
<div className="input-container">
<Input.TextArea
name="server_cert"
value={db?.server_cert || ''}
placeholder={t('Enter CA_BUNDLE')}
onChange={onTextChange}
/>
</div>
<div className="helper">
{t(
'Optional CA_BUNDLE contents to validate HTTPS requests. Only ' +
'available on certain database engines.',
)}
</div>
</StyledInputContainer>
</>
),
},
...(extraExtension && ExtraExtensionComponent && ExtensionDescription
? [
{
key: extraExtension?.title,
collapsible: extraExtension.enabled?.()
? ('icon' as const)
: ('disabled' as const),
label: (
<CollapseLabelInModal
key={extraExtension?.title}
title={
<>
{ExtraExtensionLogo && <ExtraExtensionLogo />}
{extraExtension?.title}
</>
}
subtitle={<ExtensionDescription />}
/>
),
children: (
<StyledInputContainer css={no_margin_bottom}>
<ExtraExtensionComponent
db={db}
onEdit={extraExtension.onEdit}
/>
</StyledInputContainer>
),
},
]
: []),
{
key: 'other',
label: (
<CollapseLabelInModal
title={t('Other')}
subtitle={t('Additional settings.')}
testId="other-label-test"
/>
),
children: (
<>
<StyledInputContainer>
<div className="control-label">{t('Metadata Parameters')}</div>
<div className="input-container">
<StyledJsonEditor
name="metadata_params"
placeholder={t('Metadata Parameters')}
onChange={(json: string) =>
onExtraEditorChange({ json, name: 'metadata_params' })
}
width="100%"
height="160px"
value={
!Object.keys(extraJson?.metadata_params || {}).length
? ''
: typeof extraJson?.metadata_params === 'string'
? extraJson?.metadata_params
: JSON.stringify(extraJson?.metadata_params)
}
annotations={metadataParamsAnnotations}
/>
</div>
<div className="helper">
<div>
{t(
'The metadata_params object gets unpacked into the sqlalchemy.MetaData call.',
)}
</div>
</div>
</StyledInputContainer>
<StyledInputContainer>
<div className="control-label">{t('Engine Parameters')}</div>
<div className="input-container">
<StyledJsonEditor
name="engine_params"
placeholder={t('Engine Parameters')}
onChange={(json: string) =>
onExtraEditorChange({ json, name: 'engine_params' })
}
width="100%"
height="160px"
value={
!Object.keys(extraJson?.engine_params || {}).length
? ''
: extraJson?.engine_params
}
annotations={engineParamsAnnotations}
/>
</div>
<div className="helper">
<div>
{t(
'The engine_params object gets unpacked into the sqlalchemy.create_engine call.',
)}
</div>
</div>
</StyledInputContainer>
<StyledInputContainer>
<div className="control-label" data-test="version-label-test">
{t('Version')}
</div>
<div
className="input-container"
data-test="version-spinbutton-test"
>
<Input
type="text"
name="version"
placeholder={t('Version number')}
onChange={onExtraInputChange}
value={extraJson?.version || ''}
/>
</div>
<div className="helper">
{t(
'Specify the database version. This is used with Presto for query cost ' +
'estimation, and Dremio for syntax changes, among others.',
)}
</div>
</StyledInputContainer>
<StyledInputContainer css={no_margin_bottom}>
<div className="input-container">
<Checkbox
id="disable_drill_to_detail"
name="disable_drill_to_detail"
indeterminate={false}
checked={!!extraJson?.disable_drill_to_detail}
onChange={onExtraInputChange}
>
{t('Disable drill to detail')}
</Checkbox>
<InfoTooltip
tooltip={t(
'Disables the drill to detail feature for this database.',
)}
/>
</div>
</StyledInputContainer>
{supportsDynamicCatalog && (
<StyledInputContainer css={no_margin_bottom}>
<div className="input-container">
<Checkbox
id="allow_multi_catalog"
name="allow_multi_catalog"
indeterminate={false}
checked={!!extraJson?.allow_multi_catalog}
onChange={onExtraInputChange}
>
{t('Allow changing catalogs')}
</Checkbox>
<InfoTooltip
tooltip={t(
'Give access to multiple catalogs in a single database connection.',
)}
/>
</div>
</StyledInputContainer>
)}
</>
),
},
]}
/>
);
};
export default ExtraOptions;