mirror of
https://github.com/apache/superset.git
synced 2026-06-14 03:59:22 +00:00
Compare commits
1 Commits
docs/testi
...
better-db-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f97b0ead9c |
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
import { styled, css, SupersetTheme, t } from '@superset-ui/core';
|
import { styled, css, SupersetTheme, t } from '@superset-ui/core';
|
||||||
import { Tooltip } from 'src/components/Tooltip';
|
import { Tooltip } from 'src/components/Tooltip';
|
||||||
import { Input } from 'src/components/Input';
|
import { Input, InputNumber } from 'src/components/Input';
|
||||||
import InfoTooltip from 'src/components/InfoTooltip';
|
import InfoTooltip from 'src/components/InfoTooltip';
|
||||||
import { Icons } from 'src/components/Icons';
|
import { Icons } from 'src/components/Icons';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
@@ -46,6 +46,10 @@ const StyledInput = styled(Input)`
|
|||||||
margin: ${({ theme }) => `${theme.gridUnit}px 0 ${theme.gridUnit * 2}px`};
|
margin: ${({ theme }) => `${theme.gridUnit}px 0 ${theme.gridUnit * 2}px`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledInputNumber = styled(InputNumber)`
|
||||||
|
margin: ${({ theme }) => `${theme.gridUnit}px 0 ${theme.gridUnit * 2}px`};
|
||||||
|
`;
|
||||||
|
|
||||||
const StyledInputPassword = styled(Input.Password)`
|
const StyledInputPassword = styled(Input.Password)`
|
||||||
margin: ${({ theme }) => `${theme.gridUnit}px 0 ${theme.gridUnit * 2}px`};
|
margin: ${({ theme }) => `${theme.gridUnit}px 0 ${theme.gridUnit * 2}px`};
|
||||||
`;
|
`;
|
||||||
@@ -99,6 +103,48 @@ const iconReset = css`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
interface GetInputComponentProps {
|
||||||
|
name?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInputComponent = (
|
||||||
|
props: GetInputComponentProps,
|
||||||
|
validationMethods: Record<string, unknown>,
|
||||||
|
visibilityToggle: boolean,
|
||||||
|
): JSX.Element => {
|
||||||
|
if (visibilityToggle || props.name === 'password') {
|
||||||
|
return (
|
||||||
|
<StyledInputPassword
|
||||||
|
{...props}
|
||||||
|
{...validationMethods}
|
||||||
|
iconRender={visible =>
|
||||||
|
visible ? (
|
||||||
|
<Tooltip title={t('Hide password.')}>
|
||||||
|
<Icons.EyeInvisibleOutlined iconSize="m" css={iconReset} />
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Tooltip title={t('Show password.')}>
|
||||||
|
<Icons.EyeOutlined
|
||||||
|
iconSize="m"
|
||||||
|
css={iconReset}
|
||||||
|
data-test="icon-eye"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
role="textbox"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.type === 'number') {
|
||||||
|
return <StyledInputNumber {...props} {...validationMethods} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <StyledInput {...props} {...validationMethods} />;
|
||||||
|
};
|
||||||
|
|
||||||
const LabeledErrorBoundInput = ({
|
const LabeledErrorBoundInput = ({
|
||||||
label,
|
label,
|
||||||
validationMethods,
|
validationMethods,
|
||||||
@@ -128,30 +174,7 @@ const LabeledErrorBoundInput = ({
|
|||||||
help={errorMessage || helpText}
|
help={errorMessage || helpText}
|
||||||
hasFeedback={!!errorMessage}
|
hasFeedback={!!errorMessage}
|
||||||
>
|
>
|
||||||
{visibilityToggle || props.name === 'password' ? (
|
{getInputComponent(props, validationMethods, visibilityToggle)}
|
||||||
<StyledInputPassword
|
|
||||||
{...props}
|
|
||||||
{...validationMethods}
|
|
||||||
iconRender={visible =>
|
|
||||||
visible ? (
|
|
||||||
<Tooltip title={t('Hide password.')}>
|
|
||||||
<Icons.EyeInvisibleOutlined iconSize="m" css={iconReset} />
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<Tooltip title={t('Show password.')}>
|
|
||||||
<Icons.EyeOutlined
|
|
||||||
iconSize="m"
|
|
||||||
css={iconReset}
|
|
||||||
data-test="icon-eye"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
role="textbox"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<StyledInput {...props} {...validationMethods} />
|
|
||||||
)}
|
|
||||||
{get_url && description ? (
|
{get_url && description ? (
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
|
|||||||
@@ -55,21 +55,19 @@ export const portField = ({
|
|||||||
validationErrors,
|
validationErrors,
|
||||||
db,
|
db,
|
||||||
}: FieldPropTypes) => (
|
}: FieldPropTypes) => (
|
||||||
<>
|
<ValidatedInput
|
||||||
<ValidatedInput
|
id="port"
|
||||||
id="port"
|
name="port"
|
||||||
name="port"
|
type="number"
|
||||||
type="number"
|
required={required}
|
||||||
required={required}
|
value={db?.parameters?.port as number}
|
||||||
value={db?.parameters?.port as number}
|
validationMethods={{ onBlur: getValidation }}
|
||||||
validationMethods={{ onBlur: getValidation }}
|
errorMessage={validationErrors?.port}
|
||||||
errorMessage={validationErrors?.port}
|
placeholder={t('e.g. 5432')}
|
||||||
placeholder={t('e.g. 5432')}
|
className="form-group-w-50"
|
||||||
className="form-group-w-50"
|
label={t('Port')}
|
||||||
label={t('Port')}
|
onChange={changeMethods.onParametersChange}
|
||||||
onChange={changeMethods.onParametersChange}
|
/>
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
export const httpPath = ({
|
export const httpPath = ({
|
||||||
required,
|
required,
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* import InfoTooltip from 'src/components/InfoTooltip'm
|
||||||
|
* 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 { useEffect } from 'react';
|
||||||
|
import { styled } from '@superset-ui/core';
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import { SelectValue } from 'antd/lib/select';
|
||||||
|
import InfoTooltip from 'src/components/InfoTooltip';
|
||||||
|
import Select from 'src/components/Select/Select';
|
||||||
|
import FormItem from 'src/components/Form/FormItem';
|
||||||
|
import FormLabel from 'src/components/Form/FormLabel';
|
||||||
|
import ValidatedInput from 'src/components/Form/LabeledErrorBoundInput';
|
||||||
|
import { DatabaseParameters, FieldPropTypes } from '../../types';
|
||||||
|
|
||||||
|
const StyledFormGroup = styled('div')`
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
margin-bottom: ${({ theme }) => theme.gridUnit * 3}px;
|
||||||
|
.ant-form-item {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledAlignment = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledFormLabel = styled(FormLabel)`
|
||||||
|
margin-bottom: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GenericField = ({
|
||||||
|
required,
|
||||||
|
changeMethods,
|
||||||
|
getValidation,
|
||||||
|
validationErrors,
|
||||||
|
db,
|
||||||
|
field,
|
||||||
|
parameter,
|
||||||
|
}: FieldPropTypes) => {
|
||||||
|
// set default values
|
||||||
|
useEffect(() => {
|
||||||
|
if (!db?.parameters?.[field as keyof DatabaseParameters] && parameter?.default !== undefined) {
|
||||||
|
changeMethods.onParametersChange({
|
||||||
|
target: { name: field, value: parameter.default },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOptionChange = (value: SelectValue) => {
|
||||||
|
changeMethods.onParametersChange({
|
||||||
|
target: { name: field, value },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// enums are mapped to select inputs
|
||||||
|
if (parameter?.enum) {
|
||||||
|
return (
|
||||||
|
<StyledFormGroup>
|
||||||
|
<StyledAlignment>
|
||||||
|
<StyledFormLabel htmlFor={field} required={required}>
|
||||||
|
{parameter.title}
|
||||||
|
</StyledFormLabel>
|
||||||
|
{parameter?.description && (
|
||||||
|
<InfoTooltip tooltip={parameter.description} />
|
||||||
|
)}
|
||||||
|
</StyledAlignment>
|
||||||
|
<FormItem>
|
||||||
|
<Select
|
||||||
|
onChange={handleOptionChange}
|
||||||
|
options={parameter.enum.map((value: string) => ({
|
||||||
|
value,
|
||||||
|
label: value,
|
||||||
|
}))}
|
||||||
|
value={
|
||||||
|
db?.parameters?.[field as keyof DatabaseParameters] ||
|
||||||
|
parameter?.default
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</StyledFormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// text/number inputs
|
||||||
|
return (
|
||||||
|
<ValidatedInput
|
||||||
|
id={field}
|
||||||
|
name={field}
|
||||||
|
type={parameter.type === 'integer' ? 'number' : 'text'}
|
||||||
|
required={required}
|
||||||
|
value={
|
||||||
|
db?.parameters?.[field as keyof DatabaseParameters] ||
|
||||||
|
parameter?.default
|
||||||
|
}
|
||||||
|
validationMethods={{ onBlur: getValidation }}
|
||||||
|
errorMessage={validationErrors?.[field]}
|
||||||
|
placeholder={parameter?.['x-placeholder']}
|
||||||
|
helpText={parameter?.['x-help-text']}
|
||||||
|
label={parameter.title}
|
||||||
|
hasTooltip={!!parameter?.description}
|
||||||
|
tooltipText={parameter?.description}
|
||||||
|
onChange={changeMethods.onParametersChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -17,16 +17,25 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { SupersetTheme } from '@superset-ui/core';
|
import { SupersetTheme } from '@superset-ui/core';
|
||||||
|
import { Col, Row } from 'src/components';
|
||||||
import { Form } from 'src/components/Form';
|
import { Form } from 'src/components/Form';
|
||||||
import { FormFieldOrder, FORM_FIELD_MAP } from './constants';
|
import { FORM_FIELD_MAP } from './constants';
|
||||||
import { formScrollableStyles, validatedFormStyles } from '../styles';
|
import { formScrollableStyles, validatedFormStyles } from '../styles';
|
||||||
import { DatabaseConnectionFormProps } from '../../types';
|
import { DatabaseConnectionFormProps, ParameterFieldSchema } from '../../types';
|
||||||
|
import { GenericField } from './GenericField';
|
||||||
|
|
||||||
|
interface ParametersSchema {
|
||||||
|
order: string[];
|
||||||
|
properties: {
|
||||||
|
[key: string]: ParameterFieldSchema;
|
||||||
|
};
|
||||||
|
required?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
const DatabaseConnectionForm = ({
|
const DatabaseConnectionForm = ({
|
||||||
dbModel,
|
dbModel,
|
||||||
db,
|
db,
|
||||||
editNewDb,
|
editNewDb,
|
||||||
getPlaceholder,
|
|
||||||
getValidation,
|
getValidation,
|
||||||
isEditMode = false,
|
isEditMode = false,
|
||||||
onAddTableCatalog,
|
onAddTableCatalog,
|
||||||
@@ -41,62 +50,63 @@ const DatabaseConnectionForm = ({
|
|||||||
validationErrors,
|
validationErrors,
|
||||||
clearValidationErrors,
|
clearValidationErrors,
|
||||||
}: DatabaseConnectionFormProps) => {
|
}: DatabaseConnectionFormProps) => {
|
||||||
const parameters = dbModel?.parameters as {
|
const parameters = dbModel?.parameters as ParametersSchema;
|
||||||
properties: {
|
|
||||||
[key: string]: {
|
let orderedEntries = Object.entries(parameters?.properties || []).sort(
|
||||||
default?: any;
|
([keyA], [keyB]) =>
|
||||||
description?: string;
|
parameters.order.indexOf(keyA) - parameters.order.indexOf(keyB),
|
||||||
};
|
);
|
||||||
};
|
|
||||||
required?: string[];
|
// database name should come first
|
||||||
};
|
orderedEntries = [
|
||||||
|
['database_name', { title: 'Name', type: 'string' }],
|
||||||
|
...orderedEntries,
|
||||||
|
];
|
||||||
|
|
||||||
|
const components = orderedEntries.map(([key, value]) => {
|
||||||
|
const FormComponent = FORM_FIELD_MAP[key] || GenericField;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col span={(value?.['x-width'] || 1) * 24}>
|
||||||
|
{FormComponent({
|
||||||
|
required: parameters.required?.includes(key),
|
||||||
|
changeMethods: {
|
||||||
|
onParametersChange,
|
||||||
|
onChange,
|
||||||
|
onQueryChange,
|
||||||
|
onParametersUploadFileChange,
|
||||||
|
onAddTableCatalog,
|
||||||
|
onRemoveTableCatalog,
|
||||||
|
onExtraInputChange,
|
||||||
|
onEncryptedExtraInputChange,
|
||||||
|
},
|
||||||
|
validationErrors,
|
||||||
|
getValidation,
|
||||||
|
clearValidationErrors,
|
||||||
|
db,
|
||||||
|
key,
|
||||||
|
field: key,
|
||||||
|
isEditMode,
|
||||||
|
sslForced,
|
||||||
|
editNewDb,
|
||||||
|
parameter: value,
|
||||||
|
})}
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<div
|
<div
|
||||||
// @ts-ignore
|
|
||||||
css={(theme: SupersetTheme) => [
|
css={(theme: SupersetTheme) => [
|
||||||
formScrollableStyles,
|
formScrollableStyles,
|
||||||
validatedFormStyles(theme),
|
validatedFormStyles(theme),
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{parameters &&
|
<Row gutter={[4, 4]}>{components}</Row>
|
||||||
FormFieldOrder.filter(
|
|
||||||
(key: string) =>
|
|
||||||
Object.keys(parameters.properties).includes(key) ||
|
|
||||||
key === 'database_name',
|
|
||||||
).map(field =>
|
|
||||||
// @ts-ignore TODO: fix ComponentClass for SSHTunnelSwitchComponent not having call signature.
|
|
||||||
FORM_FIELD_MAP[field]({
|
|
||||||
required: parameters.required?.includes(field),
|
|
||||||
changeMethods: {
|
|
||||||
onParametersChange,
|
|
||||||
onChange,
|
|
||||||
onQueryChange,
|
|
||||||
onParametersUploadFileChange,
|
|
||||||
onAddTableCatalog,
|
|
||||||
onRemoveTableCatalog,
|
|
||||||
onExtraInputChange,
|
|
||||||
onEncryptedExtraInputChange,
|
|
||||||
},
|
|
||||||
validationErrors,
|
|
||||||
getValidation,
|
|
||||||
clearValidationErrors,
|
|
||||||
db,
|
|
||||||
key: field,
|
|
||||||
field,
|
|
||||||
default_value: parameters.properties[field]?.default,
|
|
||||||
description: parameters.properties[field]?.description,
|
|
||||||
isEditMode,
|
|
||||||
sslForced,
|
|
||||||
editNewDb,
|
|
||||||
placeholder: getPlaceholder ? getPlaceholder(field) : undefined,
|
|
||||||
}),
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const FormFieldMap = FORM_FIELD_MAP;
|
|
||||||
|
|
||||||
export default DatabaseConnectionForm;
|
export default DatabaseConnectionForm;
|
||||||
|
|||||||
@@ -315,13 +315,12 @@ export interface FieldPropTypes {
|
|||||||
clearValidationErrors: () => void;
|
clearValidationErrors: () => void;
|
||||||
db?: DatabaseObject;
|
db?: DatabaseObject;
|
||||||
dbModel?: DatabaseForm;
|
dbModel?: DatabaseForm;
|
||||||
field: string;
|
|
||||||
default_value?: any;
|
|
||||||
description?: string;
|
|
||||||
isEditMode?: boolean;
|
isEditMode?: boolean;
|
||||||
sslForced?: boolean;
|
sslForced?: boolean;
|
||||||
defaultDBName?: string;
|
defaultDBName?: string;
|
||||||
editNewDb?: boolean;
|
editNewDb?: boolean;
|
||||||
|
field: string;
|
||||||
|
parameter: ParameterFieldSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChangeMethodsType = FieldPropTypes['changeMethods'];
|
type ChangeMethodsType = FieldPropTypes['changeMethods'];
|
||||||
@@ -369,3 +368,15 @@ export interface DatabaseConnectionFormProps {
|
|||||||
clearValidationErrors: () => void;
|
clearValidationErrors: () => void;
|
||||||
getPlaceholder?: (field: string) => string | undefined;
|
getPlaceholder?: (field: string) => string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Type for a field in the DB engine spec parameters schema */
|
||||||
|
export interface ParameterFieldSchema {
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
enum?: any[];
|
||||||
|
default?: any;
|
||||||
|
description?: string;
|
||||||
|
['x-help-text']?: string;
|
||||||
|
['x-placeholder']?: string;
|
||||||
|
['x-width']?: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1148,8 +1148,8 @@ SQLLAB_CTAS_NO_LIMIT = False
|
|||||||
# else:
|
# else:
|
||||||
# return f'tmp_{schema}'
|
# return f'tmp_{schema}'
|
||||||
# Function accepts database object, user object, schema name and sql that will be run.
|
# Function accepts database object, user object, schema name and sql that will be run.
|
||||||
SQLLAB_CTAS_SCHEMA_NAME_FUNC: (
|
SQLLAB_CTAS_SCHEMA_NAME_FUNC: None | (
|
||||||
None | (Callable[[Database, models.User, str, str], str])
|
Callable[[Database, models.User, str, str], str]
|
||||||
) = None
|
) = None
|
||||||
|
|
||||||
# If enabled, it can be used to store the results of long-running queries
|
# If enabled, it can be used to store the results of long-running queries
|
||||||
@@ -1430,6 +1430,20 @@ EXCLUDE_USERS_FROM_LISTS: list[str] | None = None
|
|||||||
# e.g., DBS_AVAILABLE_DENYLIST: Dict[str, Set[str]] = {"databricks": {"pyhive", "pyodbc"}} # noqa: E501
|
# e.g., DBS_AVAILABLE_DENYLIST: Dict[str, Set[str]] = {"databricks": {"pyhive", "pyodbc"}} # noqa: E501
|
||||||
DBS_AVAILABLE_DENYLIST: dict[str, set[str]] = {}
|
DBS_AVAILABLE_DENYLIST: dict[str, set[str]] = {}
|
||||||
|
|
||||||
|
# Allow Superset to use 3rd party DB engine specs, distributed in separate Python
|
||||||
|
# packages, and registered under the `superset.db_engine_specs` entry point. Before
|
||||||
|
# enabling this you should audit packages in your system that would be loaded, to
|
||||||
|
# prevent a malicious package from injecting a DB engine spec that could steal database
|
||||||
|
# credentials and have full access to your data. You can do this by running:
|
||||||
|
#
|
||||||
|
# import importlib.metadata
|
||||||
|
#
|
||||||
|
# entry_points = importlib.metadata.entry_points().get("superset.db_engine_specs", [])
|
||||||
|
# for ep in entry_points:
|
||||||
|
# print(ep.module)
|
||||||
|
#
|
||||||
|
ALLOW_3RD_PARTY_DB_ENGINE_SPECS = False
|
||||||
|
|
||||||
# This auth provider is used by background (offline) tasks that need to access
|
# This auth provider is used by background (offline) tasks that need to access
|
||||||
# protected resources. Can be overridden by end users in order to support
|
# protected resources. Can be overridden by end users in order to support
|
||||||
# custom auth mechanisms
|
# custom auth mechanisms
|
||||||
|
|||||||
@@ -243,3 +243,8 @@ class CacheRegion(StrEnum):
|
|||||||
DEFAULT = "default"
|
DEFAULT = "default"
|
||||||
DATA = "data"
|
DATA = "data"
|
||||||
THUMBNAIL = "thumbnail"
|
THUMBNAIL = "thumbnail"
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_SQLALCHEMY_PLACEHOLDER = (
|
||||||
|
"engine+driver://user:password@host:port/dbname[?key1=value1&key2=value2...]"
|
||||||
|
)
|
||||||
|
|||||||
@@ -72,7 +72,10 @@ from superset.commands.importers.exceptions import (
|
|||||||
NoValidFilesFoundError,
|
NoValidFilesFoundError,
|
||||||
)
|
)
|
||||||
from superset.commands.importers.v1.utils import get_contents_from_bundle
|
from superset.commands.importers.v1.utils import get_contents_from_bundle
|
||||||
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
|
from superset.constants import (
|
||||||
|
MODEL_API_RW_METHOD_PERMISSION_MAP,
|
||||||
|
RouteMethod,
|
||||||
|
)
|
||||||
from superset.daos.database import DatabaseDAO
|
from superset.daos.database import DatabaseDAO
|
||||||
from superset.databases.decorators import check_table_access
|
from superset.databases.decorators import check_table_access
|
||||||
from superset.databases.filters import DatabaseFilter, DatabaseUploadEnabledFilter
|
from superset.databases.filters import DatabaseFilter, DatabaseUploadEnabledFilter
|
||||||
@@ -126,6 +129,7 @@ from superset.utils.core import (
|
|||||||
get_username,
|
get_username,
|
||||||
parse_js_uri_path_item,
|
parse_js_uri_path_item,
|
||||||
)
|
)
|
||||||
|
from superset.utils.database import parameters_json_schema
|
||||||
from superset.utils.decorators import transaction
|
from superset.utils.decorators import transaction
|
||||||
from superset.utils.oauth2 import decode_oauth2_state
|
from superset.utils.oauth2 import decode_oauth2_state
|
||||||
from superset.utils.ssh_tunnel import mask_password_info
|
from superset.utils.ssh_tunnel import mask_password_info
|
||||||
@@ -1896,14 +1900,10 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
|||||||
payload["default_driver"] = engine_spec.default_driver
|
payload["default_driver"] = engine_spec.default_driver
|
||||||
|
|
||||||
# show configuration parameters for DBs that support it
|
# show configuration parameters for DBs that support it
|
||||||
if (
|
if engine_spec.parameters_schema:
|
||||||
hasattr(engine_spec, "parameters_json_schema")
|
payload["parameters"] = parameters_json_schema(
|
||||||
and hasattr(engine_spec, "sqlalchemy_uri_placeholder")
|
engine_spec.__name__,
|
||||||
and engine_spec.default_driver in drivers
|
engine_spec.parameters_schema,
|
||||||
):
|
|
||||||
payload["parameters"] = engine_spec.parameters_json_schema()
|
|
||||||
payload["sqlalchemy_uri_placeholder"] = (
|
|
||||||
engine_spec.sqlalchemy_uri_placeholder
|
|
||||||
)
|
)
|
||||||
|
|
||||||
available_databases.append(payload)
|
available_databases.append(payload)
|
||||||
|
|||||||
@@ -118,13 +118,11 @@ def make_url_safe(raw_url: str | URL) -> URL:
|
|||||||
:param raw_url:
|
:param raw_url:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
if not isinstance(raw_url, str):
|
||||||
if isinstance(raw_url, str):
|
|
||||||
url = raw_url.strip()
|
|
||||||
try:
|
|
||||||
return make_url(url) # noqa
|
|
||||||
except Exception as ex:
|
|
||||||
raise DatabaseInvalidError() from ex
|
|
||||||
|
|
||||||
else:
|
|
||||||
return raw_url
|
return raw_url
|
||||||
|
|
||||||
|
url = raw_url.strip()
|
||||||
|
try:
|
||||||
|
return make_url(url) # noqa
|
||||||
|
except Exception as ex:
|
||||||
|
raise DatabaseInvalidError() from ex
|
||||||
|
|||||||
@@ -61,9 +61,26 @@ def is_engine_spec(obj: Any) -> bool:
|
|||||||
def load_engine_specs() -> list[type[BaseEngineSpec]]:
|
def load_engine_specs() -> list[type[BaseEngineSpec]]:
|
||||||
"""
|
"""
|
||||||
Load all engine specs, native and 3rd party.
|
Load all engine specs, native and 3rd party.
|
||||||
|
|
||||||
|
For context, DB engine specs can be installed from 3rd party Python packages via
|
||||||
|
entry points. This allows DB vendor to maintain their own engine specs in a release
|
||||||
|
cycle that's independent from Superset's.
|
||||||
|
|
||||||
|
These DB engine specs can replace the ones that come with Superset, by specifying
|
||||||
|
the `replaces` class attribute.
|
||||||
"""
|
"""
|
||||||
engine_specs: list[type[BaseEngineSpec]] = []
|
engine_specs: list[type[BaseEngineSpec]] = []
|
||||||
|
|
||||||
|
# load 3rd party engine specs first, so they have prioerity
|
||||||
|
if app.config["ALLOW_3RD_PARTY_DB_ENGINE_SPECS"]:
|
||||||
|
for ep in entry_points(group="superset.db_engine_specs"):
|
||||||
|
try:
|
||||||
|
engine_spec = ep.load()
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
logger.warning("Unable to load Superset DB engine spec: %s", ep.name)
|
||||||
|
continue
|
||||||
|
engine_specs.append(engine_spec)
|
||||||
|
|
||||||
# load standard engines
|
# load standard engines
|
||||||
db_engine_spec_dir = str(Path(__file__).parent)
|
db_engine_spec_dir = str(Path(__file__).parent)
|
||||||
for module_info in pkgutil.iter_modules([db_engine_spec_dir], prefix="."):
|
for module_info in pkgutil.iter_modules([db_engine_spec_dir], prefix="."):
|
||||||
@@ -73,14 +90,16 @@ def load_engine_specs() -> list[type[BaseEngineSpec]]:
|
|||||||
for attr in module.__dict__
|
for attr in module.__dict__
|
||||||
if is_engine_spec(getattr(module, attr))
|
if is_engine_spec(getattr(module, attr))
|
||||||
)
|
)
|
||||||
# load additional engines from external modules
|
|
||||||
for ep in entry_points(group="superset.db_engine_specs"):
|
# remove replaced engine specs
|
||||||
try:
|
replaced = {
|
||||||
engine_spec = ep.load()
|
replaced_spec
|
||||||
except Exception: # pylint: disable=broad-except
|
for engine_spec in engine_specs
|
||||||
logger.warning("Unable to load Superset DB engine spec: %s", ep.name)
|
for replaced_spec in engine_spec.replaces
|
||||||
continue
|
}
|
||||||
engine_specs.append(engine_spec)
|
engine_specs = [
|
||||||
|
engine_spec for engine_spec in engine_specs if engine_spec not in replaced
|
||||||
|
]
|
||||||
|
|
||||||
return engine_specs
|
return engine_specs
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ from uuid import uuid4
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import requests
|
import requests
|
||||||
import sqlparse
|
import sqlparse
|
||||||
from apispec import APISpec
|
|
||||||
from apispec.ext.marshmallow import MarshmallowPlugin
|
|
||||||
from deprecation import deprecated
|
from deprecation import deprecated
|
||||||
from flask import current_app, g, url_for
|
from flask import current_app, g, url_for
|
||||||
from flask_appbuilder.security.sqla.models import User
|
from flask_appbuilder.security.sqla.models import User
|
||||||
@@ -60,7 +58,11 @@ from sqlalchemy.types import TypeEngine
|
|||||||
from sqlparse.tokens import CTE
|
from sqlparse.tokens import CTE
|
||||||
|
|
||||||
from superset import db, sql_parse
|
from superset import db, sql_parse
|
||||||
from superset.constants import QUERY_CANCEL_KEY, TimeGrain as TimeGrainConstants
|
from superset.constants import (
|
||||||
|
DEFAULT_SQLALCHEMY_PLACEHOLDER,
|
||||||
|
QUERY_CANCEL_KEY,
|
||||||
|
TimeGrain as TimeGrainConstants,
|
||||||
|
)
|
||||||
from superset.databases.utils import get_table_metadata, make_url_safe
|
from superset.databases.utils import get_table_metadata, make_url_safe
|
||||||
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
||||||
from superset.exceptions import DisallowedSQLFunction, OAuth2Error, OAuth2RedirectError
|
from superset.exceptions import DisallowedSQLFunction, OAuth2Error, OAuth2RedirectError
|
||||||
@@ -139,7 +141,9 @@ builtin_time_grains: dict[str | None, str] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TimestampExpression(ColumnClause): # pylint: disable=abstract-method, too-many-ancestors
|
class TimestampExpression(
|
||||||
|
ColumnClause
|
||||||
|
): # pylint: disable=abstract-method, too-many-ancestors
|
||||||
def __init__(self, expr: str, col: ColumnClause, **kwargs: Any) -> None:
|
def __init__(self, expr: str, col: ColumnClause, **kwargs: Any) -> None:
|
||||||
"""Sqlalchemy class that can be used to render native column elements respecting
|
"""Sqlalchemy class that can be used to render native column elements respecting
|
||||||
engine-specific quoting rules as part of a string-based expression.
|
engine-specific quoting rules as part of a string-based expression.
|
||||||
@@ -212,11 +216,14 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
|||||||
engine_aliases: set[str] = set()
|
engine_aliases: set[str] = set()
|
||||||
drivers: dict[str, str] = {}
|
drivers: dict[str, str] = {}
|
||||||
default_driver: str | None = None
|
default_driver: str | None = None
|
||||||
|
sqlalchemy_uri_placeholder = DEFAULT_SQLALCHEMY_PLACEHOLDER
|
||||||
|
|
||||||
# placeholder with the SQLAlchemy URI template
|
# 3rd-party DB engine specs can define this when they replace a built-in engine spec
|
||||||
sqlalchemy_uri_placeholder = (
|
replaces: set["BaseEngineSpec"] = set()
|
||||||
"engine+driver://user:password@host:port/dbname[?key=value&key=value...]"
|
|
||||||
)
|
# schema of parameters needed to build the SQLAlchemy URI
|
||||||
|
parameters_schema: Schema | None = None
|
||||||
|
encryption_parameters: dict[str, str] = {}
|
||||||
|
|
||||||
disable_ssh_tunneling = False
|
disable_ssh_tunneling = False
|
||||||
|
|
||||||
@@ -398,9 +405,9 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
|||||||
max_column_name_length: int | None = None
|
max_column_name_length: int | None = None
|
||||||
try_remove_schema_from_table_name = True # pylint: disable=invalid-name
|
try_remove_schema_from_table_name = True # pylint: disable=invalid-name
|
||||||
run_multiple_statements_as_one = False
|
run_multiple_statements_as_one = False
|
||||||
custom_errors: dict[
|
custom_errors: dict[Pattern[str], tuple[str, SupersetErrorType, dict[str, Any]]] = (
|
||||||
Pattern[str], tuple[str, SupersetErrorType, dict[str, Any]]
|
{}
|
||||||
] = {}
|
)
|
||||||
|
|
||||||
# List of JSON path to fields in `encrypted_extra` that should be masked when the
|
# List of JSON path to fields in `encrypted_extra` that should be masked when the
|
||||||
# database is edited. By default everything is masked.
|
# database is edited. By default everything is masked.
|
||||||
@@ -613,6 +620,46 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
|||||||
"""
|
"""
|
||||||
return cls.allows_alias_in_select
|
return cls.allows_alias_in_select
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def build_sqlalchemy_uri( # pylint: disable=unused-argument
|
||||||
|
cls,
|
||||||
|
parameters: dict[str, Any],
|
||||||
|
encrypted_extra: dict[str, str] | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Method to build SQLAlchemy URI from discrete parameters.
|
||||||
|
|
||||||
|
This requires a Marshmallow schema to be set in the `parameters_schema` class
|
||||||
|
attribute.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_parameters_from_uri( # pylint: disable=unused-argument
|
||||||
|
cls,
|
||||||
|
uri: str,
|
||||||
|
encrypted_extra: dict[str, Any] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Method to extract parameters from SQLAlchemy URI.
|
||||||
|
|
||||||
|
This is the opposite of `build_sqlalchemy_uri`.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate_parameters(
|
||||||
|
cls,
|
||||||
|
properties: BasicPropertiesType,
|
||||||
|
) -> list[SupersetError]:
|
||||||
|
"""
|
||||||
|
Validates parameters.
|
||||||
|
|
||||||
|
See the `BasicParametersMixin` class for an example of progressive validation of
|
||||||
|
parameters.
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def supports_url(cls, url: URL) -> bool:
|
def supports_url(cls, url: URL) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -2385,13 +2432,6 @@ class BasicParametersMixin:
|
|||||||
# schema describing the parameters used to configure the DB
|
# schema describing the parameters used to configure the DB
|
||||||
parameters_schema = BasicParametersSchema()
|
parameters_schema = BasicParametersSchema()
|
||||||
|
|
||||||
# recommended driver name for the DB engine spec
|
|
||||||
default_driver = ""
|
|
||||||
|
|
||||||
# query parameter to enable encryption in the database connection
|
|
||||||
# for Postgres this would be `{"sslmode": "verify-ca"}`, eg.
|
|
||||||
encryption_parameters: dict[str, str] = {}
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build_sqlalchemy_uri( # pylint: disable=unused-argument
|
def build_sqlalchemy_uri( # pylint: disable=unused-argument
|
||||||
cls,
|
cls,
|
||||||
@@ -2422,7 +2462,9 @@ class BasicParametersMixin:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_parameters_from_uri( # pylint: disable=unused-argument
|
def get_parameters_from_uri( # pylint: disable=unused-argument
|
||||||
cls, uri: str, encrypted_extra: dict[str, Any] | None = None
|
cls,
|
||||||
|
uri: str,
|
||||||
|
encrypted_extra: dict[str, Any] | None = None,
|
||||||
) -> BasicParametersType:
|
) -> BasicParametersType:
|
||||||
url = make_url_safe(uri)
|
url = make_url_safe(uri)
|
||||||
query = {
|
query = {
|
||||||
@@ -2497,6 +2539,8 @@ class BasicParametersMixin:
|
|||||||
extra={"invalid": ["port"]},
|
extra={"invalid": ["port"]},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
return errors
|
||||||
|
|
||||||
if not (isinstance(port, int) and 0 <= port < 2**16):
|
if not (isinstance(port, int) and 0 <= port < 2**16):
|
||||||
errors.append(
|
errors.append(
|
||||||
SupersetError(
|
SupersetError(
|
||||||
@@ -2519,20 +2563,3 @@ class BasicParametersMixin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parameters_json_schema(cls) -> Any:
|
|
||||||
"""
|
|
||||||
Return configuration parameters as OpenAPI.
|
|
||||||
"""
|
|
||||||
if not cls.parameters_schema:
|
|
||||||
return None
|
|
||||||
|
|
||||||
spec = APISpec(
|
|
||||||
title="Database Parameters",
|
|
||||||
version="1.0.0",
|
|
||||||
openapi_version="3.0.2",
|
|
||||||
plugins=[MarshmallowPlugin()],
|
|
||||||
)
|
|
||||||
spec.components.schema(cls.__name__, schema=cls.parameters_schema)
|
|
||||||
return spec.to_dict()["components"]["schemas"][cls.__name__]
|
|
||||||
|
|||||||
@@ -639,26 +639,6 @@ class BigQueryEngineSpec(BaseEngineSpec): # pylint: disable=too-many-public-met
|
|||||||
) -> list[SupersetError]:
|
) -> list[SupersetError]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parameters_json_schema(cls) -> Any:
|
|
||||||
"""
|
|
||||||
Return configuration parameters as OpenAPI.
|
|
||||||
"""
|
|
||||||
if not cls.parameters_schema:
|
|
||||||
return None
|
|
||||||
|
|
||||||
spec = APISpec(
|
|
||||||
title="Database Parameters",
|
|
||||||
version="1.0.0",
|
|
||||||
openapi_version="3.0.0",
|
|
||||||
plugins=[ma_plugin],
|
|
||||||
)
|
|
||||||
|
|
||||||
ma_plugin.init_spec(spec)
|
|
||||||
ma_plugin.converter.add_attribute_function(encrypted_field_properties)
|
|
||||||
spec.components.schema(cls.__name__, schema=cls.parameters_schema)
|
|
||||||
return spec.to_dict()["components"]["schemas"][cls.__name__]
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def select_star( # pylint: disable=too-many-arguments
|
def select_star( # pylint: disable=too-many-arguments
|
||||||
cls,
|
cls,
|
||||||
|
|||||||
@@ -415,22 +415,6 @@ class DatabricksNativeEngineSpec(DatabricksDynamicBaseEngineSpec):
|
|||||||
"encryption": encryption,
|
"encryption": encryption,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parameters_json_schema(cls) -> Any:
|
|
||||||
"""
|
|
||||||
Return configuration parameters as OpenAPI.
|
|
||||||
"""
|
|
||||||
if not cls.properties_schema:
|
|
||||||
return None
|
|
||||||
|
|
||||||
spec = APISpec(
|
|
||||||
title="Database Parameters",
|
|
||||||
version="1.0.0",
|
|
||||||
openapi_version="3.0.2",
|
|
||||||
plugins=[MarshmallowPlugin()],
|
|
||||||
)
|
|
||||||
spec.components.schema(cls.__name__, schema=cls.properties_schema)
|
|
||||||
return spec.to_dict()["components"]["schemas"][cls.__name__]
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_default_catalog(
|
def get_default_catalog(
|
||||||
|
|||||||
@@ -171,23 +171,6 @@ class DuckDBParametersMixin:
|
|||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parameters_json_schema(cls) -> Any:
|
|
||||||
"""
|
|
||||||
Return configuration parameters as OpenAPI.
|
|
||||||
"""
|
|
||||||
if not cls.parameters_schema:
|
|
||||||
return None
|
|
||||||
|
|
||||||
spec = APISpec(
|
|
||||||
title="Database Parameters",
|
|
||||||
version="1.0.0",
|
|
||||||
openapi_version="3.0.2",
|
|
||||||
plugins=[MarshmallowPlugin()],
|
|
||||||
)
|
|
||||||
spec.components.schema(cls.__name__, schema=cls.parameters_schema)
|
|
||||||
return spec.to_dict()["components"]["schemas"][cls.__name__]
|
|
||||||
|
|
||||||
|
|
||||||
class DuckDBEngineSpec(DuckDBParametersMixin, BaseEngineSpec):
|
class DuckDBEngineSpec(DuckDBParametersMixin, BaseEngineSpec):
|
||||||
engine = "duckdb"
|
engine = "duckdb"
|
||||||
|
|||||||
@@ -205,26 +205,6 @@ class GSheetsEngineSpec(ShillelaghEngineSpec):
|
|||||||
|
|
||||||
raise ValidationError("Invalid service credentials")
|
raise ValidationError("Invalid service credentials")
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parameters_json_schema(cls) -> Any:
|
|
||||||
"""
|
|
||||||
Return configuration parameters as OpenAPI.
|
|
||||||
"""
|
|
||||||
if not cls.parameters_schema:
|
|
||||||
return None
|
|
||||||
|
|
||||||
spec = APISpec(
|
|
||||||
title="Database Parameters",
|
|
||||||
version="1.0.0",
|
|
||||||
openapi_version="3.0.0",
|
|
||||||
plugins=[ma_plugin],
|
|
||||||
)
|
|
||||||
|
|
||||||
ma_plugin.init_spec(spec)
|
|
||||||
ma_plugin.converter.add_attribute_function(encrypted_field_properties)
|
|
||||||
spec.components.schema(cls.__name__, schema=cls.parameters_schema)
|
|
||||||
return spec.to_dict()["components"]["schemas"][cls.__name__]
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_parameters(
|
def validate_parameters(
|
||||||
cls,
|
cls,
|
||||||
|
|||||||
@@ -350,24 +350,6 @@ class SnowflakeEngineSpec(PostgresBaseEngineSpec):
|
|||||||
)
|
)
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parameters_json_schema(cls) -> Any:
|
|
||||||
"""
|
|
||||||
Return configuration parameters as OpenAPI.
|
|
||||||
"""
|
|
||||||
if not cls.parameters_schema:
|
|
||||||
return None
|
|
||||||
|
|
||||||
ma_plugin = MarshmallowPlugin()
|
|
||||||
spec = APISpec(
|
|
||||||
title="Database Parameters",
|
|
||||||
version="1.0.0",
|
|
||||||
openapi_version="3.0.0",
|
|
||||||
plugins=[ma_plugin],
|
|
||||||
)
|
|
||||||
|
|
||||||
spec.components.schema(cls.__name__, schema=cls.parameters_schema)
|
|
||||||
return spec.to_dict()["components"]["schemas"][cls.__name__]
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_params_from_encrypted_extra(
|
def update_params_from_encrypted_extra(
|
||||||
|
|||||||
@@ -127,7 +127,9 @@ class ConfigurationMethod(StrEnum):
|
|||||||
DYNAMIC_FORM = "dynamic_form"
|
DYNAMIC_FORM = "dynamic_form"
|
||||||
|
|
||||||
|
|
||||||
class Database(Model, AuditMixinNullable, ImportExportMixin): # pylint: disable=too-many-public-methods
|
class Database(
|
||||||
|
Model, AuditMixinNullable, ImportExportMixin
|
||||||
|
): # pylint: disable=too-many-public-methods
|
||||||
"""An ORM object that stores Database related information"""
|
"""An ORM object that stores Database related information"""
|
||||||
|
|
||||||
__tablename__ = "dbs"
|
__tablename__ = "dbs"
|
||||||
@@ -306,16 +308,11 @@ class Database(Model, AuditMixinNullable, ImportExportMixin): # pylint: disable
|
|||||||
if (masked_encrypted_extra := self.masked_encrypted_extra) is not None:
|
if (masked_encrypted_extra := self.masked_encrypted_extra) is not None:
|
||||||
with suppress(TypeError, json.JSONDecodeError):
|
with suppress(TypeError, json.JSONDecodeError):
|
||||||
encrypted_config = json.loads(masked_encrypted_extra)
|
encrypted_config = json.loads(masked_encrypted_extra)
|
||||||
try:
|
|
||||||
# pylint: disable=useless-suppression
|
|
||||||
parameters = self.db_engine_spec.get_parameters_from_uri( # type: ignore
|
|
||||||
masked_uri,
|
|
||||||
encrypted_extra=encrypted_config,
|
|
||||||
)
|
|
||||||
except Exception: # pylint: disable=broad-except
|
|
||||||
parameters = {}
|
|
||||||
|
|
||||||
return parameters
|
return self.db_engine_spec.get_parameters_from_uri(
|
||||||
|
masked_uri,
|
||||||
|
encrypted_extra=encrypted_config,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parameters_schema(self) -> dict[str, Any]:
|
def parameters_schema(self) -> dict[str, Any]:
|
||||||
@@ -402,9 +399,7 @@ class Database(Model, AuditMixinNullable, ImportExportMixin): # pylint: disable
|
|||||||
return (
|
return (
|
||||||
username
|
username
|
||||||
if (username := get_username())
|
if (username := get_username())
|
||||||
else object_url.username
|
else object_url.username if self.impersonate_user else None
|
||||||
if self.impersonate_user
|
|
||||||
else None
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
|
|||||||
@@ -17,15 +17,20 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import Any, TYPE_CHECKING
|
||||||
|
|
||||||
|
from apispec import APISpec
|
||||||
|
from apispec.ext.marshmallow import MarshmallowPlugin
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
from superset.constants import EXAMPLES_DB_UUID
|
from superset.constants import EXAMPLES_DB_UUID
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from marshmallow import Schema
|
||||||
|
|
||||||
from superset.connectors.sqla.models import Database
|
from superset.connectors.sqla.models import Database
|
||||||
|
|
||||||
|
|
||||||
logging.getLogger("MARKDOWN").setLevel(logging.INFO)
|
logging.getLogger("MARKDOWN").setLevel(logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -80,3 +85,28 @@ def remove_database(database: Database) -> None:
|
|||||||
|
|
||||||
db.session.delete(database)
|
db.session.delete(database)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def parameters_json_schema(
|
||||||
|
name: str,
|
||||||
|
parameters_schema: Schema | None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Return configuration parameters as OpenAPI.
|
||||||
|
"""
|
||||||
|
if not parameters_schema:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
spec = APISpec(
|
||||||
|
title="Database Parameters",
|
||||||
|
version="1.0.0",
|
||||||
|
openapi_version="3.0.2",
|
||||||
|
plugins=[MarshmallowPlugin()],
|
||||||
|
)
|
||||||
|
spec.components.schema(name, schema=parameters_schema)
|
||||||
|
json_schema = spec.to_dict()["components"]["schemas"][name]
|
||||||
|
|
||||||
|
# preserve field order
|
||||||
|
json_schema["order"] = list(json_schema["properties"].keys())
|
||||||
|
|
||||||
|
return json_schema
|
||||||
|
|||||||
Reference in New Issue
Block a user