| { target: HTMLInputElement },
- ) => void;
- onAddTableCatalog: () => void;
- onRemoveTableCatalog: (idx: number) => void;
- validationErrors: JsonObject | null;
- getValidation: () => void;
-}) => (
- <>
- [
- formScrollableStyles,
- validatedFormStyles(theme),
- ]}
- >
- {parameters &&
- FormFieldOrder.filter(
- (key: string) =>
- Object.keys(parameters.properties).includes(key) ||
- key === 'database_name',
- ).map(field =>
- FORM_FIELD_MAP[field]({
- required: parameters.required?.includes(field),
- changeMethods: {
- onParametersChange,
- onChange,
- onQueryChange,
- onParametersUploadFileChange,
- onAddTableCatalog,
- onRemoveTableCatalog,
- },
- validationErrors,
- getValidation,
- db,
- key: field,
- isEditMode,
- sslForced,
- editNewDb,
- }),
- )}
-
- >
-);
-export const FormFieldMap = FORM_FIELD_MAP;
-
-export default DatabaseConnectionForm;
diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx
new file mode 100644
index 0000000000000..e1ea9fef5f8d7
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx
@@ -0,0 +1,207 @@
+/**
+ * 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 from 'react';
+import { SupersetTheme, t } from '@superset-ui/core';
+import { Switch } from 'src/common/components';
+import InfoTooltip from 'src/components/InfoTooltip';
+import ValidatedInput from 'src/components/Form/LabeledErrorBoundInput';
+import { FieldPropTypes } from '.';
+import { toggleStyle, infoTooltip } from '../styles';
+
+export const hostField = ({
+ required,
+ changeMethods,
+ getValidation,
+ validationErrors,
+ db,
+}: FieldPropTypes) => (
+
+);
+export const portField = ({
+ required,
+ changeMethods,
+ getValidation,
+ validationErrors,
+ db,
+}: FieldPropTypes) => (
+ <>
+
+ >
+);
+export const databaseField = ({
+ required,
+ changeMethods,
+ getValidation,
+ validationErrors,
+ db,
+}: FieldPropTypes) => (
+
+);
+export const usernameField = ({
+ required,
+ changeMethods,
+ getValidation,
+ validationErrors,
+ db,
+}: FieldPropTypes) => (
+
+);
+export const passwordField = ({
+ required,
+ changeMethods,
+ getValidation,
+ validationErrors,
+ db,
+ isEditMode,
+}: FieldPropTypes) => (
+
+);
+export const displayField = ({
+ changeMethods,
+ getValidation,
+ validationErrors,
+ db,
+}: FieldPropTypes) => (
+ <>
+
+ >
+);
+
+export const queryField = ({
+ required,
+ changeMethods,
+ getValidation,
+ validationErrors,
+ db,
+}: FieldPropTypes) => (
+
+);
+
+export const forceSSLField = ({
+ isEditMode,
+ changeMethods,
+ db,
+ sslForced,
+}: FieldPropTypes) => (
+ infoTooltip(theme)}>
+ {
+ changeMethods.onParametersChange({
+ target: {
+ type: 'toggle',
+ name: 'encryption',
+ checked: true,
+ value: changed,
+ },
+ });
+ }}
+ />
+ SSL
+
+
+);
diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/EncryptedField.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/EncryptedField.tsx
new file mode 100644
index 0000000000000..9403762ce2008
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/EncryptedField.tsx
@@ -0,0 +1,198 @@
+/**
+ * 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, { useState } from 'react';
+import { SupersetTheme, t } from '@superset-ui/core';
+import { Select, Button } from 'src/common/components';
+import InfoTooltip from 'src/components/InfoTooltip';
+import FormLabel from 'src/components/Form/FormLabel';
+import { DeleteFilled } from '@ant-design/icons';
+import { FieldPropTypes } from '.';
+import { infoTooltip, labelMarginBotton, CredentialInfoForm } from '../styles';
+
+enum CredentialInfoOptions {
+ jsonUpload,
+ copyPaste,
+}
+
+// These are the columns that are going to be added to encrypted extra, they differ in name based
+// on the engine, however we want to use the same component for each of them. Make sure to add the
+// the engine specific name here.
+export const encryptedCredentialsMap = {
+ gsheets: 'service_account_info',
+ bigquery: 'credentials_info',
+};
+
+const castStringToBoolean = (optionValue: string) => optionValue === 'true';
+
+export const EncryptedField = ({
+ changeMethods,
+ isEditMode,
+ db,
+ editNewDb,
+}: FieldPropTypes) => {
+ const [uploadOption, setUploadOption] = useState(
+ CredentialInfoOptions.jsonUpload.valueOf(),
+ );
+ const [fileToUpload, setFileToUpload] = useState(
+ null,
+ );
+ const [isPublic, setIsPublic] = useState(true);
+ const showCredentialsInfo =
+ db?.engine === 'gsheets' ? !isEditMode && !isPublic : !isEditMode;
+ // a database that has an optional encrypted field has an encrypted_extra that is an empty object, this checks for that.
+ const isEncrypted = isEditMode && db?.encrypted_extra !== '{}';
+ const encryptedField = db?.engine && encryptedCredentialsMap[db.engine];
+ const encryptedValue =
+ typeof db?.parameters?.[encryptedField] === 'object'
+ ? JSON.stringify(db?.parameters?.[encryptedField])
+ : db?.parameters?.[encryptedField];
+ return (
+
+ {db?.engine === 'gsheets' && (
+
+ labelMarginBotton(theme)}
+ required
+ >
+ {t('Type of Google Sheets allowed')}
+
+
+
+ )}
+ {showCredentialsInfo && (
+ <>
+
+ {t('How do you want to enter service account credentials?')}
+
+
+ >
+ )}
+ {uploadOption === CredentialInfoOptions.copyPaste ||
+ isEditMode ||
+ editNewDb ? (
+
+ {t('Service Account')}
+
+
+ {t('Copy and paste the entire service account .json file here')}
+
+
+ ) : (
+ showCredentialsInfo && (
+ infoTooltip(theme)}
+ >
+
+ {t('Upload Credentials')}
+
+
+
+ {!fileToUpload && (
+
+ )}
+ {fileToUpload && (
+
+ {fileToUpload}
+ {
+ setFileToUpload(null);
+ changeMethods.onParametersChange({
+ target: {
+ name: encryptedField,
+ value: '',
+ },
+ });
+ }}
+ />
+
+ )}
+
+
{
+ let file;
+ if (event.target.files) {
+ file = event.target.files[0];
+ }
+ setFileToUpload(file?.name);
+ changeMethods.onParametersChange({
+ target: {
+ type: null,
+ name: encryptedField,
+ value: await file?.text(),
+ checked: false,
+ },
+ });
+ (document.getElementById(
+ 'selectedFile',
+ ) as HTMLInputElement).value = null as any;
+ }}
+ />
+
+ )
+ )}
+
+ );
+};
diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/TableCatalog.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/TableCatalog.tsx
new file mode 100644
index 0000000000000..bc8cb40c161c7
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/TableCatalog.tsx
@@ -0,0 +1,104 @@
+/**
+ * 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 from 'react';
+import { t } from '@superset-ui/core';
+import ValidatedInput from 'src/components/Form/LabeledErrorBoundInput';
+import FormLabel from 'src/components/Form/FormLabel';
+import { CloseOutlined } from '@ant-design/icons';
+import { FieldPropTypes } from '.';
+import { StyledFooterButton, StyledCatalogTable } from '../styles';
+import { CatalogObject } from '../../types';
+
+export const TableCatalog = ({
+ required,
+ changeMethods,
+ getValidation,
+ validationErrors,
+ db,
+}: FieldPropTypes) => {
+ const tableCatalog = db?.catalog || [];
+ const catalogError = validationErrors || {};
+
+ return (
+
+
+ {t('Connect Google Sheets as tables to this database')}
+
+
+ {tableCatalog?.map((sheet: CatalogObject, idx: number) => (
+ <>
+
+ {t('Google Sheet Name and URL')}
+
+
+ {
+ changeMethods.onParametersChange({
+ target: {
+ type: `catalog-${idx}`,
+ name: 'name',
+ value: e.target.value,
+ },
+ });
+ }}
+ value={sheet.name}
+ />
+ {tableCatalog?.length > 1 && (
+ changeMethods.onRemoveTableCatalog(idx)}
+ />
+ )}
+
+
+ changeMethods.onParametersChange({
+ target: {
+ type: `catalog-${idx}`,
+ name: 'value',
+ value: e.target.value,
+ },
+ })
+ }
+ value={sheet.value}
+ />
+ >
+ ))}
+ {
+ changeMethods.onAddTableCatalog();
+ }}
+ >
+ + {t('Add sheet')}
+
+
+
+ );
+};
diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/ValidatedInputField.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/ValidatedInputField.tsx
new file mode 100644
index 0000000000000..93ba1f2d1ffe9
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/ValidatedInputField.tsx
@@ -0,0 +1,62 @@
+/**
+ * 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 from 'react';
+import { t } from '@superset-ui/core';
+import ValidatedInput from 'src/components/Form/LabeledErrorBoundInput';
+import { FieldPropTypes } from '.';
+
+const FIELD_TEXT_MAP = {
+ account: {
+ helpText: t(
+ 'Copy the account name of that database you are trying to connect to.',
+ ),
+ placeholder: 'e.g. world_population',
+ },
+ warehouse: {
+ placeholder: 'e.g. compute_wh',
+ className: 'form-group-w-50',
+ },
+ role: {
+ placeholder: 'e.g. AccountAdmin',
+ className: 'form-group-w-50',
+ },
+};
+
+export const validatedInputField = ({
+ required,
+ changeMethods,
+ getValidation,
+ validationErrors,
+ db,
+ field,
+}: FieldPropTypes) => (
+
+);
diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/index.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/index.tsx
new file mode 100644
index 0000000000000..0bb2cb4dad5f7
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/index.tsx
@@ -0,0 +1,172 @@
+/**
+ * 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, { FormEvent } from 'react';
+import { SupersetTheme, JsonObject } from '@superset-ui/core';
+import { InputProps } from 'antd/lib/input';
+import {
+ hostField,
+ portField,
+ databaseField,
+ usernameField,
+ passwordField,
+ displayField,
+ queryField,
+ forceSSLField,
+} from './CommonParameters';
+import { validatedInputField } from './ValidatedInputField';
+import { EncryptedField } from './EncryptedField';
+import { TableCatalog } from './TableCatalog';
+import { formScrollableStyles, validatedFormStyles } from '../styles';
+import { DatabaseForm, DatabaseObject } from '../../types';
+
+export const FormFieldOrder = [
+ 'host',
+ 'port',
+ 'database',
+ 'username',
+ 'password',
+ 'database_name',
+ 'credentials_info',
+ 'service_account_info',
+ 'catalog',
+ 'query',
+ 'encryption',
+ 'account',
+ 'warehouse',
+ 'role',
+];
+
+export interface FieldPropTypes {
+ required: boolean;
+ hasTooltip?: boolean;
+ tooltipText?: (value: any) => string;
+ onParametersChange: (value: any) => string;
+ onParametersUploadFileChange: (value: any) => string;
+ changeMethods: { onParametersChange: (value: any) => string } & {
+ onChange: (value: any) => string;
+ } & {
+ onQueryChange: (value: any) => string;
+ } & { onParametersUploadFileChange: (value: any) => string } & {
+ onAddTableCatalog: () => void;
+ onRemoveTableCatalog: (idx: number) => void;
+ };
+ validationErrors: JsonObject | null;
+ getValidation: () => void;
+ db?: DatabaseObject;
+ field: string;
+ isEditMode?: boolean;
+ sslForced?: boolean;
+ defaultDBName?: string;
+ editNewDb?: boolean;
+}
+
+const FORM_FIELD_MAP = {
+ host: hostField,
+ port: portField,
+ database: databaseField,
+ username: usernameField,
+ password: passwordField,
+ database_name: displayField,
+ query: queryField,
+ encryption: forceSSLField,
+ credentials_info: EncryptedField,
+ service_account_info: EncryptedField,
+ catalog: TableCatalog,
+ warehouse: validatedInputField,
+ role: validatedInputField,
+ account: validatedInputField,
+};
+
+const DatabaseConnectionForm = ({
+ dbModel: { parameters },
+ onParametersChange,
+ onChange,
+ onQueryChange,
+ onParametersUploadFileChange,
+ onAddTableCatalog,
+ onRemoveTableCatalog,
+ validationErrors,
+ getValidation,
+ db,
+ isEditMode = false,
+ sslForced,
+ editNewDb,
+}: {
+ isEditMode?: boolean;
+ sslForced: boolean;
+ editNewDb?: boolean;
+ dbModel: DatabaseForm;
+ db: Partial | null;
+ onParametersChange: (
+ event: FormEvent | { target: HTMLInputElement },
+ ) => void;
+ onChange: (
+ event: FormEvent | { target: HTMLInputElement },
+ ) => void;
+ onQueryChange: (
+ event: FormEvent | { target: HTMLInputElement },
+ ) => void;
+ onParametersUploadFileChange?: (
+ event: FormEvent | { target: HTMLInputElement },
+ ) => void;
+ onAddTableCatalog: () => void;
+ onRemoveTableCatalog: (idx: number) => void;
+ validationErrors: JsonObject | null;
+ getValidation: () => void;
+}) => (
+ <>
+ [
+ formScrollableStyles,
+ validatedFormStyles(theme),
+ ]}
+ >
+ {parameters &&
+ FormFieldOrder.filter(
+ (key: string) =>
+ Object.keys(parameters.properties).includes(key) ||
+ key === 'database_name',
+ ).map(field =>
+ FORM_FIELD_MAP[field]({
+ required: parameters.required?.includes(field),
+ changeMethods: {
+ onParametersChange,
+ onChange,
+ onQueryChange,
+ onParametersUploadFileChange,
+ onAddTableCatalog,
+ onRemoveTableCatalog,
+ },
+ validationErrors,
+ getValidation,
+ db,
+ key: field,
+ field,
+ isEditMode,
+ sslForced,
+ editNewDb,
+ }),
+ )}
+
+ >
+);
+export const FormFieldMap = FORM_FIELD_MAP;
+
+export default DatabaseConnectionForm;
diff --git a/superset-frontend/src/views/CRUD/data/database/types.ts b/superset-frontend/src/views/CRUD/data/database/types.ts
index 8d8ebdc76f312..4ffb69535cbe2 100644
--- a/superset-frontend/src/views/CRUD/data/database/types.ts
+++ b/superset-frontend/src/views/CRUD/data/database/types.ts
@@ -49,6 +49,9 @@ export type DatabaseObject = {
query?: Record;
catalog?: Record;
properties?: Record;
+ warehouse?: string;
+ role?: string;
+ account?: string;
};
configuration_method: CONFIGURATION_METHOD;
engine?: string;
diff --git a/superset/config.py b/superset/config.py
index cca27ff512b57..1492fdfcdab87 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -381,6 +381,7 @@ def _try_json_readsha(filepath: str, length: int) -> Optional[str]:
"ESCAPE_MARKDOWN_HTML": False,
"DASHBOARD_NATIVE_FILTERS": True,
"DASHBOARD_CROSS_FILTERS": False,
+ # Feature is under active development and breaking changes are expected
"DASHBOARD_NATIVE_FILTERS_SET": False,
"DASHBOARD_FILTERS_EXPERIMENTAL": False,
"GLOBAL_ASYNC_QUERIES": False,
diff --git a/superset/db_engine_specs/snowflake.py b/superset/db_engine_specs/snowflake.py
index 88ff0e20e092a..749a61f174fd4 100644
--- a/superset/db_engine_specs/snowflake.py
+++ b/superset/db_engine_specs/snowflake.py
@@ -17,14 +17,18 @@
import json
import re
from datetime import datetime
-from typing import Any, Dict, Optional, Pattern, Tuple, TYPE_CHECKING
+from typing import Any, Dict, List, Optional, Pattern, Tuple, TYPE_CHECKING
from urllib import parse
+from apispec import APISpec
+from apispec.ext.marshmallow import MarshmallowPlugin
from flask_babel import gettext as __
-from sqlalchemy.engine.url import URL
+from marshmallow import fields, Schema
+from sqlalchemy.engine.url import make_url, URL
+from typing_extensions import TypedDict
from superset.db_engine_specs.postgres import PostgresBaseEngineSpec
-from superset.errors import SupersetErrorType
+from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.models.sql_lab import Query
from superset.utils import core as utils
@@ -42,12 +46,34 @@
)
+class SnowflakeParametersSchema(Schema):
+ username = fields.Str(required=True)
+ password = fields.Str(required=True)
+ account = fields.Str(required=True)
+ database = fields.Str(required=True)
+ role = fields.Str(required=True)
+ warehouse = fields.Str(required=True)
+
+
+class SnowflakeParametersType(TypedDict):
+ username: str
+ password: str
+ account: str
+ database: str
+ role: str
+ warehouse: str
+
+
class SnowflakeEngineSpec(PostgresBaseEngineSpec):
engine = "snowflake"
engine_name = "Snowflake"
force_column_alias_quotes = True
max_column_name_length = 256
+ parameters_schema = SnowflakeParametersSchema()
+ default_driver = "snowflake"
+ sqlalchemy_uri_placeholder = "snowflake://"
+
_time_grain_expressions = {
None: "{col}",
"PT1S": "DATE_TRUNC('SECOND', {col})",
@@ -160,3 +186,91 @@ def cancel_query(cls, cursor: Any, query: Query, cancel_query_id: str) -> bool:
return False
return True
+
+ @classmethod
+ def build_sqlalchemy_uri(
+ cls,
+ parameters: SnowflakeParametersType,
+ encrypted_extra: Optional[ # pylint: disable=unused-argument
+ Dict[str, Any]
+ ] = None,
+ ) -> str:
+
+ return str(
+ URL(
+ "snowflake",
+ username=parameters.get("username"),
+ password=parameters.get("password"),
+ host=parameters.get("account"),
+ database=parameters.get("database"),
+ query={
+ "role": parameters.get("role"),
+ "warehouse": parameters.get("warehouse"),
+ },
+ )
+ )
+
+ @classmethod
+ def get_parameters_from_uri(
+ cls,
+ uri: str,
+ encrypted_extra: Optional[ # pylint: disable=unused-argument
+ Dict[str, str]
+ ] = None,
+ ) -> Any:
+ url = make_url(uri)
+ query = dict(url.query.items())
+ return {
+ "username": url.username,
+ "password": url.password,
+ "account": url.host,
+ "database": url.database,
+ "role": query.get("role"),
+ "warehouse": query.get("warehouse"),
+ }
+
+ @classmethod
+ def validate_parameters(
+ cls, parameters: SnowflakeParametersType # pylint: disable=unused-argument
+ ) -> List[SupersetError]:
+ errors: List[SupersetError] = []
+ required = {
+ "warehouse",
+ "username",
+ "database",
+ "account",
+ "role",
+ "password",
+ }
+ present = {key for key in parameters if parameters.get(key, ())}
+ missing = sorted(required - present)
+
+ if missing:
+ errors.append(
+ SupersetError(
+ message=f'One or more parameters are missing: {", ".join(missing)}',
+ error_type=SupersetErrorType.CONNECTION_MISSING_PARAMETERS_ERROR,
+ level=ErrorLevel.WARNING,
+ extra={"missing": missing},
+ ),
+ )
+ 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__]