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;