refactor: Moves CRUD features to src/features (#23482)

This commit is contained in:
Michael S. Molina
2023-04-06 09:23:32 -03:00
committed by GitHub
parent c5eecc7cc2
commit adcb8cf0ac
127 changed files with 300 additions and 285 deletions

View File

@@ -0,0 +1,252 @@
/**
* 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 { AntdSwitch } from 'src/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) => (
<ValidatedInput
id="host"
name="host"
value={db?.parameters?.host}
required={required}
hasTooltip
tooltipText={t(
'This can be either an IP address (e.g. 127.0.0.1) or a domain name (e.g. mydatabase.com).',
)}
validationMethods={{ onBlur: getValidation }}
errorMessage={validationErrors?.host}
placeholder={t('e.g. 127.0.0.1')}
className="form-group-w-50"
label={t('Host')}
onChange={changeMethods.onParametersChange}
/>
);
export const portField = ({
required,
changeMethods,
getValidation,
validationErrors,
db,
}: FieldPropTypes) => (
<>
<ValidatedInput
id="port"
name="port"
type="number"
required={required}
value={db?.parameters?.port as number}
validationMethods={{ onBlur: getValidation }}
errorMessage={validationErrors?.port}
placeholder={t('e.g. 5432')}
className="form-group-w-50"
label={t('Port')}
onChange={changeMethods.onParametersChange}
/>
</>
);
export const httpPath = ({
required,
changeMethods,
getValidation,
validationErrors,
db,
}: FieldPropTypes) => {
const extraJson = JSON.parse(db?.extra || '{}');
return (
<ValidatedInput
id="http_path"
name="http_path"
required={required}
value={extraJson.engine_params?.connect_args?.http_path}
validationMethods={{ onBlur: getValidation }}
errorMessage={validationErrors?.http_path}
placeholder={t('e.g. sql/protocolv1/o/12345')}
label="HTTP Path"
onChange={changeMethods.onExtraInputChange}
helpText={t('Copy the name of the HTTP Path of your cluster.')}
/>
);
};
export const databaseField = ({
required,
changeMethods,
getValidation,
validationErrors,
placeholder,
db,
}: FieldPropTypes) => (
<ValidatedInput
id="database"
name="database"
required={required}
value={db?.parameters?.database}
validationMethods={{ onBlur: getValidation }}
errorMessage={validationErrors?.database}
placeholder={placeholder ?? t('e.g. world_population')}
label={t('Database name')}
onChange={changeMethods.onParametersChange}
helpText={t('Copy the name of the database you are trying to connect to.')}
/>
);
export const usernameField = ({
required,
changeMethods,
getValidation,
validationErrors,
db,
}: FieldPropTypes) => (
<ValidatedInput
id="username"
name="username"
required={required}
value={db?.parameters?.username}
validationMethods={{ onBlur: getValidation }}
errorMessage={validationErrors?.username}
placeholder={t('e.g. Analytics')}
label={t('Username')}
onChange={changeMethods.onParametersChange}
/>
);
export const passwordField = ({
required,
changeMethods,
getValidation,
validationErrors,
db,
isEditMode,
}: FieldPropTypes) => (
<ValidatedInput
id="password"
name="password"
required={required}
visibilityToggle={!isEditMode}
value={db?.parameters?.password}
validationMethods={{ onBlur: getValidation }}
errorMessage={validationErrors?.password}
placeholder={t('e.g. ********')}
label={t('Password')}
onChange={changeMethods.onParametersChange}
/>
);
export const accessTokenField = ({
required,
changeMethods,
getValidation,
validationErrors,
db,
isEditMode,
}: FieldPropTypes) => (
<ValidatedInput
id="access_token"
name="access_token"
required={required}
visibilityToggle={!isEditMode}
value={db?.parameters?.access_token}
validationMethods={{ onBlur: getValidation }}
errorMessage={validationErrors?.access_token}
placeholder={t('e.g. ********')}
label={t('Access token')}
onChange={changeMethods.onParametersChange}
/>
);
export const displayField = ({
changeMethods,
getValidation,
validationErrors,
db,
}: FieldPropTypes) => (
<>
<ValidatedInput
id="database_name"
name="database_name"
required
value={db?.database_name}
validationMethods={{ onBlur: getValidation }}
errorMessage={validationErrors?.database_name}
placeholder=""
label={t('Display Name')}
onChange={changeMethods.onChange}
helpText={t(
'Pick a nickname for how the database will display in Superset.',
)}
/>
</>
);
export const queryField = ({
required,
changeMethods,
getValidation,
validationErrors,
db,
}: FieldPropTypes) => (
<ValidatedInput
id="query_input"
name="query_input"
required={required}
value={db?.query_input || ''}
validationMethods={{ onBlur: getValidation }}
errorMessage={validationErrors?.query}
placeholder={t('e.g. param1=value1&param2=value2')}
label={t('Additional Parameters')}
onChange={changeMethods.onQueryChange}
helpText={t('Add additional custom parameters')}
/>
);
export const forceSSLField = ({
isEditMode,
changeMethods,
db,
sslForced,
}: FieldPropTypes) => (
<div css={(theme: SupersetTheme) => infoTooltip(theme)}>
<AntdSwitch
disabled={sslForced && !isEditMode}
checked={db?.parameters?.encryption || sslForced}
onChange={changed => {
changeMethods.onParametersChange({
target: {
type: 'toggle',
name: 'encryption',
checked: true,
value: changed,
},
});
}}
/>
<span css={toggleStyle}>SSL</span>
<InfoTooltip
tooltip={t('SSL Mode "require" will be used.')}
placement="right"
viewBox="0 -5 24 24"
/>
</div>
);

View File

@@ -0,0 +1,201 @@
/**
* 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 { AntdButton, AntdSelect } from 'src/components';
import InfoTooltip from 'src/components/InfoTooltip';
import FormLabel from 'src/components/Form/FormLabel';
import Icons from 'src/components/Icons';
import { FieldPropTypes } from '.';
import { infoTooltip, labelMarginBottom, 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<number>(
CredentialInfoOptions.jsonUpload.valueOf(),
);
const [fileToUpload, setFileToUpload] = useState<string | null | undefined>(
null,
);
const [isPublic, setIsPublic] = useState<boolean>(true);
const showCredentialsInfo =
db?.engine === 'gsheets' ? !isEditMode && !isPublic : !isEditMode;
const isEncrypted = isEditMode && db?.masked_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 (
<CredentialInfoForm>
{db?.engine === 'gsheets' && (
<div className="catalog-type-select">
<FormLabel
css={(theme: SupersetTheme) => labelMarginBottom(theme)}
required
>
{t('Type of Google Sheets allowed')}
</FormLabel>
<AntdSelect
style={{ width: '100%' }}
defaultValue={isEncrypted ? 'false' : 'true'}
onChange={(value: string) =>
setIsPublic(castStringToBoolean(value))
}
>
<AntdSelect.Option value="true" key={1}>
{t('Publicly shared sheets only')}
</AntdSelect.Option>
<AntdSelect.Option value="false" key={2}>
{t('Public and privately shared sheets')}
</AntdSelect.Option>
</AntdSelect>
</div>
)}
{showCredentialsInfo && (
<>
<FormLabel required>
{t('How do you want to enter service account credentials?')}
</FormLabel>
<AntdSelect
defaultValue={uploadOption}
style={{ width: '100%' }}
onChange={option => setUploadOption(option)}
>
<AntdSelect.Option value={CredentialInfoOptions.jsonUpload}>
{t('Upload JSON file')}
</AntdSelect.Option>
<AntdSelect.Option value={CredentialInfoOptions.copyPaste}>
{t('Copy and Paste JSON credentials')}
</AntdSelect.Option>
</AntdSelect>
</>
)}
{uploadOption === CredentialInfoOptions.copyPaste ||
isEditMode ||
editNewDb ? (
<div className="input-container">
<FormLabel required>{t('Service Account')}</FormLabel>
<textarea
className="input-form"
name={encryptedField}
value={encryptedValue}
onChange={changeMethods.onParametersChange}
placeholder={t(
'Paste content of service credentials JSON file here',
)}
/>
<span className="label-paste">
{t('Copy and paste the entire service account .json file here')}
</span>
</div>
) : (
showCredentialsInfo && (
<div
className="input-container"
css={(theme: SupersetTheme) => infoTooltip(theme)}
>
<div css={{ display: 'flex', alignItems: 'center' }}>
<FormLabel required>{t('Upload Credentials')}</FormLabel>
<InfoTooltip
tooltip={t(
'Use the JSON file you automatically downloaded when creating your service account.',
)}
viewBox="0 0 24 24"
/>
</div>
{!fileToUpload && (
<AntdButton
className="input-upload-btn"
onClick={() =>
document?.getElementById('selectedFile')?.click()
}
>
{t('Choose File')}
</AntdButton>
)}
{fileToUpload && (
<div className="input-upload-current">
{fileToUpload}
<Icons.DeleteFilled
iconSize="m"
onClick={() => {
setFileToUpload(null);
changeMethods.onParametersChange({
target: {
name: encryptedField,
value: '',
},
});
}}
/>
</div>
)}
<input
id="selectedFile"
accept=".json"
className="input-upload"
type="file"
onChange={async event => {
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;
}}
/>
</div>
)
)}
</CredentialInfoForm>
);
};

View File

@@ -0,0 +1,112 @@
/**
* 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 { css, SupersetTheme, t } from '@superset-ui/core';
import ValidatedInput from 'src/components/Form/LabeledErrorBoundInput';
import FormLabel from 'src/components/Form/FormLabel';
import Icons from 'src/components/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 (
<StyledCatalogTable>
<h4 className="gsheet-title">
{t('Connect Google Sheets as tables to this database')}
</h4>
<div>
{tableCatalog?.map((sheet: CatalogObject, idx: number) => (
<>
<FormLabel className="catalog-label" required>
{t('Google Sheet Name and URL')}
</FormLabel>
<div className="catalog-name">
<ValidatedInput
className="catalog-name-input"
required={required}
validationMethods={{ onBlur: getValidation }}
errorMessage={catalogError[idx]?.name}
placeholder={t('Enter a name for this sheet')}
onChange={(e: { target: { value: any } }) => {
changeMethods.onParametersChange({
target: {
type: `catalog-${idx}`,
name: 'name',
value: e.target.value,
},
});
}}
value={sheet.name}
/>
{tableCatalog?.length > 1 && (
<Icons.CloseOutlined
css={(theme: SupersetTheme) => css`
align-self: center;
background: ${theme.colors.grayscale.light4};
margin: 5px 5px 8px 5px;
&.anticon > * {
line-height: 0;
}
`}
iconSize="m"
onClick={() => changeMethods.onRemoveTableCatalog(idx)}
/>
)}
</div>
<ValidatedInput
className="catalog-name-url"
required={required}
validationMethods={{ onBlur: getValidation }}
errorMessage={catalogError[idx]?.url}
placeholder={t('Paste the shareable Google Sheet URL here')}
onChange={(e: { target: { value: any } }) =>
changeMethods.onParametersChange({
target: {
type: `catalog-${idx}`,
name: 'value',
value: e.target.value,
},
})
}
value={sheet.value}
/>
</>
))}
<StyledFooterButton
className="catalog-add-btn"
onClick={() => {
changeMethods.onAddTableCatalog();
}}
>
+ {t('Add sheet')}
</StyledFooterButton>
</div>
</StyledCatalogTable>
);
};

View File

@@ -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: t('e.g. world_population'),
},
warehouse: {
placeholder: t('e.g. compute_wh'),
className: 'form-group-w-50',
},
role: {
placeholder: t('e.g. AccountAdmin'),
className: 'form-group-w-50',
},
};
export const validatedInputField = ({
required,
changeMethods,
getValidation,
validationErrors,
db,
field,
}: FieldPropTypes) => (
<ValidatedInput
id={field}
name={field}
required={required}
value={db?.parameters?.[field]}
validationMethods={{ onBlur: getValidation }}
errorMessage={validationErrors?.[field]}
placeholder={FIELD_TEXT_MAP[field].placeholder}
helpText={FIELD_TEXT_MAP[field].helpText}
label={field}
onChange={changeMethods.onParametersChange}
className={FIELD_TEXT_MAP[field].className || field}
/>
);

View File

@@ -0,0 +1,193 @@
/**
* 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 { Form } from 'src/components/Form';
import {
accessTokenField,
databaseField,
displayField,
forceSSLField,
hostField,
httpPath,
passwordField,
portField,
queryField,
usernameField,
} 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',
'access_token',
'http_path',
'database_name',
'credentials_info',
'service_account_info',
'catalog',
'query',
'encryption',
'account',
'warehouse',
'role',
];
export interface FieldPropTypes {
required: boolean;
hasTooltip?: boolean;
tooltipText?: (value: any) => string;
placeholder?: 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;
} & {
onExtraInputChange: (value: any) => void;
onSSHTunnelParametersChange: (value: any) => string;
};
validationErrors: JsonObject | null;
getValidation: () => void;
db?: DatabaseObject;
field: string;
isEditMode?: boolean;
sslForced?: boolean;
defaultDBName?: string;
editNewDb?: boolean;
}
const FORM_FIELD_MAP = {
host: hostField,
http_path: httpPath,
port: portField,
database: databaseField,
username: usernameField,
password: passwordField,
access_token: accessTokenField,
database_name: displayField,
query: queryField,
encryption: forceSSLField,
credentials_info: EncryptedField,
service_account_info: EncryptedField,
catalog: TableCatalog,
warehouse: validatedInputField,
role: validatedInputField,
account: validatedInputField,
};
interface DatabaseConnectionFormProps {
isEditMode?: boolean;
sslForced: boolean;
editNewDb?: boolean;
dbModel: DatabaseForm;
db: Partial<DatabaseObject> | null;
onParametersChange: (
event: FormEvent<InputProps> | { target: HTMLInputElement },
) => void;
onChange: (
event: FormEvent<InputProps> | { target: HTMLInputElement },
) => void;
onQueryChange: (
event: FormEvent<InputProps> | { target: HTMLInputElement },
) => void;
onParametersUploadFileChange?: (
event: FormEvent<InputProps> | { target: HTMLInputElement },
) => void;
onExtraInputChange: (
event: FormEvent<InputProps> | { target: HTMLInputElement },
) => void;
onAddTableCatalog: () => void;
onRemoveTableCatalog: (idx: number) => void;
validationErrors: JsonObject | null;
getValidation: () => void;
getPlaceholder?: (field: string) => string | undefined;
}
const DatabaseConnectionForm = ({
dbModel: { parameters },
db,
editNewDb,
getPlaceholder,
getValidation,
isEditMode = false,
onAddTableCatalog,
onChange,
onExtraInputChange,
onParametersChange,
onParametersUploadFileChange,
onQueryChange,
onRemoveTableCatalog,
sslForced,
validationErrors,
}: DatabaseConnectionFormProps) => (
<Form>
<div
// @ts-ignore
css={(theme: SupersetTheme) => [
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,
onExtraInputChange,
},
validationErrors,
getValidation,
db,
key: field,
field,
isEditMode,
sslForced,
editNewDb,
placeholder: getPlaceholder ? getPlaceholder(field) : undefined,
}),
)}
</div>
</Form>
);
export const FormFieldMap = FORM_FIELD_MAP;
export default DatabaseConnectionForm;

View File

@@ -0,0 +1,526 @@
/**
* 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, { ChangeEvent, EventHandler } from 'react';
import cx from 'classnames';
import { t, SupersetTheme } from '@superset-ui/core';
import InfoTooltip from 'src/components/InfoTooltip';
import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
import Collapse from 'src/components/Collapse';
import {
StyledInputContainer,
StyledJsonEditor,
StyledExpandableForm,
antdCollapseStyles,
no_margin_bottom,
} from './styles';
import { DatabaseObject, ExtraJson } from '../types';
const ExtraOptions = ({
db,
onInputChange,
onTextChange,
onEditorChange,
onExtraInputChange,
onExtraEditorChange,
}: {
db: DatabaseObject | null;
onInputChange: EventHandler<ChangeEvent<HTMLInputElement>>;
onTextChange: EventHandler<ChangeEvent<HTMLTextAreaElement>>;
onEditorChange: Function;
onExtraInputChange: EventHandler<ChangeEvent<HTMLInputElement>>;
onExtraEditorChange: Function;
}) => {
const expandableModalIsOpen = !!db?.expose_in_sqllab;
const createAsOpen = !!(db?.allow_ctas || db?.allow_cvas);
const isFileUploadSupportedByEngine =
db?.engine_information?.supports_file_upload;
// 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;
});
return (
<Collapse
expandIconPosition="right"
accordion
css={(theme: SupersetTheme) => antdCollapseStyles(theme)}
>
<Collapse.Panel
header={
<div>
<h4>{t('SQL Lab')}</h4>
<p className="helper">
{t('Adjust how this database will interact with SQL Lab.')}
</p>
</div>
}
key="1"
>
<StyledInputContainer css={no_margin_bottom}>
<div className="input-container">
<IndeterminateCheckbox
id="expose_in_sqllab"
indeterminate={false}
checked={!!db?.expose_in_sqllab}
onChange={onInputChange}
labelText={t('Expose database in SQL Lab')}
/>
<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">
<IndeterminateCheckbox
id="allow_ctas"
indeterminate={false}
checked={!!db?.allow_ctas}
onChange={onInputChange}
labelText={t('Allow CREATE TABLE AS')}
/>
<InfoTooltip
tooltip={t('Allow creation of new tables based on queries')}
/>
</div>
</StyledInputContainer>
<StyledInputContainer css={no_margin_bottom}>
<div className="input-container">
<IndeterminateCheckbox
id="allow_cvas"
indeterminate={false}
checked={!!db?.allow_cvas}
onChange={onInputChange}
labelText={t('Allow CREATE VIEW AS')}
/>
<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">
<IndeterminateCheckbox
id="allow_dml"
indeterminate={false}
checked={!!db?.allow_dml}
onChange={onInputChange}
labelText={t('Allow DML')}
/>
<InfoTooltip
tooltip={t(
'Allow manipulation of the database using non-SELECT statements such as UPDATE, DELETE, CREATE, etc.',
)}
/>
</div>
</StyledInputContainer>
<StyledInputContainer css={no_margin_bottom}>
<div className="input-container">
<IndeterminateCheckbox
id="cost_estimate_enabled"
indeterminate={false}
checked={!!extraJson?.cost_estimate_enabled}
onChange={onExtraInputChange}
labelText={t('Enable query cost estimation')}
/>
<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">
<IndeterminateCheckbox
id="allows_virtual_table_explore"
indeterminate={false}
checked={!!extraJson?.allows_virtual_table_explore}
onChange={onExtraInputChange}
labelText={t('Allow this database to be explored')}
/>
<InfoTooltip
tooltip={t(
'When enabled, users are able to visualize SQL Lab results in Explore.',
)}
/>
</div>
</StyledInputContainer>
<StyledInputContainer>
<div className="input-container">
<IndeterminateCheckbox
id="disable_data_preview"
indeterminate={false}
checked={!!extraJson?.disable_data_preview}
onChange={onExtraInputChange}
labelText={t('Disable SQL Lab data preview queries')}
/>
<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>
</StyledExpandableForm>
</StyledInputContainer>
</Collapse.Panel>
<Collapse.Panel
header={
<div>
<h4>{t('Performance')}</h4>
<p className="helper">
{t('Adjust performance settings of this database.')}
</p>
</div>
}
key="2"
>
<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}
/>
</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">
<IndeterminateCheckbox
id="allow_run_async"
indeterminate={false}
checked={!!db?.allow_run_async}
onChange={onInputChange}
labelText={t('Asynchronous query execution')}
/>
<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.',
)}
/>
</div>
</StyledInputContainer>
<StyledInputContainer css={{ no_margin_bottom }}>
<div className="input-container">
<IndeterminateCheckbox
id="cancel_query_on_windows_unload"
indeterminate={false}
checked={!!extraJson?.cancel_query_on_windows_unload}
onChange={onExtraInputChange}
labelText={t('Cancel query on window unload event')}
/>
<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>
</Collapse.Panel>
<Collapse.Panel
header={
<div>
<h4>{t('Security')}</h4>
<p className="helper">{t('Add extra connection information.')}</p>
</div>
}
key="3"
>
<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"
/>
</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">
<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>
<StyledInputContainer
css={!isFileUploadSupportedByEngine ? no_margin_bottom : {}}
>
<div className="input-container">
<IndeterminateCheckbox
id="impersonate_user"
indeterminate={false}
checked={!!db?.impersonate_user}
onChange={onInputChange}
labelText={t(
'Impersonate logged in user (Presto, Trino, Drill, Hive, and GSheets)',
)}
/>
<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">
<IndeterminateCheckbox
id="allow_file_upload"
indeterminate={false}
checked={!!db?.allow_file_upload}
onChange={onInputChange}
labelText={t('Allow file uploads to database')}
/>
</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>
)}
</Collapse.Panel>
<Collapse.Panel
header={
<div>
<h4>{t('Other')}</h4>
<p className="helper">{t('Additional settings.')}</p>
</div>
}
key="4"
>
<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"
defaultValue={
!Object.keys(extraJson?.metadata_params || {}).length
? ''
: extraJson?.metadata_params
}
/>
</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"
defaultValue={
!Object.keys(extraJson?.engine_params || {}).length
? ''
: extraJson?.engine_params
}
/>
</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="number"
name="version"
placeholder={t('Version number')}
onChange={onExtraInputChange}
value={extraJson?.version || ''}
/>
</div>
<div className="helper">
{t(
'Specify the database version. This should be used with ' +
'Presto in order to enable query cost estimation.',
)}
</div>
</StyledInputContainer>
</Collapse.Panel>
</Collapse>
);
};
export default ExtraOptions;

View File

@@ -0,0 +1,203 @@
/**
* 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 { getDatabaseDocumentationLinks } from 'src/views/CRUD/hooks';
import { UploadFile } from 'antd/lib/upload/interface';
import { t } from '@superset-ui/core';
import {
EditHeaderTitle,
EditHeaderSubtitle,
StyledFormHeader,
StyledStickyHeader,
} from './styles';
import { DatabaseForm, DatabaseObject } from '../types';
const supersetTextDocs = getDatabaseDocumentationLinks();
export const DOCUMENTATION_LINK = supersetTextDocs
? supersetTextDocs.support
: 'https://superset.apache.org/docs/databases/installing-database-drivers';
const irregularDocumentationLinks = {
postgresql: 'https://superset.apache.org/docs/databases/postgres',
mssql: 'https://superset.apache.org/docs/databases/sql-server',
gsheets: 'https://superset.apache.org/docs/databases/google-sheets',
};
const documentationLink = (engine: string | undefined) => {
if (!engine) return null;
if (supersetTextDocs) {
// override doc link for superset_txt yml
return supersetTextDocs[engine] || supersetTextDocs.default;
}
if (!irregularDocumentationLinks[engine]) {
return `https://superset.apache.org/docs/databases/${engine}`;
}
return irregularDocumentationLinks[engine];
};
const ModalHeader = ({
isLoading,
isEditMode,
useSqlAlchemyForm,
hasConnectedDb,
db,
dbName,
dbModel,
editNewDb,
fileList,
}: {
isLoading: boolean;
isEditMode: boolean;
useSqlAlchemyForm: boolean;
hasConnectedDb: boolean;
db: Partial<DatabaseObject> | null;
dbName: string;
dbModel: DatabaseForm;
editNewDb?: boolean;
fileList?: UploadFile[];
passwordFields?: string[];
needsOverwriteConfirm?: boolean;
}) => {
const fileCheck = fileList && fileList?.length > 0;
const isEditHeader = (
<StyledFormHeader>
<EditHeaderTitle>{db?.backend}</EditHeaderTitle>
<EditHeaderSubtitle>{dbName}</EditHeaderSubtitle>
</StyledFormHeader>
);
const useSqlAlchemyFormHeader = (
<StyledFormHeader>
<p className="helper-top">
{t('STEP %(stepCurr)s OF %(stepLast)s', {
stepCurr: 2,
stepLast: 2,
})}
</p>
<h4>{t('Enter Primary Credentials')}</h4>
<p className="helper-bottom">
{t('Need help? Learn how to connect your database')}{' '}
<a
href={supersetTextDocs?.default || DOCUMENTATION_LINK}
target="_blank"
rel="noopener noreferrer"
>
{t('here')}
</a>
.
</p>
</StyledFormHeader>
);
const hasConnectedDbHeader = (
<StyledStickyHeader>
<StyledFormHeader>
<p className="helper-top">
{t('STEP %(stepCurr)s OF %(stepLast)s', {
stepCurr: 3,
stepLast: 3,
})}
</p>
<h4 className="step-3-text">{t('Database connected')}</h4>
<p className="subheader-text">
{t(`Create a dataset to begin visualizing your data as a chart or go to
SQL Lab to query your data.`)}
</p>
</StyledFormHeader>
</StyledStickyHeader>
);
const hasDbHeader = (
<StyledStickyHeader>
<StyledFormHeader>
<p className="helper-top">
{t('STEP %(stepCurr)s OF %(stepLast)s', {
stepCurr: 2,
stepLast: 3,
})}
</p>
<h4>
{t('Enter the required %(dbModelName)s credentials', {
dbModelName: dbModel.name,
})}
</h4>
<p className="helper-bottom">
{t('Need help? Learn more about')}{' '}
<a
href={documentationLink(db?.engine)}
target="_blank"
rel="noopener noreferrer"
>
{t('connecting to %(dbModelName)s.', { dbModelName: dbModel.name })}
.
</a>
</p>
</StyledFormHeader>
</StyledStickyHeader>
);
const noDbHeader = (
<StyledFormHeader>
<div className="select-db">
<p className="helper-top">
{t('STEP %(stepCurr)s OF %(stepLast)s', {
stepCurr: 1,
stepLast: 3,
})}
</p>
<h4>{t('Select a database to connect')}</h4>
</div>
</StyledFormHeader>
);
const importDbHeader = (
<StyledStickyHeader>
<StyledFormHeader>
<p className="helper-top">
{t('STEP %(stepCurr)s OF %(stepLast)s', {
stepCurr: 2,
stepLast: 2,
})}
</p>
<h4>
{t('Enter the required %(dbModelName)s credentials', {
dbModelName: dbModel.name,
})}
</h4>
<p className="helper-bottom">{fileCheck ? fileList[0].name : ''}</p>
</StyledFormHeader>
</StyledStickyHeader>
);
if (fileCheck) return importDbHeader;
if (isLoading) return <></>;
if (isEditMode) return isEditHeader;
if (useSqlAlchemyForm) return useSqlAlchemyFormHeader;
if (hasConnectedDb && !editNewDb) return hasConnectedDbHeader;
if (db || editNewDb) return hasDbHeader;
return noDbHeader;
};
export default ModalHeader;

View File

@@ -0,0 +1,227 @@
/**
* 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, { EventHandler, ChangeEvent, useState } from 'react';
import { t, styled } from '@superset-ui/core';
import { AntdForm, Col, Row } from 'src/components';
import { Form, FormLabel } from 'src/components/Form';
import { Radio } from 'src/components/Radio';
import { Input, TextArea } from 'src/components/Input';
import { Input as AntdInput, Tooltip } from 'antd';
import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons';
import { DatabaseObject } from '../types';
import { AuthType } from '.';
const StyledDiv = styled.div`
padding-top: ${({ theme }) => theme.gridUnit * 2}px;
label {
color: ${({ theme }) => theme.colors.grayscale.base};
text-transform: uppercase;
margin-bottom: ${({ theme }) => theme.gridUnit * 2}px;
}
`;
const StyledRow = styled(Row)`
padding-bottom: ${({ theme }) => theme.gridUnit * 2}px;
`;
const StyledFormItem = styled(AntdForm.Item)`
margin-bottom: 0 !important;
`;
const StyledInputPassword = styled(AntdInput.Password)`
margin: ${({ theme }) => `${theme.gridUnit}px 0 ${theme.gridUnit * 2}px`};
`;
const SSHTunnelForm = ({
db,
onSSHTunnelParametersChange,
setSSHTunnelLoginMethod,
}: {
db: DatabaseObject | null;
onSSHTunnelParametersChange: EventHandler<
ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
>;
setSSHTunnelLoginMethod: (method: AuthType) => void;
}) => {
const [usePassword, setUsePassword] = useState<AuthType>(AuthType.password);
return (
<Form>
<StyledRow gutter={16}>
<Col xs={24} md={12}>
<StyledDiv>
<FormLabel htmlFor="server_address" required>
{t('SSH Host')}
</FormLabel>
<Input
name="server_address"
type="text"
placeholder={t('e.g. 127.0.0.1')}
value={db?.ssh_tunnel?.server_address || ''}
onChange={onSSHTunnelParametersChange}
data-test="ssh-tunnel-server_address-input"
/>
</StyledDiv>
</Col>
<Col xs={24} md={12}>
<StyledDiv>
<FormLabel htmlFor="server_port" required>
{t('SSH Port')}
</FormLabel>
<Input
name="server_port"
type="text"
placeholder={t('22')}
value={db?.ssh_tunnel?.server_port || ''}
onChange={onSSHTunnelParametersChange}
data-test="ssh-tunnel-server_port-input"
/>
</StyledDiv>
</Col>
</StyledRow>
<StyledRow gutter={16}>
<Col xs={24}>
<StyledDiv>
<FormLabel htmlFor="username" required>
{t('Username')}
</FormLabel>
<Input
name="username"
type="text"
placeholder={t('e.g. Analytics')}
value={db?.ssh_tunnel?.username || ''}
onChange={onSSHTunnelParametersChange}
data-test="ssh-tunnel-username-input"
/>
</StyledDiv>
</Col>
</StyledRow>
<StyledRow gutter={16}>
<Col xs={24}>
<StyledDiv>
<FormLabel htmlFor="use_password" required>
{t('Login with')}
</FormLabel>
<StyledFormItem name="use_password" initialValue={usePassword}>
<Radio.Group
onChange={({ target: { value } }) => {
setUsePassword(value);
setSSHTunnelLoginMethod(value);
}}
>
<Radio
value={AuthType.password}
data-test="ssh-tunnel-use_password-radio"
>
{t('Password')}
</Radio>
<Radio
value={AuthType.privateKey}
data-test="ssh-tunnel-use_private_key-radio"
>
{t('Private Key & Password')}
</Radio>
</Radio.Group>
</StyledFormItem>
</StyledDiv>
</Col>
</StyledRow>
{usePassword === AuthType.password && (
<StyledRow gutter={16}>
<Col xs={24}>
<StyledDiv>
<FormLabel htmlFor="password" required>
{t('SSH Password')}
</FormLabel>
<StyledInputPassword
name="password"
placeholder={t('e.g. ********')}
value={db?.ssh_tunnel?.password || ''}
onChange={onSSHTunnelParametersChange}
data-test="ssh-tunnel-password-input"
iconRender={visible =>
visible ? (
<Tooltip title="Hide password.">
<EyeInvisibleOutlined />
</Tooltip>
) : (
<Tooltip title="Show password.">
<EyeOutlined />
</Tooltip>
)
}
role="textbox"
/>
</StyledDiv>
</Col>
</StyledRow>
)}
{usePassword === AuthType.privateKey && (
<>
<StyledRow gutter={16}>
<Col xs={24}>
<StyledDiv>
<FormLabel htmlFor="private_key" required>
{t('Private Key')}
</FormLabel>
<TextArea
name="private_key"
placeholder={t('Paste Private Key here')}
value={db?.ssh_tunnel?.private_key || ''}
onChange={onSSHTunnelParametersChange}
data-test="ssh-tunnel-private_key-input"
rows={4}
/>
</StyledDiv>
</Col>
</StyledRow>
<StyledRow gutter={16}>
<Col xs={24}>
<StyledDiv>
<FormLabel htmlFor="private_key_password" required>
{t('Private Key Password')}
</FormLabel>
<StyledInputPassword
name="private_key_password"
placeholder={t('e.g. ********')}
value={db?.ssh_tunnel?.private_key_password || ''}
onChange={onSSHTunnelParametersChange}
data-test="ssh-tunnel-private_key_password-input"
iconRender={visible =>
visible ? (
<Tooltip title="Hide password.">
<EyeInvisibleOutlined />
</Tooltip>
) : (
<Tooltip title="Show password.">
<EyeOutlined />
</Tooltip>
)
}
role="textbox"
/>
</StyledDiv>
</Col>
</StyledRow>
</>
)}
</Form>
);
};
export default SSHTunnelForm;

View File

@@ -0,0 +1,58 @@
/**
* 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, SupersetTheme, SwitchProps } from '@superset-ui/core';
import { AntdSwitch } from 'src/components';
import InfoTooltip from 'src/components/InfoTooltip';
import { isEmpty } from 'lodash';
import { ActionType } from '.';
import { infoTooltip, toggleStyle } from './styles';
const SSHTunnelSwitch = ({
isEditMode,
dbFetched,
useSSHTunneling,
setUseSSHTunneling,
setDB,
isSSHTunneling,
}: SwitchProps) =>
isSSHTunneling ? (
<div css={(theme: SupersetTheme) => infoTooltip(theme)}>
<AntdSwitch
disabled={isEditMode && !isEmpty(dbFetched?.ssh_tunnel)}
checked={useSSHTunneling}
onChange={changed => {
setUseSSHTunneling(changed);
if (!changed) {
setDB({
type: ActionType.removeSSHTunnelConfig,
});
}
}}
data-test="ssh-tunnel-switch"
/>
<span css={toggleStyle}>{t('SSH Tunnel')}</span>
<InfoTooltip
tooltip={t('SSH Tunnel configuration parameters')}
placement="right"
viewBox="0 -5 24 24"
/>
</div>
) : null;
export default SSHTunnelSwitch;

View File

@@ -0,0 +1,116 @@
/**
* 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, { EventHandler, ChangeEvent, MouseEvent, ReactNode } from 'react';
import { t, SupersetTheme } from '@superset-ui/core';
import SupersetText from 'src/utils/textUtils';
import Button from 'src/components/Button';
import { StyledInputContainer, wideButton } from './styles';
import { DatabaseObject } from '../types';
const SqlAlchemyTab = ({
db,
onInputChange,
testConnection,
conf,
testInProgress = false,
children,
}: {
db: DatabaseObject | null;
onInputChange: EventHandler<ChangeEvent<HTMLInputElement>>;
testConnection: EventHandler<MouseEvent<HTMLElement>>;
conf: { SQLALCHEMY_DOCS_URL: string; SQLALCHEMY_DISPLAY_TEXT: string };
testInProgress?: boolean;
children?: ReactNode;
}) => {
let fallbackDocsUrl;
let fallbackDisplayText;
if (SupersetText) {
fallbackDocsUrl =
SupersetText.DB_MODAL_SQLALCHEMY_FORM?.SQLALCHEMY_DOCS_URL;
fallbackDisplayText =
SupersetText.DB_MODAL_SQLALCHEMY_FORM?.SQLALCHEMY_DISPLAY_TEXT;
} else {
fallbackDocsUrl = 'https://docs.sqlalchemy.org/en/13/core/engines.html';
fallbackDisplayText = 'SQLAlchemy docs';
}
return (
<>
<StyledInputContainer>
<div className="control-label">
{t('Display Name')}
<span className="required">*</span>
</div>
<div className="input-container">
<input
type="text"
name="database_name"
data-test="database-name-input"
value={db?.database_name || ''}
placeholder={t('Name your database')}
onChange={onInputChange}
/>
</div>
<div className="helper">
{t('Pick a name to help you identify this database.')}
</div>
</StyledInputContainer>
<StyledInputContainer>
<div className="control-label">
{t('SQLAlchemy URI')}
<span className="required">*</span>
</div>
<div className="input-container">
<input
type="text"
name="sqlalchemy_uri"
data-test="sqlalchemy-uri-input"
value={db?.sqlalchemy_uri || ''}
autoComplete="off"
placeholder={t(
'dialect+driver://username:password@host:port/database',
)}
onChange={onInputChange}
/>
</div>
<div className="helper">
{t('Refer to the')}{' '}
<a
href={fallbackDocsUrl || conf?.SQLALCHEMY_DOCS_URL || ''}
target="_blank"
rel="noopener noreferrer"
>
{fallbackDisplayText || conf?.SQLALCHEMY_DISPLAY_TEXT || ''}
</a>{' '}
{t('for more information on how to structure your URI.')}
</div>
</StyledInputContainer>
{children}
<Button
onClick={testConnection}
loading={testInProgress}
cta
buttonStyle="link"
css={(theme: SupersetTheme) => wideButton(theme)}
>
{t('Test connection')}
</Button>
</>
);
};
export default SqlAlchemyTab;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,621 @@
/**
* 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 { css, styled, SupersetTheme } from '@superset-ui/core';
import { JsonEditor } from 'src/components/AsyncAceEditor';
import Button from 'src/components/Button';
const CTAS_CVAS_SCHEMA_FORM_HEIGHT = 108;
const EXPOSE_IN_SQLLAB_FORM_HEIGHT = CTAS_CVAS_SCHEMA_FORM_HEIGHT + 153;
const EXPOSE_ALL_FORM_HEIGHT = EXPOSE_IN_SQLLAB_FORM_HEIGHT + 102;
const MODAL_BODY_HEIGHT = 180.5;
const anticonHeight = 12;
export const no_margin_bottom = css`
margin-bottom: 0;
`;
export const labelMarginBottom = (theme: SupersetTheme) => css`
margin-bottom: ${theme.gridUnit * 2}px;
`;
export const marginBottom = (theme: SupersetTheme) => css`
margin-bottom: ${theme.gridUnit * 4}px;
`;
export const StyledFormHeader = styled.header`
padding: ${({ theme }) => theme.gridUnit * 2}px
${({ theme }) => theme.gridUnit * 4}px;
line-height: ${({ theme }) => theme.gridUnit * 6}px;
.helper-top {
padding-bottom: 0;
color: ${({ theme }) => theme.colors.grayscale.base};
font-size: ${({ theme }) => theme.typography.sizes.s}px;
margin: 0;
}
.subheader-text {
line-height: ${({ theme }) => theme.gridUnit * 4.25}px;
}
.helper-bottom {
padding-top: 0;
color: ${({ theme }) => theme.colors.grayscale.base};
font-size: ${({ theme }) => theme.typography.sizes.s}px;
margin: 0;
}
h4 {
color: ${({ theme }) => theme.colors.grayscale.dark2};
font-size: ${({ theme }) => theme.typography.sizes.l}px;
margin: 0;
padding: 0;
line-height: ${({ theme }) => theme.gridUnit * 8}px;
}
.select-db {
padding-bottom: ${({ theme }) => theme.gridUnit * 2}px;
.helper {
margin: 0;
}
h4 {
margin: 0 0 ${({ theme }) => theme.gridUnit * 4}px;
}
}
`;
export const antdCollapseStyles = (theme: SupersetTheme) => css`
.ant-collapse-header {
padding-top: ${theme.gridUnit * 3.5}px;
padding-bottom: ${theme.gridUnit * 2.5}px;
.anticon.ant-collapse-arrow {
top: calc(50% - ${anticonHeight / 2}px);
}
.helper {
color: ${theme.colors.grayscale.base};
}
}
h4 {
font-size: 16px;
margin-top: 0;
margin-bottom: ${theme.gridUnit}px;
}
p.helper {
margin-bottom: 0;
padding: 0;
}
`;
export const antDTabsStyles = css`
.ant-tabs-top {
margin-top: 0;
}
.ant-tabs-top > .ant-tabs-nav {
margin-bottom: 0;
}
.ant-tabs-tab {
margin-right: 0;
}
`;
export const antDModalNoPaddingStyles = css`
.ant-modal-body {
padding-left: 0;
padding-right: 0;
padding-top: 0;
}
`;
export const infoTooltip = (theme: SupersetTheme) => css`
margin-bottom: ${theme.gridUnit * 5}px;
svg {
margin-bottom: ${theme.gridUnit * 0.25}px;
}
`;
export const toggleStyle = (theme: SupersetTheme) => css`
padding-left: ${theme.gridUnit * 2}px;
`;
export const formScrollableStyles = (theme: SupersetTheme) => css`
padding: ${theme.gridUnit * 4}px ${theme.gridUnit * 4}px 0;
`;
export const antDModalStyles = (theme: SupersetTheme) => css`
.ant-select-dropdown {
height: ${theme.gridUnit * 40}px;
}
.ant-modal-header {
padding: ${theme.gridUnit * 4.5}px ${theme.gridUnit * 4}px
${theme.gridUnit * 4}px;
}
.ant-modal-close-x .close {
color: ${theme.colors.grayscale.dark1};
opacity: 1;
}
.ant-modal-body {
height: ${theme.gridUnit * MODAL_BODY_HEIGHT}px;
}
.ant-modal-footer {
height: ${theme.gridUnit * 16.25}px;
}
`;
export const antDAlertStyles = (theme: SupersetTheme) => css`
border: 1px solid ${theme.colors.info.base};
padding: ${theme.gridUnit * 4}px;
margin: ${theme.gridUnit * 4}px 0;
.ant-alert-message {
color: ${theme.colors.info.dark2};
font-size: ${theme.typography.sizes.m}px;
font-weight: ${theme.typography.weights.bold};
}
.ant-alert-description {
color: ${theme.colors.info.dark2};
font-size: ${theme.typography.sizes.m}px;
line-height: ${theme.gridUnit * 5}px;
a {
text-decoration: underline;
}
.ant-alert-icon {
margin-right: ${theme.gridUnit * 2.5}px;
font-size: ${theme.typography.sizes.l}px;
position: relative;
top: ${theme.gridUnit / 4}px;
}
}
`;
export const StyledAlertMargin = styled.div`
${({ theme }) => css`
margin: 0 ${theme.gridUnit * 4}px -${theme.gridUnit * 4}px;
`}
`;
export const antDErrorAlertStyles = (theme: SupersetTheme) => css`
border: ${theme.colors.error.base} 1px solid;
padding: ${theme.gridUnit * 4}px;
margin: ${theme.gridUnit * 8}px ${theme.gridUnit * 4}px;
color: ${theme.colors.error.dark2};
.ant-alert-message {
font-size: ${theme.typography.sizes.m}px;
font-weight: ${theme.typography.weights.bold};
}
.ant-alert-description {
font-size: ${theme.typography.sizes.m}px;
line-height: ${theme.gridUnit * 5}px;
.ant-alert-icon {
margin-right: ${theme.gridUnit * 2.5}px;
font-size: ${theme.typography.sizes.l}px;
position: relative;
top: ${theme.gridUnit / 4}px;
}
}
`;
export const antdWarningAlertStyles = (theme: SupersetTheme) => css`
border: 1px solid ${theme.colors.warning.light1};
padding: ${theme.gridUnit * 4}px;
margin: ${theme.gridUnit * 4}px 0;
color: ${theme.colors.warning.dark2};
.ant-alert-message {
margin: 0;
}
.ant-alert-description {
font-size: ${theme.typography.sizes.s + 1}px;
line-height: ${theme.gridUnit * 4}px;
.ant-alert-icon {
margin-right: ${theme.gridUnit * 2.5}px;
font-size: ${theme.typography.sizes.l + 1}px;
position: relative;
top: ${theme.gridUnit / 4}px;
}
}
`;
export const formHelperStyles = (theme: SupersetTheme) => css`
.required {
margin-left: ${theme.gridUnit / 2}px;
color: ${theme.colors.error.base};
}
.helper {
display: block;
padding: ${theme.gridUnit}px 0;
color: ${theme.colors.grayscale.light1};
font-size: ${theme.typography.sizes.s}px;
text-align: left;
}
`;
export const wideButton = (theme: SupersetTheme) => css`
width: 100%;
border: 1px solid ${theme.colors.primary.dark2};
color: ${theme.colors.primary.dark2};
&:hover,
&:focus {
border: 1px solid ${theme.colors.primary.dark1};
color: ${theme.colors.primary.dark1};
}
`;
export const formStyles = (theme: SupersetTheme) => css`
.form-group {
margin-bottom: ${theme.gridUnit * 4}px;
&-w-50 {
display: inline-block;
width: ${`calc(50% - ${theme.gridUnit * 4}px)`};
& + .form-group-w-50 {
margin-left: ${theme.gridUnit * 8}px;
}
}
}
.control-label {
color: ${theme.colors.grayscale.dark1};
font-size: ${theme.typography.sizes.s}px;
}
.helper {
color: ${theme.colors.grayscale.light1};
font-size: ${theme.typography.sizes.s}px;
margin-top: ${theme.gridUnit * 1.5}px;
}
.ant-tabs-content-holder {
overflow: auto;
max-height: 480px;
}
`;
export const validatedFormStyles = (theme: SupersetTheme) => css`
label {
color: ${theme.colors.grayscale.dark1};
font-size: ${theme.typography.sizes.s}px;
margin-bottom: 0;
}
`;
export const StyledInputContainer = styled.div`
${({ theme }) => css`
margin-bottom: ${theme.gridUnit * 6}px;
&.mb-0 {
margin-bottom: 0;
}
&.mb-8 {
margin-bottom: ${theme.gridUnit * 2}px;
}
.control-label {
color: ${theme.colors.grayscale.dark1};
font-size: ${theme.typography.sizes.s}px;
margin-bottom: ${theme.gridUnit * 2}px;
}
&.extra-container {
padding-top: ${theme.gridUnit * 2}px;
}
.input-container {
display: flex;
align-items: top;
label {
display: flex;
margin-left: ${theme.gridUnit * 2}px;
margin-top: ${theme.gridUnit * 0.75}px;
font-family: ${theme.typography.families.sansSerif};
font-size: ${theme.typography.sizes.m}px;
}
i {
margin: 0 ${theme.gridUnit}px;
}
}
input,
textarea {
flex: 1 1 auto;
}
textarea {
height: 160px;
resize: none;
}
input::placeholder,
textarea::placeholder {
color: ${theme.colors.grayscale.light1};
}
textarea,
input[type='text'],
input[type='number'] {
padding: ${theme.gridUnit * 1.5}px ${theme.gridUnit * 2}px;
border-style: none;
border: 1px solid ${theme.colors.grayscale.light2};
border-radius: ${theme.gridUnit}px;
&[name='name'] {
flex: 0 1 auto;
width: 40%;
}
}
&.expandable {
height: 0;
overflow: hidden;
transition: height 0.25s;
margin-left: ${theme.gridUnit * 8}px;
margin-bottom: 0;
padding: 0;
.control-label {
margin-bottom: 0;
}
&.open {
height: ${CTAS_CVAS_SCHEMA_FORM_HEIGHT}px;
padding-right: ${theme.gridUnit * 5}px;
}
}
`}
`;
export const StyledJsonEditor = styled(JsonEditor)`
flex: 1 1 auto;
border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
border-radius: ${({ theme }) => theme.gridUnit}px;
`;
export const StyledExpandableForm = styled.div`
padding-top: ${({ theme }) => theme.gridUnit}px;
.input-container {
padding-top: ${({ theme }) => theme.gridUnit}px;
padding-bottom: ${({ theme }) => theme.gridUnit}px;
}
&.expandable {
height: 0;
overflow: hidden;
transition: height 0.25s;
margin-left: ${({ theme }) => theme.gridUnit * 7}px;
&.open {
height: ${EXPOSE_IN_SQLLAB_FORM_HEIGHT}px;
&.ctas-open {
height: ${EXPOSE_ALL_FORM_HEIGHT}px;
}
}
}
`;
export const StyledAlignment = styled.div`
padding: 0 ${({ theme }) => theme.gridUnit * 4}px;
margin-top: ${({ theme }) => theme.gridUnit * 6}px;
`;
export const buttonLinkStyles = (theme: SupersetTheme) => css`
font-weight: ${theme.typography.weights.normal};
text-transform: initial;
padding-right: ${theme.gridUnit * 2}px;
`;
export const importDbButtonLinkStyles = (theme: SupersetTheme) => css`
font-size: ${theme.gridUnit * 3.5}px;
font-weight: ${theme.typography.weights.normal};
text-transform: initial;
padding-right: ${theme.gridUnit * 2}px;
`;
export const alchemyButtonLinkStyles = (theme: SupersetTheme) => css`
font-weight: ${theme.typography.weights.normal};
text-transform: initial;
padding: ${theme.gridUnit * 8}px 0 0;
margin-left: 0px;
`;
export const TabHeader = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
padding: 0px;
.helper {
color: ${({ theme }) => theme.colors.grayscale.base};
font-size: ${({ theme }) => theme.typography.sizes.s}px;
margin: 0px;
}
`;
export const CreateHeaderTitle = styled.div`
color: ${({ theme }) => theme.colors.grayscale.dark2};
font-weight: ${({ theme }) => theme.typography.weights.bold};
font-size: ${({ theme }) => theme.typography.sizes.m}px;
`;
export const CreateHeaderSubtitle = styled.div`
color: ${({ theme }) => theme.colors.grayscale.dark1};
font-size: ${({ theme }) => theme.typography.sizes.s}px;
`;
export const EditHeaderTitle = styled.div`
color: ${({ theme }) => theme.colors.grayscale.light1};
font-size: ${({ theme }) => theme.typography.sizes.s}px;
text-transform: uppercase;
`;
export const EditHeaderSubtitle = styled.div`
color: ${({ theme }) => theme.colors.grayscale.dark1};
font-size: ${({ theme }) => theme.typography.sizes.l}px;
font-weight: ${({ theme }) => theme.typography.weights.bold};
`;
export const CredentialInfoForm = styled.div`
.catalog-type-select {
margin: 0 0 20px;
}
.label-select {
text-transform: uppercase;
color: ${({ theme }) => theme.colors.grayscale.dark1};
font-size: 11px;
margin: 0 5px ${({ theme }) => theme.gridUnit * 2}px;
}
.label-paste {
color: ${({ theme }) => theme.colors.grayscale.light1};
font-size: 11px;
line-height: 16px;
}
.input-container {
margin: ${({ theme }) => theme.gridUnit * 7}px 0;
display: flex;
flex-direction: column;
}
}
.input-form {
height: 100px;
width: 100%;
border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
border-radius: ${({ theme }) => theme.gridUnit}px;
resize: vertical;
padding: ${({ theme }) => theme.gridUnit * 1.5}px
${({ theme }) => theme.gridUnit * 2}px;
&::placeholder {
color: ${({ theme }) => theme.colors.grayscale.light1};
}
}
.input-container {
.input-upload {
display: none !important;
}
.input-upload-current {
display: flex;
justify-content: space-between;
}
.input-upload-btn {
width: ${({ theme }) => theme.gridUnit * 32}px
}
}`;
export const SelectDatabaseStyles = styled.div`
.preferred {
.superset-button {
margin-left: 0;
}
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin: ${({ theme }) => theme.gridUnit * 4}px;
}
.preferred-item {
width: 32%;
margin-bottom: ${({ theme }) => theme.gridUnit * 2.5}px;
}
.available {
margin: ${({ theme }) => theme.gridUnit * 4}px;
.available-label {
font-size: ${({ theme }) => theme.typography.sizes.l}px;
font-weight: ${({ theme }) => theme.typography.weights.bold};
margin: ${({ theme }) => theme.gridUnit * 6}px 0;
}
.available-select {
width: 100%;
}
}
.label-available-select {
text-transform: uppercase;
font-size: ${({ theme }) => theme.typography.sizes.s}px;
}
.control-label {
color: ${({ theme }) => theme.colors.grayscale.dark1};
font-size: ${({ theme }) => theme.typography.sizes.s}px;
margin-bottom: ${({ theme }) => theme.gridUnit * 2}px;
}
`;
export const StyledFooterButton = styled(Button)`
width: ${({ theme }) => theme.gridUnit * 40}px;
`;
export const StyledStickyHeader = styled.div`
position: sticky;
top: 0;
z-index: ${({ theme }) => theme.zIndex.max};
background: ${({ theme }) => theme.colors.grayscale.light5};
`;
export const StyledCatalogTable = styled.div`
margin-bottom: 16px;
.catalog-type-select {
margin: 0 0 20px;
}
.gsheet-title {
font-size: ${({ theme }) => theme.typography.sizes.l}px;
font-weight: ${({ theme }) => theme.typography.weights.bold};
margin: ${({ theme }) => theme.gridUnit * 10}px 0 16px;
}
.catalog-label {
margin: 0 0 7px;
}
.catalog-name {
display: flex;
.catalog-name-input {
width: 95%;
margin-bottom: 0px;
}
}
.catalog-name-url {
margin: 4px 0;
width: 95%;
}
.catalog-add-btn {
width: 95%;
}
`;
export const StyledUploadWrapper = styled.div`
.ant-progress-inner {
display: none;
}
.ant-upload-list-item-card-actions {
display: none;
}
`;