mirror of
https://github.com/apache/superset.git
synced 2026-06-01 05:39:17 +00:00
refactor: Moves CRUD features to src/features (#23482)
This commit is contained in:
committed by
GitHub
parent
c5eecc7cc2
commit
adcb8cf0ac
@@ -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¶m2=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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
1974
superset-frontend/src/features/databases/DatabaseModal/index.tsx
Normal file
1974
superset-frontend/src/features/databases/DatabaseModal/index.tsx
Normal file
File diff suppressed because it is too large
Load Diff
621
superset-frontend/src/features/databases/DatabaseModal/styles.ts
Normal file
621
superset-frontend/src/features/databases/DatabaseModal/styles.ts
Normal 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;
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user