mirror of
https://github.com/apache/superset.git
synced 2026-04-22 17:45:21 +00:00
fix(databases): GSheets and Clickhouse DBs are not allowed to upload files (#21065)
This commit is contained in:
committed by
GitHub
parent
82bd5a31b8
commit
b36bd3f083
13
docs/static/resources/openapi.json
vendored
13
docs/static/resources/openapi.json
vendored
@@ -3557,6 +3557,9 @@
|
|||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"engine_information": {
|
||||||
|
"type": "object"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
@@ -3738,6 +3741,9 @@
|
|||||||
"sqlalchemy_uri": {
|
"sqlalchemy_uri": {
|
||||||
"maxLength": 1024,
|
"maxLength": 1024,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"engine_information": {
|
||||||
|
"readOnly": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -3817,6 +3823,9 @@
|
|||||||
"id": {
|
"id": {
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"engine_information": {
|
||||||
|
"readOnly": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -13634,6 +13643,10 @@
|
|||||||
"sqlalchemy_uri_placeholder": {
|
"sqlalchemy_uri_placeholder": {
|
||||||
"description": "Example placeholder for the SQLAlchemy URI",
|
"description": "Example placeholder for the SQLAlchemy URI",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"engine_information": {
|
||||||
|
"description": "Object with properties we want to expose from our DB engine",
|
||||||
|
"type": "object"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|||||||
@@ -229,7 +229,13 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
|
|||||||
SupersetClient.get({
|
SupersetClient.get({
|
||||||
endpoint: `/api/v1/database/?q=${rison.encode(payload)}`,
|
endpoint: `/api/v1/database/?q=${rison.encode(payload)}`,
|
||||||
}).then(({ json }: Record<string, any>) => {
|
}).then(({ json }: Record<string, any>) => {
|
||||||
setAllowUploads(json.count >= 1);
|
// There might be some existings Gsheets and Clickhouse DBs
|
||||||
|
// with allow_file_upload set as True which is not possible from now on
|
||||||
|
const allowedDatabasesWithFileUpload =
|
||||||
|
json?.result?.filter(
|
||||||
|
(database: any) => database?.engine_information?.supports_file_upload,
|
||||||
|
) || [];
|
||||||
|
setAllowUploads(allowedDatabasesWithFileUpload?.length >= 1);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ const ExtraOptions = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const expandableModalIsOpen = !!db?.expose_in_sqllab;
|
const expandableModalIsOpen = !!db?.expose_in_sqllab;
|
||||||
const createAsOpen = !!(db?.allow_ctas || db?.allow_cvas);
|
const createAsOpen = !!(db?.allow_ctas || db?.allow_cvas);
|
||||||
|
const isFileUploadSupportedByEngine =
|
||||||
|
db?.engine_information?.supports_file_upload;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapse
|
<Collapse
|
||||||
@@ -364,28 +366,9 @@ const ExtraOptions = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</StyledInputContainer>
|
</StyledInputContainer>
|
||||||
<StyledInputContainer>
|
<StyledInputContainer
|
||||||
<div className="control-label">
|
css={!isFileUploadSupportedByEngine ? no_margin_bottom : {}}
|
||||||
{t('Schemas allowed for CSV upload')}
|
>
|
||||||
</div>
|
|
||||||
<div className="input-container">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="schemas_allowed_for_file_upload"
|
|
||||||
value={(
|
|
||||||
db?.extra_json?.schemas_allowed_for_file_upload || []
|
|
||||||
).join(',')}
|
|
||||||
placeholder="schema1,schema2"
|
|
||||||
onChange={onExtraInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="helper">
|
|
||||||
{t(
|
|
||||||
'A comma-separated list of schemas that CSVs are allowed to upload to.',
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</StyledInputContainer>
|
|
||||||
<StyledInputContainer css={{ no_margin_bottom }}>
|
|
||||||
<div className="input-container">
|
<div className="input-container">
|
||||||
<IndeterminateCheckbox
|
<IndeterminateCheckbox
|
||||||
id="impersonate_user"
|
id="impersonate_user"
|
||||||
@@ -407,22 +390,44 @@ const ExtraOptions = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</StyledInputContainer>
|
</StyledInputContainer>
|
||||||
<StyledInputContainer css={{ ...no_margin_bottom }}>
|
{isFileUploadSupportedByEngine && (
|
||||||
<div className="input-container">
|
<StyledInputContainer
|
||||||
<IndeterminateCheckbox
|
css={!db?.allow_file_upload ? no_margin_bottom : {}}
|
||||||
id="allow_file_upload"
|
>
|
||||||
indeterminate={false}
|
<div className="input-container">
|
||||||
checked={!!db?.allow_file_upload}
|
<IndeterminateCheckbox
|
||||||
onChange={onInputChange}
|
id="allow_file_upload"
|
||||||
labelText={t('Allow data upload')}
|
indeterminate={false}
|
||||||
/>
|
checked={!!db?.allow_file_upload}
|
||||||
<InfoTooltip
|
onChange={onInputChange}
|
||||||
tooltip={t(
|
labelText={t('Allow file uploads to database')}
|
||||||
'If selected, please set the schemas allowed for data upload in Extra.',
|
/>
|
||||||
|
</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={(
|
||||||
|
db?.extra_json?.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>
|
||||||
</div>
|
</StyledInputContainer>
|
||||||
</StyledInputContainer>
|
)}
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
header={
|
header={
|
||||||
|
|||||||
@@ -99,12 +99,18 @@ fetchMock.mock(AVAILABLE_DB_ENDPOINT, {
|
|||||||
preferred: true,
|
preferred: true,
|
||||||
sqlalchemy_uri_placeholder:
|
sqlalchemy_uri_placeholder:
|
||||||
'postgresql://user:password@host:port/dbname[?key=value&key=value...]',
|
'postgresql://user:password@host:port/dbname[?key=value&key=value...]',
|
||||||
|
engine_information: {
|
||||||
|
supports_file_upload: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
available_drivers: ['rest'],
|
available_drivers: ['rest'],
|
||||||
engine: 'presto',
|
engine: 'presto',
|
||||||
name: 'Presto',
|
name: 'Presto',
|
||||||
preferred: true,
|
preferred: true,
|
||||||
|
engine_information: {
|
||||||
|
supports_file_upload: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
available_drivers: ['mysqldb'],
|
available_drivers: ['mysqldb'],
|
||||||
@@ -154,18 +160,27 @@ fetchMock.mock(AVAILABLE_DB_ENDPOINT, {
|
|||||||
preferred: true,
|
preferred: true,
|
||||||
sqlalchemy_uri_placeholder:
|
sqlalchemy_uri_placeholder:
|
||||||
'mysql://user:password@host:port/dbname[?key=value&key=value...]',
|
'mysql://user:password@host:port/dbname[?key=value&key=value...]',
|
||||||
|
engine_information: {
|
||||||
|
supports_file_upload: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
available_drivers: ['pysqlite'],
|
available_drivers: ['pysqlite'],
|
||||||
engine: 'sqlite',
|
engine: 'sqlite',
|
||||||
name: 'SQLite',
|
name: 'SQLite',
|
||||||
preferred: true,
|
preferred: true,
|
||||||
|
engine_information: {
|
||||||
|
supports_file_upload: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
available_drivers: ['rest'],
|
available_drivers: ['rest'],
|
||||||
engine: 'druid',
|
engine: 'druid',
|
||||||
name: 'Apache Druid',
|
name: 'Apache Druid',
|
||||||
preferred: false,
|
preferred: false,
|
||||||
|
engine_information: {
|
||||||
|
supports_file_upload: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
available_drivers: ['bigquery'],
|
available_drivers: ['bigquery'],
|
||||||
@@ -187,6 +202,19 @@ fetchMock.mock(AVAILABLE_DB_ENDPOINT, {
|
|||||||
},
|
},
|
||||||
preferred: false,
|
preferred: false,
|
||||||
sqlalchemy_uri_placeholder: 'bigquery://{project_id}',
|
sqlalchemy_uri_placeholder: 'bigquery://{project_id}',
|
||||||
|
engine_information: {
|
||||||
|
supports_file_upload: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
available_drivers: ['rest'],
|
||||||
|
default_driver: 'apsw',
|
||||||
|
engine: 'gsheets',
|
||||||
|
name: 'Google Sheets',
|
||||||
|
preferred: false,
|
||||||
|
engine_information: {
|
||||||
|
supports_file_upload: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -761,8 +789,17 @@ describe('DatabaseModal', () => {
|
|||||||
const securityTab = screen.getByRole('tab', {
|
const securityTab = screen.getByRole('tab', {
|
||||||
name: /right security add extra connection information\./i,
|
name: /right security add extra connection information\./i,
|
||||||
});
|
});
|
||||||
|
const allowFileUploadCheckbox = screen.getByRole('checkbox', {
|
||||||
|
name: /Allow file uploads to database/i,
|
||||||
|
});
|
||||||
|
const allowFileUploadText = screen.getByText(
|
||||||
|
/Allow file uploads to database/i,
|
||||||
|
);
|
||||||
|
|
||||||
|
const schemasForFileUploadText = screen.queryByText(
|
||||||
|
/Schemas allowed for File upload/i,
|
||||||
|
);
|
||||||
|
|
||||||
// ---------- Assertions ----------
|
|
||||||
const visibleComponents = [
|
const visibleComponents = [
|
||||||
closeButton,
|
closeButton,
|
||||||
advancedHeader,
|
advancedHeader,
|
||||||
@@ -775,11 +812,105 @@ describe('DatabaseModal', () => {
|
|||||||
sqlLabTab,
|
sqlLabTab,
|
||||||
performanceTab,
|
performanceTab,
|
||||||
securityTab,
|
securityTab,
|
||||||
|
allowFileUploadText,
|
||||||
];
|
];
|
||||||
|
// These components exist in the DOM but are not visible
|
||||||
|
const invisibleComponents = [allowFileUploadCheckbox];
|
||||||
|
|
||||||
|
// ---------- Assertions ----------
|
||||||
visibleComponents.forEach(component => {
|
visibleComponents.forEach(component => {
|
||||||
expect(component).toBeVisible();
|
expect(component).toBeVisible();
|
||||||
});
|
});
|
||||||
|
invisibleComponents.forEach(component => {
|
||||||
|
expect(component).not.toBeVisible();
|
||||||
|
});
|
||||||
|
expect(schemasForFileUploadText).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the "Advanced" - SECURITY tab correctly after selecting Allow file uploads', async () => {
|
||||||
|
// ---------- Components ----------
|
||||||
|
// On step 1, click dbButton to access step 2
|
||||||
|
userEvent.click(
|
||||||
|
screen.getByRole('button', {
|
||||||
|
name: /sqlite/i,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// Click the "Advanced" tab
|
||||||
|
userEvent.click(screen.getByRole('tab', { name: /advanced/i }));
|
||||||
|
// Click the "Security" tab
|
||||||
|
userEvent.click(
|
||||||
|
screen.getByRole('tab', {
|
||||||
|
name: /right security add extra connection information\./i,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// Click the "Allow file uploads" tab
|
||||||
|
|
||||||
|
const allowFileUploadCheckbox = screen.getByRole('checkbox', {
|
||||||
|
name: /Allow file uploads to database/i,
|
||||||
|
});
|
||||||
|
userEvent.click(allowFileUploadCheckbox);
|
||||||
|
|
||||||
|
// ----- BEGIN STEP 2 (ADVANCED - SECURITY)
|
||||||
|
// <TabHeader> - AntD header
|
||||||
|
const closeButton = screen.getByRole('button', { name: /close/i });
|
||||||
|
const advancedHeader = screen.getByRole('heading', {
|
||||||
|
name: /connect a database/i,
|
||||||
|
});
|
||||||
|
// <ModalHeader> - Connection header
|
||||||
|
const basicHelper = screen.getByText(/step 2 of 2/i);
|
||||||
|
const basicHeaderTitle = screen.getByText(/enter primary credentials/i);
|
||||||
|
const basicHeaderSubtitle = screen.getByText(
|
||||||
|
/need help\? learn how to connect your database \./i,
|
||||||
|
);
|
||||||
|
const basicHeaderLink = within(basicHeaderSubtitle).getByRole('link', {
|
||||||
|
name: /here/i,
|
||||||
|
});
|
||||||
|
// <Tabs> - Basic/Advanced tabs
|
||||||
|
const basicTab = screen.getByRole('tab', { name: /basic/i });
|
||||||
|
const advancedTab = screen.getByRole('tab', { name: /advanced/i });
|
||||||
|
// <ExtraOptions> - Advanced tabs
|
||||||
|
const sqlLabTab = screen.getByRole('tab', {
|
||||||
|
name: /right sql lab adjust how this database will interact with sql lab\./i,
|
||||||
|
});
|
||||||
|
const performanceTab = screen.getByRole('tab', {
|
||||||
|
name: /right performance adjust performance settings of this database\./i,
|
||||||
|
});
|
||||||
|
const securityTab = screen.getByRole('tab', {
|
||||||
|
name: /right security add extra connection information\./i,
|
||||||
|
});
|
||||||
|
const allowFileUploadText = screen.getByText(
|
||||||
|
/Allow file uploads to database/i,
|
||||||
|
);
|
||||||
|
|
||||||
|
const schemasForFileUploadText = screen.queryByText(
|
||||||
|
/Schemas allowed for File upload/i,
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibleComponents = [
|
||||||
|
closeButton,
|
||||||
|
advancedHeader,
|
||||||
|
basicHelper,
|
||||||
|
basicHeaderTitle,
|
||||||
|
basicHeaderSubtitle,
|
||||||
|
basicHeaderLink,
|
||||||
|
basicTab,
|
||||||
|
advancedTab,
|
||||||
|
sqlLabTab,
|
||||||
|
performanceTab,
|
||||||
|
securityTab,
|
||||||
|
allowFileUploadText,
|
||||||
|
];
|
||||||
|
// These components exist in the DOM but are not visible
|
||||||
|
const invisibleComponents = [allowFileUploadCheckbox];
|
||||||
|
|
||||||
|
// ---------- Assertions ----------
|
||||||
|
visibleComponents.forEach(component => {
|
||||||
|
expect(component).toBeVisible();
|
||||||
|
});
|
||||||
|
invisibleComponents.forEach(component => {
|
||||||
|
expect(component).not.toBeVisible();
|
||||||
|
});
|
||||||
|
expect(schemasForFileUploadText).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders the "Advanced" - OTHER tab correctly', async () => {
|
test('renders the "Advanced" - OTHER tab correctly', async () => {
|
||||||
@@ -1072,4 +1203,70 @@ describe('DatabaseModal', () => {
|
|||||||
expect(step2of3text).toBeVisible();
|
expect(step2of3text).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('DatabaseModal w/ GSheet Engine', () => {
|
||||||
|
const renderAndWait = async () => {
|
||||||
|
const dbProps = {
|
||||||
|
show: true,
|
||||||
|
database_name: 'my database',
|
||||||
|
sqlalchemy_uri: 'gsheets://',
|
||||||
|
};
|
||||||
|
const mounted = act(async () => {
|
||||||
|
render(<DatabaseModal {...dbProps} dbEngine="Google Sheets" />, {
|
||||||
|
useRedux: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return mounted;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await renderAndWait();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('enters step 2 of 2 when proper database is selected', () => {
|
||||||
|
const step2of2text = screen.getByText(/step 2 of 2/i);
|
||||||
|
expect(step2of2text).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the "Advanced" - SECURITY tab without Allow File Upload Checkbox', async () => {
|
||||||
|
// Click the "Advanced" tab
|
||||||
|
userEvent.click(screen.getByRole('tab', { name: /advanced/i }));
|
||||||
|
// Click the "Security" tab
|
||||||
|
userEvent.click(
|
||||||
|
screen.getByRole('tab', {
|
||||||
|
name: /right security add extra connection information\./i,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ----- BEGIN STEP 2 (ADVANCED - SECURITY)
|
||||||
|
// <ExtraOptions> - Advanced tabs
|
||||||
|
const impersonateLoggerUserCheckbox = screen.getByRole('checkbox', {
|
||||||
|
name: /impersonate logged in/i,
|
||||||
|
});
|
||||||
|
const impersonateLoggerUserText = screen.getByText(
|
||||||
|
/impersonate logged in/i,
|
||||||
|
);
|
||||||
|
const allowFileUploadText = screen.queryByText(
|
||||||
|
/Allow file uploads to database/i,
|
||||||
|
);
|
||||||
|
const schemasForFileUploadText = screen.queryByText(
|
||||||
|
/Schemas allowed for File upload/i,
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibleComponents = [impersonateLoggerUserText];
|
||||||
|
// These components exist in the DOM but are not visible
|
||||||
|
const invisibleComponents = [impersonateLoggerUserCheckbox];
|
||||||
|
|
||||||
|
// ---------- Assertions ----------
|
||||||
|
visibleComponents.forEach(component => {
|
||||||
|
expect(component).toBeVisible();
|
||||||
|
});
|
||||||
|
invisibleComponents.forEach(component => {
|
||||||
|
expect(component).not.toBeVisible();
|
||||||
|
});
|
||||||
|
expect(allowFileUploadText).not.toBeInTheDocument();
|
||||||
|
expect(schemasForFileUploadText).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ type DBReducerActionType =
|
|||||||
database_name?: string;
|
database_name?: string;
|
||||||
engine?: string;
|
engine?: string;
|
||||||
configuration_method: CONFIGURATION_METHOD;
|
configuration_method: CONFIGURATION_METHOD;
|
||||||
|
engine_information?: {};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -718,7 +719,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||||||
const selectedDbModel = availableDbs?.databases.filter(
|
const selectedDbModel = availableDbs?.databases.filter(
|
||||||
(db: DatabaseObject) => db.name === database_name,
|
(db: DatabaseObject) => db.name === database_name,
|
||||||
)[0];
|
)[0];
|
||||||
const { engine, parameters } = selectedDbModel;
|
const { engine, parameters, engine_information } = selectedDbModel;
|
||||||
const isDynamic = parameters !== undefined;
|
const isDynamic = parameters !== undefined;
|
||||||
setDB({
|
setDB({
|
||||||
type: ActionType.dbSelected,
|
type: ActionType.dbSelected,
|
||||||
@@ -728,6 +729,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||||||
configuration_method: isDynamic
|
configuration_method: isDynamic
|
||||||
? CONFIGURATION_METHOD.DYNAMIC_FORM
|
? CONFIGURATION_METHOD.DYNAMIC_FORM
|
||||||
: CONFIGURATION_METHOD.SQLALCHEMY_URI,
|
: CONFIGURATION_METHOD.SQLALCHEMY_URI,
|
||||||
|
engine_information,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,11 @@ export type DatabaseObject = {
|
|||||||
catalog?: Array<CatalogObject>;
|
catalog?: Array<CatalogObject>;
|
||||||
query_input?: string;
|
query_input?: string;
|
||||||
extra?: string;
|
extra?: string;
|
||||||
|
|
||||||
|
// DB Engine Spec information
|
||||||
|
engine_information?: {
|
||||||
|
supports_file_upload?: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DatabaseForm = {
|
export type DatabaseForm = {
|
||||||
|
|||||||
268
superset-frontend/src/views/components/RightMenu.test.tsx
Normal file
268
superset-frontend/src/views/components/RightMenu.test.tsx
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
/**
|
||||||
|
* 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 * as reactRedux from 'react-redux';
|
||||||
|
import fetchMock from 'fetch-mock';
|
||||||
|
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||||
|
import { styledMount as mount } from 'spec/helpers/theming';
|
||||||
|
import RightMenu from './RightMenu';
|
||||||
|
import { RightMenuProps } from './types';
|
||||||
|
|
||||||
|
jest.mock('react-redux', () => ({
|
||||||
|
...jest.requireActual('react-redux'),
|
||||||
|
useSelector: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const createProps = (): RightMenuProps => ({
|
||||||
|
align: 'flex-end',
|
||||||
|
navbarRight: {
|
||||||
|
show_watermark: false,
|
||||||
|
bug_report_url: '/report/',
|
||||||
|
documentation_url: '/docs/',
|
||||||
|
languages: {
|
||||||
|
en: {
|
||||||
|
flag: 'us',
|
||||||
|
name: 'English',
|
||||||
|
url: '/lang/en',
|
||||||
|
},
|
||||||
|
it: {
|
||||||
|
flag: 'it',
|
||||||
|
name: 'Italian',
|
||||||
|
url: '/lang/it',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
show_language_picker: true,
|
||||||
|
user_is_anonymous: true,
|
||||||
|
user_info_url: '/users/userinfo/',
|
||||||
|
user_logout_url: '/logout/',
|
||||||
|
user_login_url: '/login/',
|
||||||
|
user_profile_url: '/profile/',
|
||||||
|
locale: 'en',
|
||||||
|
version_string: '1.0.0',
|
||||||
|
version_sha: 'randomSHA',
|
||||||
|
build_number: 'randomBuildNumber',
|
||||||
|
},
|
||||||
|
settings: [
|
||||||
|
{
|
||||||
|
name: 'Security',
|
||||||
|
icon: 'fa-cogs',
|
||||||
|
label: 'Security',
|
||||||
|
index: 1,
|
||||||
|
childs: [
|
||||||
|
{
|
||||||
|
name: 'List Users',
|
||||||
|
icon: 'fa-user',
|
||||||
|
label: 'List Users',
|
||||||
|
url: '/users/list/',
|
||||||
|
index: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isFrontendRoute: () => true,
|
||||||
|
environmentTag: {
|
||||||
|
color: 'error.base',
|
||||||
|
text: 'Development',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
|
||||||
|
const useStateMock = jest.spyOn(React, 'useState');
|
||||||
|
|
||||||
|
let setShowModal: any;
|
||||||
|
let setEngine: any;
|
||||||
|
let setAllowUploads: any;
|
||||||
|
|
||||||
|
const mockNonGSheetsDBs = [...new Array(2)].map((_, i) => ({
|
||||||
|
changed_by: {
|
||||||
|
first_name: `user`,
|
||||||
|
last_name: `${i}`,
|
||||||
|
},
|
||||||
|
database_name: `db ${i}`,
|
||||||
|
backend: 'postgresql',
|
||||||
|
allow_run_async: true,
|
||||||
|
allow_dml: false,
|
||||||
|
allow_file_upload: true,
|
||||||
|
expose_in_sqllab: false,
|
||||||
|
changed_on_delta_humanized: `${i} day(s) ago`,
|
||||||
|
changed_on: new Date().toISOString,
|
||||||
|
id: i,
|
||||||
|
engine_information: {
|
||||||
|
supports_file_upload: true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockGsheetsDbs = [...new Array(2)].map((_, i) => ({
|
||||||
|
changed_by: {
|
||||||
|
first_name: `user`,
|
||||||
|
last_name: `${i}`,
|
||||||
|
},
|
||||||
|
database_name: `db ${i}`,
|
||||||
|
backend: 'gsheets',
|
||||||
|
allow_run_async: true,
|
||||||
|
allow_dml: false,
|
||||||
|
allow_file_upload: true,
|
||||||
|
expose_in_sqllab: false,
|
||||||
|
changed_on_delta_humanized: `${i} day(s) ago`,
|
||||||
|
changed_on: new Date().toISOString,
|
||||||
|
id: i,
|
||||||
|
engine_information: {
|
||||||
|
supports_file_upload: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('RightMenu', () => {
|
||||||
|
const mockedProps = createProps();
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
useSelectorMock.mockReset();
|
||||||
|
useStateMock.mockReset();
|
||||||
|
fetchMock.get(
|
||||||
|
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
|
||||||
|
{ result: [], database_count: 0 },
|
||||||
|
);
|
||||||
|
// By default we get file extensions to be uploaded
|
||||||
|
useSelectorMock.mockReturnValue({
|
||||||
|
CSV_EXTENSIONS: ['csv'],
|
||||||
|
EXCEL_EXTENSIONS: ['xls', 'xlsx'],
|
||||||
|
COLUMNAR_EXTENSIONS: ['parquet', 'zip'],
|
||||||
|
ALLOWED_EXTENSIONS: ['parquet', 'zip', 'xls', 'xlsx', 'csv'],
|
||||||
|
});
|
||||||
|
setShowModal = jest.fn();
|
||||||
|
setEngine = jest.fn();
|
||||||
|
setAllowUploads = jest.fn();
|
||||||
|
const mockSetStateModal: any = (x: any) => [x, setShowModal];
|
||||||
|
const mockSetStateEngine: any = (x: any) => [x, setEngine];
|
||||||
|
const mockSetStateAllow: any = (x: any) => [x, setAllowUploads];
|
||||||
|
useStateMock.mockImplementationOnce(mockSetStateModal);
|
||||||
|
useStateMock.mockImplementationOnce(mockSetStateEngine);
|
||||||
|
useStateMock.mockImplementationOnce(mockSetStateAllow);
|
||||||
|
});
|
||||||
|
afterEach(fetchMock.restore);
|
||||||
|
it('renders', async () => {
|
||||||
|
const wrapper = mount(<RightMenu {...mockedProps} />);
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
expect(wrapper.find(RightMenu)).toExist();
|
||||||
|
});
|
||||||
|
it('If user has permission to upload files we query the existing DBs that has allow_file_upload as True', async () => {
|
||||||
|
useSelectorMock.mockReturnValueOnce({
|
||||||
|
createdOn: '2021-04-27T18:12:38.952304',
|
||||||
|
email: 'admin',
|
||||||
|
firstName: 'admin',
|
||||||
|
isActive: true,
|
||||||
|
lastName: 'admin',
|
||||||
|
permissions: {},
|
||||||
|
roles: {
|
||||||
|
Admin: [
|
||||||
|
['can_this_form_get', 'CsvToDatabaseView'], // So we can upload CSV
|
||||||
|
],
|
||||||
|
},
|
||||||
|
userId: 1,
|
||||||
|
username: 'admin',
|
||||||
|
});
|
||||||
|
// Second call we get the dashboardId
|
||||||
|
useSelectorMock.mockReturnValueOnce('1');
|
||||||
|
const wrapper = mount(<RightMenu {...mockedProps} />);
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
const callsD = fetchMock.calls(/database\/\?q/);
|
||||||
|
expect(callsD).toHaveLength(1);
|
||||||
|
expect(callsD[0][0]).toMatchInlineSnapshot(
|
||||||
|
`"http://localhost/api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('If user has no permission to upload files the query API should not be called', async () => {
|
||||||
|
useSelectorMock.mockReturnValueOnce({
|
||||||
|
createdOn: '2021-04-27T18:12:38.952304',
|
||||||
|
email: 'admin',
|
||||||
|
firstName: 'admin',
|
||||||
|
isActive: true,
|
||||||
|
lastName: 'admin',
|
||||||
|
permissions: {},
|
||||||
|
roles: {
|
||||||
|
Admin: [['can_write', 'Chart']], // no file permissions
|
||||||
|
},
|
||||||
|
userId: 1,
|
||||||
|
username: 'admin',
|
||||||
|
});
|
||||||
|
// Second call we get the dashboardId
|
||||||
|
useSelectorMock.mockReturnValueOnce('1');
|
||||||
|
const wrapper = mount(<RightMenu {...mockedProps} />);
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
const callsD = fetchMock.calls(/database\/\?q/);
|
||||||
|
expect(callsD).toHaveLength(0);
|
||||||
|
});
|
||||||
|
it('If user has permission to upload files but there are only gsheets and clickhouse DBs', async () => {
|
||||||
|
fetchMock.get(
|
||||||
|
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
|
||||||
|
{ result: [...mockGsheetsDbs], database_count: 2 },
|
||||||
|
{ overwriteRoutes: true },
|
||||||
|
);
|
||||||
|
useSelectorMock.mockReturnValueOnce({
|
||||||
|
createdOn: '2021-04-27T18:12:38.952304',
|
||||||
|
email: 'admin',
|
||||||
|
firstName: 'admin',
|
||||||
|
isActive: true,
|
||||||
|
lastName: 'admin',
|
||||||
|
permissions: {},
|
||||||
|
roles: {
|
||||||
|
Admin: [
|
||||||
|
['can_this_form_get', 'CsvToDatabaseView'], // So we can upload CSV
|
||||||
|
],
|
||||||
|
},
|
||||||
|
userId: 1,
|
||||||
|
username: 'admin',
|
||||||
|
});
|
||||||
|
// Second call we get the dashboardId
|
||||||
|
useSelectorMock.mockReturnValueOnce('1');
|
||||||
|
const wrapper = mount(<RightMenu {...mockedProps} />);
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
const callsD = fetchMock.calls(/database\/\?q/);
|
||||||
|
expect(callsD).toHaveLength(1);
|
||||||
|
expect(setAllowUploads).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
it('If user has permission to upload files and some DBs with allow_file_upload are not gsheets nor clickhouse', async () => {
|
||||||
|
fetchMock.get(
|
||||||
|
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
|
||||||
|
{ result: [...mockNonGSheetsDBs, ...mockGsheetsDbs], database_count: 2 },
|
||||||
|
{ overwriteRoutes: true },
|
||||||
|
);
|
||||||
|
useSelectorMock.mockReturnValueOnce({
|
||||||
|
createdOn: '2021-04-27T18:12:38.952304',
|
||||||
|
email: 'admin',
|
||||||
|
firstName: 'admin',
|
||||||
|
isActive: true,
|
||||||
|
lastName: 'admin',
|
||||||
|
permissions: {},
|
||||||
|
roles: {
|
||||||
|
Admin: [
|
||||||
|
['can_this_form_get', 'CsvToDatabaseView'], // So we can upload CSV
|
||||||
|
],
|
||||||
|
},
|
||||||
|
userId: 1,
|
||||||
|
username: 'admin',
|
||||||
|
});
|
||||||
|
// Second call we get the dashboardId
|
||||||
|
useSelectorMock.mockReturnValueOnce('1');
|
||||||
|
const wrapper = mount(<RightMenu {...mockedProps} />);
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
const callsD = fetchMock.calls(/database\/\?q/);
|
||||||
|
expect(callsD).toHaveLength(1);
|
||||||
|
expect(setAllowUploads).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { Fragment, useState, useEffect } from 'react';
|
import React, { Fragment, useEffect } from 'react';
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@@ -118,8 +118,8 @@ const RightMenu = ({
|
|||||||
ALLOWED_EXTENSIONS,
|
ALLOWED_EXTENSIONS,
|
||||||
HAS_GSHEETS_INSTALLED,
|
HAS_GSHEETS_INSTALLED,
|
||||||
} = useSelector<any, ExtentionConfigs>(state => state.common.conf);
|
} = useSelector<any, ExtentionConfigs>(state => state.common.conf);
|
||||||
const [showModal, setShowModal] = useState<boolean>(false);
|
const [showModal, setShowModal] = React.useState<boolean>(false);
|
||||||
const [engine, setEngine] = useState<string>('');
|
const [engine, setEngine] = React.useState<string>('');
|
||||||
const canSql = findPermission('can_sqllab', 'Superset', roles);
|
const canSql = findPermission('can_sqllab', 'Superset', roles);
|
||||||
const canDashboard = findPermission('can_write', 'Dashboard', roles);
|
const canDashboard = findPermission('can_write', 'Dashboard', roles);
|
||||||
const canChart = findPermission('can_write', 'Chart', roles);
|
const canChart = findPermission('can_write', 'Chart', roles);
|
||||||
@@ -135,7 +135,7 @@ const RightMenu = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const showActionDropdown = canSql || canChart || canDashboard;
|
const showActionDropdown = canSql || canChart || canDashboard;
|
||||||
const [allowUploads, setAllowUploads] = useState<boolean>(false);
|
const [allowUploads, setAllowUploads] = React.useState<boolean>(false);
|
||||||
const isAdmin = isUserAdmin(user);
|
const isAdmin = isUserAdmin(user);
|
||||||
const showUploads = allowUploads || isAdmin;
|
const showUploads = allowUploads || isAdmin;
|
||||||
const dropdownItems: MenuObjectProps[] = [
|
const dropdownItems: MenuObjectProps[] = [
|
||||||
@@ -207,7 +207,13 @@ const RightMenu = ({
|
|||||||
SupersetClient.get({
|
SupersetClient.get({
|
||||||
endpoint: `/api/v1/database/?q=${rison.encode(payload)}`,
|
endpoint: `/api/v1/database/?q=${rison.encode(payload)}`,
|
||||||
}).then(({ json }: Record<string, any>) => {
|
}).then(({ json }: Record<string, any>) => {
|
||||||
setAllowUploads(json.count >= 1);
|
// There might be some existings Gsheets and Clickhouse DBs
|
||||||
|
// with allow_file_upload set as True which is not possible from now on
|
||||||
|
const allowedDatabasesWithFileUpload =
|
||||||
|
json?.result?.filter(
|
||||||
|
(database: any) => database?.engine_information?.supports_file_upload,
|
||||||
|
) || [];
|
||||||
|
setAllowUploads(allowedDatabasesWithFileUpload?.length >= 1);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -241,7 +247,7 @@ const RightMenu = ({
|
|||||||
const isDisabled = isAdmin && !allowUploads;
|
const isDisabled = isAdmin && !allowUploads;
|
||||||
|
|
||||||
const tooltipText = t(
|
const tooltipText = t(
|
||||||
"Enable 'Allow data upload' in any database's settings",
|
"Enable 'Allow file uploads to database' in any database's settings",
|
||||||
);
|
);
|
||||||
|
|
||||||
const buildMenuItem = (item: Record<string, any>) => {
|
const buildMenuItem = (item: Record<string, any>) => {
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ const SubMenuComponent: React.FunctionComponent<SubMenuProps> = props => {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
placement="top"
|
placement="top"
|
||||||
title={t(
|
title={t(
|
||||||
"Enable 'Allow data upload' in any database's settings",
|
"Enable 'Allow file uploads to database' in any database's settings",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
|||||||
"server_cert",
|
"server_cert",
|
||||||
"sqlalchemy_uri",
|
"sqlalchemy_uri",
|
||||||
"is_managed_externally",
|
"is_managed_externally",
|
||||||
|
"engine_information",
|
||||||
]
|
]
|
||||||
list_columns = [
|
list_columns = [
|
||||||
"allow_file_upload",
|
"allow_file_upload",
|
||||||
@@ -151,6 +152,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
|||||||
"force_ctas_schema",
|
"force_ctas_schema",
|
||||||
"id",
|
"id",
|
||||||
"disable_data_preview",
|
"disable_data_preview",
|
||||||
|
"engine_information",
|
||||||
]
|
]
|
||||||
add_columns = [
|
add_columns = [
|
||||||
"database_name",
|
"database_name",
|
||||||
@@ -1062,6 +1064,13 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
|||||||
parameters:
|
parameters:
|
||||||
description: JSON schema defining the needed parameters
|
description: JSON schema defining the needed parameters
|
||||||
type: object
|
type: object
|
||||||
|
engine_information:
|
||||||
|
description: Dict with public properties form the DB Engine
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
supports_file_upload:
|
||||||
|
description: Whether the engine supports file uploads
|
||||||
|
type: boolean
|
||||||
400:
|
400:
|
||||||
$ref: '#/components/responses/400'
|
$ref: '#/components/responses/400'
|
||||||
500:
|
500:
|
||||||
@@ -1078,6 +1087,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
|||||||
"engine": engine_spec.engine,
|
"engine": engine_spec.engine,
|
||||||
"available_drivers": sorted(drivers),
|
"available_drivers": sorted(drivers),
|
||||||
"preferred": engine_spec.engine_name in preferred_databases,
|
"preferred": engine_spec.engine_name in preferred_databases,
|
||||||
|
"engine_information": engine_spec.get_public_information(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if engine_spec.default_driver:
|
if engine_spec.default_driver:
|
||||||
|
|||||||
@@ -361,6 +361,10 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
|||||||
Pattern[str], Tuple[str, SupersetErrorType, Dict[str, Any]]
|
Pattern[str], Tuple[str, SupersetErrorType, Dict[str, Any]]
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
|
# Whether the engine supports file uploads
|
||||||
|
# if True, database will be listed as option in the upload file form
|
||||||
|
supports_file_upload = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def supports_url(cls, url: URL) -> bool:
|
def supports_url(cls, url: URL) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -1637,6 +1641,17 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
|||||||
"""
|
"""
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_public_information(cls) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Construct a Dict with properties we want to expose.
|
||||||
|
|
||||||
|
:returns: Dict with properties of our class like supports_file_upload
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"supports_file_upload": cls.supports_file_upload,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# schema for adding a database by providing parameters instead of the
|
# schema for adding a database by providing parameters instead of the
|
||||||
# full SQLAlchemy URI
|
# full SQLAlchemy URI
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ class ClickHouseEngineSpec(BaseEngineSpec): # pylint: disable=abstract-method
|
|||||||
|
|
||||||
_show_functions_column = "name"
|
_show_functions_column = "name"
|
||||||
|
|
||||||
|
supports_file_upload = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_dbapi_exception_mapping(cls) -> Dict[Type[Exception], Type[Exception]]:
|
def get_dbapi_exception_mapping(cls) -> Dict[Type[Exception], Type[Exception]]:
|
||||||
return {NewConnectionError: SupersetDBAPIDatabaseError}
|
return {NewConnectionError: SupersetDBAPIDatabaseError}
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ class GSheetsEngineSpec(SqliteEngineSpec):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
supports_file_upload = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_url_for_impersonation(
|
def get_url_for_impersonation(
|
||||||
cls,
|
cls,
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ class Database(
|
|||||||
"parameters": self.parameters,
|
"parameters": self.parameters,
|
||||||
"disable_data_preview": self.disable_data_preview,
|
"disable_data_preview": self.disable_data_preview,
|
||||||
"parameters_schema": self.parameters_schema,
|
"parameters_schema": self.parameters_schema,
|
||||||
|
"engine_information": self.engine_information,
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -312,6 +313,14 @@ class Database(
|
|||||||
def connect_args(self) -> Dict[str, Any]:
|
def connect_args(self) -> Dict[str, Any]:
|
||||||
return self.get_extra().get("engine_params", {}).get("connect_args", {})
|
return self.get_extra().get("engine_params", {}).get("connect_args", {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def engine_information(self) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
engine_information = self.db_engine_spec.get_public_information()
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
engine_information = {}
|
||||||
|
return engine_information
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_password_masked_url_from_uri( # pylint: disable=invalid-name
|
def get_password_masked_url_from_uri( # pylint: disable=invalid-name
|
||||||
cls, uri: str
|
cls, uri: str
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class UploadToDatabaseForm(DynamicForm):
|
|||||||
file_enabled_db
|
file_enabled_db
|
||||||
for file_enabled_db in file_enabled_dbs
|
for file_enabled_db in file_enabled_dbs
|
||||||
if UploadToDatabaseForm.at_least_one_schema_is_allowed(file_enabled_db)
|
if UploadToDatabaseForm.at_least_one_schema_is_allowed(file_enabled_db)
|
||||||
|
and UploadToDatabaseForm.is_engine_allowed_to_file_upl(file_enabled_db)
|
||||||
]
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -89,6 +90,19 @@ class UploadToDatabaseForm(DynamicForm):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_engine_allowed_to_file_upl(database: Database) -> bool:
|
||||||
|
"""
|
||||||
|
This method is mainly used for existing Gsheets and Clickhouse DBs
|
||||||
|
that have allow_file_upload set as True but they are no longer valid
|
||||||
|
DBs for file uploading.
|
||||||
|
New GSheets and Clickhouse DBs won't have the option to set
|
||||||
|
allow_file_upload set as True.
|
||||||
|
"""
|
||||||
|
if database.db_engine_spec.supports_file_upload:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class CsvToDatabaseForm(UploadToDatabaseForm):
|
class CsvToDatabaseForm(UploadToDatabaseForm):
|
||||||
name = StringField(
|
name = StringField(
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ class TestDatabaseApi(SupersetTestCase):
|
|||||||
"created_by",
|
"created_by",
|
||||||
"database_name",
|
"database_name",
|
||||||
"disable_data_preview",
|
"disable_data_preview",
|
||||||
|
"engine_information",
|
||||||
"explore_database_id",
|
"explore_database_id",
|
||||||
"expose_in_sqllab",
|
"expose_in_sqllab",
|
||||||
"extra",
|
"extra",
|
||||||
@@ -1941,6 +1942,9 @@ class TestDatabaseApi(SupersetTestCase):
|
|||||||
},
|
},
|
||||||
"preferred": True,
|
"preferred": True,
|
||||||
"sqlalchemy_uri_placeholder": "postgresql://user:password@host:port/dbname[?key=value&key=value...]",
|
"sqlalchemy_uri_placeholder": "postgresql://user:password@host:port/dbname[?key=value&key=value...]",
|
||||||
|
"engine_information": {
|
||||||
|
"supports_file_upload": True,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"available_drivers": ["bigquery"],
|
"available_drivers": ["bigquery"],
|
||||||
@@ -1960,6 +1964,9 @@ class TestDatabaseApi(SupersetTestCase):
|
|||||||
},
|
},
|
||||||
"preferred": True,
|
"preferred": True,
|
||||||
"sqlalchemy_uri_placeholder": "bigquery://{project_id}",
|
"sqlalchemy_uri_placeholder": "bigquery://{project_id}",
|
||||||
|
"engine_information": {
|
||||||
|
"supports_file_upload": True,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"available_drivers": ["psycopg2"],
|
"available_drivers": ["psycopg2"],
|
||||||
@@ -2008,6 +2015,9 @@ class TestDatabaseApi(SupersetTestCase):
|
|||||||
},
|
},
|
||||||
"preferred": False,
|
"preferred": False,
|
||||||
"sqlalchemy_uri_placeholder": "redshift+psycopg2://user:password@host:port/dbname[?key=value&key=value...]",
|
"sqlalchemy_uri_placeholder": "redshift+psycopg2://user:password@host:port/dbname[?key=value&key=value...]",
|
||||||
|
"engine_information": {
|
||||||
|
"supports_file_upload": True,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"available_drivers": ["apsw"],
|
"available_drivers": ["apsw"],
|
||||||
@@ -2027,6 +2037,9 @@ class TestDatabaseApi(SupersetTestCase):
|
|||||||
},
|
},
|
||||||
"preferred": False,
|
"preferred": False,
|
||||||
"sqlalchemy_uri_placeholder": "gsheets://",
|
"sqlalchemy_uri_placeholder": "gsheets://",
|
||||||
|
"engine_information": {
|
||||||
|
"supports_file_upload": False,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"available_drivers": ["mysqlconnector", "mysqldb"],
|
"available_drivers": ["mysqlconnector", "mysqldb"],
|
||||||
@@ -2075,12 +2088,18 @@ class TestDatabaseApi(SupersetTestCase):
|
|||||||
},
|
},
|
||||||
"preferred": False,
|
"preferred": False,
|
||||||
"sqlalchemy_uri_placeholder": "mysql://user:password@host:port/dbname[?key=value&key=value...]",
|
"sqlalchemy_uri_placeholder": "mysql://user:password@host:port/dbname[?key=value&key=value...]",
|
||||||
|
"engine_information": {
|
||||||
|
"supports_file_upload": True,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"available_drivers": [""],
|
"available_drivers": [""],
|
||||||
"engine": "hana",
|
"engine": "hana",
|
||||||
"name": "SAP HANA",
|
"name": "SAP HANA",
|
||||||
"preferred": False,
|
"preferred": False,
|
||||||
|
"engine_information": {
|
||||||
|
"supports_file_upload": True,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -2108,12 +2127,18 @@ class TestDatabaseApi(SupersetTestCase):
|
|||||||
"engine": "mysql",
|
"engine": "mysql",
|
||||||
"name": "MySQL",
|
"name": "MySQL",
|
||||||
"preferred": True,
|
"preferred": True,
|
||||||
|
"engine_information": {
|
||||||
|
"supports_file_upload": True,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"available_drivers": [""],
|
"available_drivers": [""],
|
||||||
"engine": "hana",
|
"engine": "hana",
|
||||||
"name": "SAP HANA",
|
"name": "SAP HANA",
|
||||||
"preferred": False,
|
"preferred": False,
|
||||||
|
"engine_information": {
|
||||||
|
"supports_file_upload": True,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user