mirror of
https://github.com/apache/superset.git
synced 2024-09-06 13:57:40 -04:00
feat: Move Database Import option into DB Connection modal (#19314)
* rebase * more progress * Fix unintended changes * DB import goes to step 3 * debugging * DB list refreshing properly * import screens flowing properly * Code cleanup * Fixed back button on import flow * Remove import db tooltip test * Fix test * Password field resets properly * Changed import modal state dictators and removed unneeded comment * Removed unneeded param pass and corrected modal spelling * Fixed typos * Changed file to fileList * Clarified import footer comment * Cleaned passwordNeededField and confirmOverwriteField state checks * debugging * Import state flow fixed * Removed unneeded importModal check in unreachable area * Fixed import db footer behavior when pressing back on step 2 * Import db button now at 14px * Removed animation from import db button * Fixed doble-loading successToast * Fixed errored import behavior * Updated import password check info box text * Connect button disables while importing db is loading * Connect button disables while overwrite confirmation is still needed * Connect button disables while password confirmation is still needed * Removed gray line under upload filename * Hide trashcan icon on import filename * Modal scroll behavior fixed for importing filename * Changed errored to failed * RTL testing for db import
This commit is contained in:
parent
1ad82af058
commit
d52e3867ac
@ -66,6 +66,14 @@ export interface ButtonProps {
|
||||
cta?: boolean;
|
||||
loading?: boolean | { delay?: number | undefined } | undefined;
|
||||
showMarginRight?: boolean;
|
||||
type?:
|
||||
| 'default'
|
||||
| 'text'
|
||||
| 'link'
|
||||
| 'primary'
|
||||
| 'dashed'
|
||||
| 'ghost'
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export default function Button(props: ButtonProps) {
|
||||
|
@ -23,10 +23,6 @@ import configureStore from 'redux-mock-store';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { Provider } from 'react-redux';
|
||||
import { styledMount as mount } from 'spec/helpers/theming';
|
||||
import { render, screen, cleanup } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { QueryParamProvider } from 'use-query-params';
|
||||
import * as featureFlags from 'src/featureFlags';
|
||||
|
||||
import DatabaseList from 'src/views/CRUD/data/database/DatabaseList';
|
||||
import DatabaseModal from 'src/views/CRUD/data/database/DatabaseModal';
|
||||
@ -41,17 +37,6 @@ import { act } from 'react-dom/test-utils';
|
||||
const mockStore = configureStore([thunk]);
|
||||
const store = mockStore({});
|
||||
|
||||
const mockAppState = {
|
||||
common: {
|
||||
config: {
|
||||
CSV_EXTENSIONS: ['csv'],
|
||||
EXCEL_EXTENSIONS: ['xls', 'xlsx'],
|
||||
COLUMNAR_EXTENSIONS: ['parquet', 'zip'],
|
||||
ALLOWED_EXTENSIONS: ['parquet', 'zip', 'xls', 'xlsx', 'csv'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const databasesInfoEndpoint = 'glob:*/api/v1/database/_info*';
|
||||
const databasesEndpoint = 'glob:*/api/v1/database/?*';
|
||||
const databaseEndpoint = 'glob:*/api/v1/database/*';
|
||||
@ -208,44 +193,3 @@ describe('DatabaseList', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RTL', () => {
|
||||
async function renderAndWait() {
|
||||
const mounted = act(async () => {
|
||||
render(
|
||||
<QueryParamProvider>
|
||||
<DatabaseList user={mockUser} />
|
||||
</QueryParamProvider>,
|
||||
{ useRedux: true },
|
||||
mockAppState,
|
||||
);
|
||||
});
|
||||
|
||||
return mounted;
|
||||
}
|
||||
|
||||
let isFeatureEnabledMock;
|
||||
beforeEach(async () => {
|
||||
isFeatureEnabledMock = jest
|
||||
.spyOn(featureFlags, 'isFeatureEnabled')
|
||||
.mockImplementation(() => true);
|
||||
await renderAndWait();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
isFeatureEnabledMock.mockRestore();
|
||||
});
|
||||
|
||||
it('renders an "Import Database" tooltip under import button', async () => {
|
||||
const importButton = await screen.findByTestId('import-button');
|
||||
userEvent.hover(importButton);
|
||||
|
||||
await screen.findByRole('tooltip');
|
||||
const importTooltip = screen.getByRole('tooltip', {
|
||||
name: 'Import databases',
|
||||
});
|
||||
|
||||
expect(importTooltip).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -30,7 +30,6 @@ import { Tooltip } from 'src/components/Tooltip';
|
||||
import Icons from 'src/components/Icons';
|
||||
import ListView, { FilterOperator, Filters } from 'src/components/ListView';
|
||||
import { commonMenuData } from 'src/views/CRUD/data/common';
|
||||
import ImportModelsModal from 'src/components/ImportModal/index';
|
||||
import handleResourceExport from 'src/utils/export';
|
||||
import { ExtentionConfigs } from 'src/views/components/types';
|
||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||
@ -39,17 +38,6 @@ import DatabaseModal from './DatabaseModal';
|
||||
import { DatabaseObject } from './types';
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
const PASSWORDS_NEEDED_MESSAGE = t(
|
||||
'The passwords for the databases below are needed in order to ' +
|
||||
'import them. Please note that the "Secure Extra" and "Certificate" ' +
|
||||
'sections of the database configuration are not present in export ' +
|
||||
'files, and should be added manually after the import if they are needed.',
|
||||
);
|
||||
const CONFIRM_OVERWRITE_MESSAGE = t(
|
||||
'You are importing one or more databases that already exist. ' +
|
||||
'Overwriting might cause you to lose some of your work. Are you ' +
|
||||
'sure you want to overwrite?',
|
||||
);
|
||||
|
||||
interface DatabaseDeleteObject extends DatabaseObject {
|
||||
chart_count: number;
|
||||
@ -103,8 +91,6 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
|
||||
const [currentDatabase, setCurrentDatabase] = useState<DatabaseObject | null>(
|
||||
null,
|
||||
);
|
||||
const [importingDatabase, showImportModal] = useState<boolean>(false);
|
||||
const [passwordFields, setPasswordFields] = useState<string[]>([]);
|
||||
const [preparingExport, setPreparingExport] = useState<boolean>(false);
|
||||
const { roles } = useSelector<any, UserWithPermissionsAndRoles>(
|
||||
state => state.user,
|
||||
@ -116,20 +102,6 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
|
||||
ALLOWED_EXTENSIONS,
|
||||
} = useSelector<any, ExtentionConfigs>(state => state.common.conf);
|
||||
|
||||
const openDatabaseImportModal = () => {
|
||||
showImportModal(true);
|
||||
};
|
||||
|
||||
const closeDatabaseImportModal = () => {
|
||||
showImportModal(false);
|
||||
};
|
||||
|
||||
const handleDatabaseImport = () => {
|
||||
showImportModal(false);
|
||||
refreshData();
|
||||
addSuccessToast(t('Database imported'));
|
||||
};
|
||||
|
||||
const openDatabaseDeleteModal = (database: DatabaseObject) =>
|
||||
SupersetClient.get({
|
||||
endpoint: `/api/v1/database/${database.id}/related_objects/`,
|
||||
@ -245,22 +217,6 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT)) {
|
||||
menuData.buttons.push({
|
||||
name: (
|
||||
<Tooltip
|
||||
id="import-tooltip"
|
||||
title={t('Import databases')}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Icons.Import data-test="import-button" />
|
||||
</Tooltip>
|
||||
),
|
||||
buttonStyle: 'link',
|
||||
onClick: openDatabaseImportModal,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleDatabaseExport(database: DatabaseObject) {
|
||||
@ -526,19 +482,6 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
|
||||
pageSize={PAGE_SIZE}
|
||||
/>
|
||||
|
||||
<ImportModelsModal
|
||||
resourceName="database"
|
||||
resourceLabel={t('database')}
|
||||
passwordsNeededMessage={PASSWORDS_NEEDED_MESSAGE}
|
||||
confirmOverwriteMessage={CONFIRM_OVERWRITE_MESSAGE}
|
||||
addDangerToast={addDangerToast}
|
||||
addSuccessToast={addSuccessToast}
|
||||
onModelImport={handleDatabaseImport}
|
||||
show={importingDatabase}
|
||||
onHide={closeDatabaseImportModal}
|
||||
passwordFields={passwordFields}
|
||||
setPasswordFields={setPasswordFields}
|
||||
/>
|
||||
{preparingExport && <Loading />}
|
||||
</>
|
||||
);
|
||||
|
@ -19,6 +19,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { getDatabaseDocumentationLinks } from 'src/views/CRUD/hooks';
|
||||
import { UploadFile } from 'antd/lib/upload/interface';
|
||||
import {
|
||||
EditHeaderTitle,
|
||||
EditHeaderSubtitle,
|
||||
@ -52,6 +53,7 @@ const documentationLink = (engine: string | undefined) => {
|
||||
}
|
||||
return irregularDocumentationLinks[engine];
|
||||
};
|
||||
|
||||
const ModalHeader = ({
|
||||
isLoading,
|
||||
isEditMode,
|
||||
@ -61,6 +63,7 @@ const ModalHeader = ({
|
||||
dbName,
|
||||
dbModel,
|
||||
editNewDb,
|
||||
fileList,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
isEditMode: boolean;
|
||||
@ -70,13 +73,19 @@ const ModalHeader = ({
|
||||
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"> STEP 2 OF 2 </p>
|
||||
@ -94,6 +103,7 @@ const ModalHeader = ({
|
||||
</p>
|
||||
</StyledFormHeader>
|
||||
);
|
||||
|
||||
const hasConnectedDbHeader = (
|
||||
<StyledStickyHeader>
|
||||
<StyledFormHeader>
|
||||
@ -115,6 +125,7 @@ const ModalHeader = ({
|
||||
</StyledFormHeader>
|
||||
</StyledStickyHeader>
|
||||
);
|
||||
|
||||
const hasDbHeader = (
|
||||
<StyledStickyHeader>
|
||||
<StyledFormHeader>
|
||||
@ -133,6 +144,7 @@ const ModalHeader = ({
|
||||
</StyledFormHeader>
|
||||
</StyledStickyHeader>
|
||||
);
|
||||
|
||||
const noDbHeader = (
|
||||
<StyledFormHeader>
|
||||
<div className="select-db">
|
||||
@ -142,19 +154,23 @@ const ModalHeader = ({
|
||||
</StyledFormHeader>
|
||||
);
|
||||
|
||||
const importDbHeader = (
|
||||
<StyledStickyHeader>
|
||||
<StyledFormHeader>
|
||||
<p className="helper-top"> STEP 2 OF 2 </p>
|
||||
<h4>Enter the required {dbModel.name} credentials</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;
|
||||
}
|
||||
if (isEditMode) return isEditHeader;
|
||||
if (useSqlAlchemyForm) return useSqlAlchemyFormHeader;
|
||||
if (hasConnectedDb && !editNewDb) return hasConnectedDbHeader;
|
||||
if (db || editNewDb) return hasDbHeader;
|
||||
|
||||
return noDbHeader;
|
||||
};
|
||||
|
||||
|
@ -1028,7 +1028,24 @@ describe('DatabaseModal', () => {
|
||||
*/
|
||||
});
|
||||
});
|
||||
|
||||
describe('Import database flow', () => {
|
||||
it('imports a file', () => {
|
||||
const importDbButton = screen.getByTestId('import-database-btn');
|
||||
expect(importDbButton).toBeVisible();
|
||||
|
||||
const testFile = new File([new ArrayBuffer(1)], 'model_export.zip');
|
||||
|
||||
userEvent.click(importDbButton);
|
||||
userEvent.upload(importDbButton, testFile);
|
||||
|
||||
expect(importDbButton.files[0]).toStrictEqual(testFile);
|
||||
expect(importDbButton.files.item(0)).toStrictEqual(testFile);
|
||||
expect(importDbButton.files).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DatabaseModal w/ Deeplinking Engine', () => {
|
||||
const renderAndWait = async () => {
|
||||
const mounted = act(async () => {
|
||||
|
@ -25,18 +25,21 @@ import {
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useReducer,
|
||||
Reducer,
|
||||
} from 'react';
|
||||
import { UploadChangeParam, UploadFile } from 'antd/lib/upload/interface';
|
||||
import Tabs from 'src/components/Tabs';
|
||||
import { AntdSelect } from 'src/components';
|
||||
import { AntdSelect, Upload } from 'src/components';
|
||||
import Alert from 'src/components/Alert';
|
||||
import Modal from 'src/components/Modal';
|
||||
import Button from 'src/components/Button';
|
||||
import IconButton from 'src/components/IconButton';
|
||||
import InfoTooltip from 'src/components/InfoTooltip';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import ValidatedInput from 'src/components/Form/LabeledErrorBoundInput';
|
||||
import {
|
||||
testDatabaseConnection,
|
||||
useSingleViewResource,
|
||||
@ -44,6 +47,7 @@ import {
|
||||
useDatabaseValidation,
|
||||
getDatabaseImages,
|
||||
getConnectionAlert,
|
||||
useImportResource,
|
||||
} from 'src/views/CRUD/hooks';
|
||||
import { useCommonConf } from 'src/views/CRUD/data/database/state';
|
||||
import {
|
||||
@ -59,11 +63,13 @@ import DatabaseConnectionForm from './DatabaseConnectionForm';
|
||||
import {
|
||||
antDErrorAlertStyles,
|
||||
antDAlertStyles,
|
||||
antdWarningAlertStyles,
|
||||
StyledAlertMargin,
|
||||
antDModalNoPaddingStyles,
|
||||
antDModalStyles,
|
||||
antDTabsStyles,
|
||||
buttonLinkStyles,
|
||||
importDbButtonLinkStyles,
|
||||
alchemyButtonLinkStyles,
|
||||
TabHeader,
|
||||
formHelperStyles,
|
||||
@ -73,6 +79,8 @@ import {
|
||||
infoTooltip,
|
||||
StyledFooterButton,
|
||||
StyledStickyHeader,
|
||||
formScrollableStyles,
|
||||
StyledUploadWrapper,
|
||||
} from './styles';
|
||||
import ModalHeader, { DOCUMENTATION_LINK } from './ModalHeader';
|
||||
|
||||
@ -402,10 +410,12 @@ function dbReducer(
|
||||
return {
|
||||
...action.payload,
|
||||
};
|
||||
|
||||
case ActionType.configMethodChange:
|
||||
return {
|
||||
...action.payload,
|
||||
};
|
||||
|
||||
case ActionType.reset:
|
||||
default:
|
||||
return null;
|
||||
@ -436,27 +446,6 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
const [db, setDB] = useReducer<
|
||||
Reducer<Partial<DatabaseObject> | null, DBReducerActionType>
|
||||
>(dbReducer, null);
|
||||
const [tabKey, setTabKey] = useState<string>(DEFAULT_TAB_KEY);
|
||||
const [availableDbs, getAvailableDbs] = useAvailableDatabases();
|
||||
const [validationErrors, getValidation, setValidationErrors] =
|
||||
useDatabaseValidation();
|
||||
const [hasConnectedDb, setHasConnectedDb] = useState<boolean>(false);
|
||||
const [dbName, setDbName] = useState('');
|
||||
const [editNewDb, setEditNewDb] = useState<boolean>(false);
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
const [testInProgress, setTestInProgress] = useState<boolean>(false);
|
||||
const conf = useCommonConf();
|
||||
const dbImages = getDatabaseImages();
|
||||
const connectionAlert = getConnectionAlert();
|
||||
const isEditMode = !!databaseId;
|
||||
const sslForced = isFeatureEnabled(
|
||||
FeatureFlag.FORCE_DATABASE_CONNECTIONS_SSL,
|
||||
);
|
||||
const hasAlert =
|
||||
connectionAlert || !!(db?.engine && engineSpecificAlertMapping[db.engine]);
|
||||
const useSqlAlchemyForm =
|
||||
db?.configuration_method === CONFIGURATION_METHOD.SQLALCHEMY_URI;
|
||||
const useTabLayout = isEditMode || useSqlAlchemyForm;
|
||||
// Database fetch logic
|
||||
const {
|
||||
state: { loading: dbLoading, resource: dbFetched, error: dbErrors },
|
||||
@ -469,6 +458,33 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
t('database'),
|
||||
addDangerToast,
|
||||
);
|
||||
|
||||
const [tabKey, setTabKey] = useState<string>(DEFAULT_TAB_KEY);
|
||||
const [availableDbs, getAvailableDbs] = useAvailableDatabases();
|
||||
const [validationErrors, getValidation, setValidationErrors] =
|
||||
useDatabaseValidation();
|
||||
const [hasConnectedDb, setHasConnectedDb] = useState<boolean>(false);
|
||||
const [dbName, setDbName] = useState('');
|
||||
const [editNewDb, setEditNewDb] = useState<boolean>(false);
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
const [testInProgress, setTestInProgress] = useState<boolean>(false);
|
||||
const [passwords, setPasswords] = useState<Record<string, string>>({});
|
||||
const [confirmedOverwrite, setConfirmedOverwrite] = useState<boolean>(false);
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
const [importingModal, setImportingModal] = useState<boolean>(false);
|
||||
|
||||
const conf = useCommonConf();
|
||||
const dbImages = getDatabaseImages();
|
||||
const connectionAlert = getConnectionAlert();
|
||||
const isEditMode = !!databaseId;
|
||||
const sslForced = isFeatureEnabled(
|
||||
FeatureFlag.FORCE_DATABASE_CONNECTIONS_SSL,
|
||||
);
|
||||
const hasAlert =
|
||||
connectionAlert || !!(db?.engine && engineSpecificAlertMapping[db.engine]);
|
||||
const useSqlAlchemyForm =
|
||||
db?.configuration_method === CONFIGURATION_METHOD.SQLALCHEMY_URI;
|
||||
const useTabLayout = isEditMode || useSqlAlchemyForm;
|
||||
const isDynamic = (engine: string | undefined) =>
|
||||
availableDbs?.databases?.find(
|
||||
(DB: DatabaseObject) => DB.backend === engine || DB.engine === engine,
|
||||
@ -513,14 +529,43 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const removeFile = (removedFile: UploadFile) => {
|
||||
setFileList(fileList.filter(file => file.uid !== removedFile.uid));
|
||||
return false;
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
setDB({ type: ActionType.reset });
|
||||
setHasConnectedDb(false);
|
||||
setValidationErrors(null); // reset validation errors on close
|
||||
clearError();
|
||||
setEditNewDb(false);
|
||||
setFileList([]);
|
||||
setImportingModal(false);
|
||||
setPasswords({});
|
||||
setConfirmedOverwrite(false);
|
||||
if (onDatabaseAdd) onDatabaseAdd();
|
||||
onHide();
|
||||
};
|
||||
|
||||
// Database import logic
|
||||
const {
|
||||
state: {
|
||||
alreadyExists,
|
||||
passwordsNeeded,
|
||||
loading: importLoading,
|
||||
failed: importErrored,
|
||||
},
|
||||
importResource,
|
||||
} = useImportResource('database', t('database'), msg => {
|
||||
addDangerToast(msg);
|
||||
onClose();
|
||||
});
|
||||
|
||||
const onChange = (type: any, payload: any) => {
|
||||
setDB({ type, payload } as DBReducerActionType);
|
||||
};
|
||||
|
||||
const onSave = async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { id, ...update } = db || {};
|
||||
@ -596,9 +641,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
dbToUpdate.configuration_method === CONFIGURATION_METHOD.DYNAMIC_FORM, // onShow toast on SQLA Forms
|
||||
);
|
||||
if (result) {
|
||||
if (onDatabaseAdd) {
|
||||
onDatabaseAdd();
|
||||
}
|
||||
if (onDatabaseAdd) onDatabaseAdd();
|
||||
if (!editNewDb) {
|
||||
onClose();
|
||||
addSuccessToast(t('Database settings updated'));
|
||||
@ -613,9 +656,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
);
|
||||
if (dbId) {
|
||||
setHasConnectedDb(true);
|
||||
if (onDatabaseAdd) {
|
||||
onDatabaseAdd();
|
||||
}
|
||||
if (onDatabaseAdd) onDatabaseAdd();
|
||||
if (useTabLayout) {
|
||||
// tab layout only has one step
|
||||
// so it should close immediately on save
|
||||
@ -624,14 +665,29 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Import - doesn't use db state
|
||||
if (!db) {
|
||||
setLoading(true);
|
||||
setImportingModal(true);
|
||||
|
||||
if (!(fileList[0].originFileObj instanceof File)) return;
|
||||
const dbId = await importResource(
|
||||
fileList[0].originFileObj,
|
||||
passwords,
|
||||
confirmedOverwrite,
|
||||
);
|
||||
|
||||
if (dbId) {
|
||||
onClose();
|
||||
addSuccessToast(t('Database connected'));
|
||||
}
|
||||
}
|
||||
|
||||
setEditNewDb(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const onChange = (type: any, payload: any) => {
|
||||
setDB({ type, payload } as DBReducerActionType);
|
||||
};
|
||||
|
||||
// Initialize
|
||||
const fetchDB = () => {
|
||||
if (isEditMode && databaseId) {
|
||||
@ -773,10 +829,20 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
};
|
||||
|
||||
const handleBackButtonOnConnect = () => {
|
||||
if (editNewDb) {
|
||||
setHasConnectedDb(false);
|
||||
}
|
||||
if (editNewDb) setHasConnectedDb(false);
|
||||
if (importingModal) setImportingModal(false);
|
||||
setDB({ type: ActionType.reset });
|
||||
setFileList([]);
|
||||
};
|
||||
|
||||
const handleDisableOnImport = () => {
|
||||
if (
|
||||
importLoading ||
|
||||
(alreadyExists.length && !confirmedOverwrite) ||
|
||||
(passwordsNeeded.length && JSON.stringify(passwords) === '{}')
|
||||
)
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
const renderModalFooter = () => {
|
||||
@ -815,6 +881,26 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Import doesn't use db state, so footer will not render in the if statement above
|
||||
if (importingModal) {
|
||||
return (
|
||||
<>
|
||||
<StyledFooterButton key="back" onClick={handleBackButtonOnConnect}>
|
||||
{t('Back')}
|
||||
</StyledFooterButton>
|
||||
<StyledFooterButton
|
||||
key="submit"
|
||||
buttonStyle="primary"
|
||||
onClick={onSave}
|
||||
disabled={handleDisableOnImport()}
|
||||
>
|
||||
{t('Connect')}
|
||||
</StyledFooterButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
@ -840,6 +926,28 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
</StyledFooterButton>
|
||||
</>
|
||||
);
|
||||
|
||||
const firstUpdate = useRef(true); // Captures first render
|
||||
// Only runs when importing files don't need user input
|
||||
useEffect(() => {
|
||||
// Will not run on first render
|
||||
if (firstUpdate.current) {
|
||||
firstUpdate.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!importLoading &&
|
||||
!alreadyExists.length &&
|
||||
!passwordsNeeded.length &&
|
||||
!isLoading && // This prevents a double toast for non-related imports
|
||||
!importErrored // This prevents a success toast on error
|
||||
) {
|
||||
onClose();
|
||||
addSuccessToast(t('Database connected'));
|
||||
}
|
||||
}, [alreadyExists, passwordsNeeded, importLoading, importErrored]);
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
setTabKey(DEFAULT_TAB_KEY);
|
||||
@ -874,19 +982,111 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
}
|
||||
}, [availableDbs]);
|
||||
|
||||
const tabChange = (key: string) => {
|
||||
setTabKey(key);
|
||||
// This forces the modal to scroll until the importing filename is in view
|
||||
useEffect(() => {
|
||||
if (importingModal) {
|
||||
document
|
||||
.getElementsByClassName('ant-upload-list-item-name')[0]
|
||||
.scrollIntoView();
|
||||
}
|
||||
}, [importingModal]);
|
||||
|
||||
const onDbImport = async (info: UploadChangeParam) => {
|
||||
setImportingModal(true);
|
||||
setFileList([
|
||||
{
|
||||
...info.file,
|
||||
status: 'done',
|
||||
},
|
||||
]);
|
||||
|
||||
if (!(info.file.originFileObj instanceof File)) return;
|
||||
await importResource(
|
||||
info.file.originFileObj,
|
||||
passwords,
|
||||
confirmedOverwrite,
|
||||
);
|
||||
};
|
||||
|
||||
const passwordNeededField = () => {
|
||||
if (!passwordsNeeded.length) return null;
|
||||
|
||||
return passwordsNeeded.map(database => (
|
||||
<>
|
||||
<StyledAlertMargin>
|
||||
<Alert
|
||||
closable={false}
|
||||
css={(theme: SupersetTheme) => antDAlertStyles(theme)}
|
||||
type="info"
|
||||
showIcon
|
||||
message="Database passwords"
|
||||
description={t(
|
||||
`The passwords for the databases below are needed in order to import them. Please note that the "Secure Extra" and "Certificate" sections of the database configuration are not present in explore files and should be added manually after the import if they are needed.`,
|
||||
)}
|
||||
/>
|
||||
</StyledAlertMargin>
|
||||
<ValidatedInput
|
||||
id="password_needed"
|
||||
name="password_needed"
|
||||
required
|
||||
value={passwords[database]}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setPasswords({ ...passwords, [database]: event.target.value })
|
||||
}
|
||||
validationMethods={{ onBlur: () => {} }}
|
||||
errorMessage={validationErrors?.password_needed}
|
||||
label={t(`${database.slice(10)} PASSWORD`)}
|
||||
css={formScrollableStyles}
|
||||
/>
|
||||
</>
|
||||
));
|
||||
};
|
||||
|
||||
const confirmOverwrite = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const targetValue = (event.currentTarget?.value as string) ?? '';
|
||||
setConfirmedOverwrite(targetValue.toUpperCase() === t('OVERWRITE'));
|
||||
};
|
||||
|
||||
const confirmOverwriteField = () => {
|
||||
if (!alreadyExists.length) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledAlertMargin>
|
||||
<Alert
|
||||
closable={false}
|
||||
css={(theme: SupersetTheme) => antdWarningAlertStyles(theme)}
|
||||
type="warning"
|
||||
showIcon
|
||||
message=""
|
||||
description={t(
|
||||
'You are importing one or more databases that already exist. Overwriting might cause you to lose some of your work. Are you sure you want to overwrite?',
|
||||
)}
|
||||
/>
|
||||
</StyledAlertMargin>
|
||||
<ValidatedInput
|
||||
id="confirm_overwrite"
|
||||
name="confirm_overwrite"
|
||||
required
|
||||
validationMethods={{ onBlur: () => {} }}
|
||||
errorMessage={validationErrors?.confirm_overwrite}
|
||||
label={t(`TYPE "OVERWRITE" TO CONFIRM`)}
|
||||
onChange={confirmOverwrite}
|
||||
css={formScrollableStyles}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const tabChange = (key: string) => setTabKey(key);
|
||||
|
||||
const renderStepTwoAlert = () => {
|
||||
const { hostname } = window.location;
|
||||
let ipAlert = connectionAlert?.REGIONAL_IPS?.default || '';
|
||||
const regionalIPs = connectionAlert?.REGIONAL_IPS || {};
|
||||
Object.entries(regionalIPs).forEach(([ipRegion, ipRange]) => {
|
||||
const regex = new RegExp(ipRegion);
|
||||
if (hostname.match(regex)) {
|
||||
ipAlert = ipRange;
|
||||
}
|
||||
if (hostname.match(regex)) ipAlert = ipRange;
|
||||
});
|
||||
return (
|
||||
db?.engine && (
|
||||
@ -1027,6 +1227,41 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
if (fileList.length > 0 && (alreadyExists.length || passwordsNeeded.length)) {
|
||||
return (
|
||||
<Modal
|
||||
css={(theme: SupersetTheme) => [
|
||||
antDModalNoPaddingStyles,
|
||||
antDModalStyles(theme),
|
||||
formHelperStyles(theme),
|
||||
formStyles(theme),
|
||||
]}
|
||||
name="database"
|
||||
onHandledPrimaryAction={onSave}
|
||||
onHide={onClose}
|
||||
primaryButtonName={t('Connect')}
|
||||
width="500px"
|
||||
centered
|
||||
show={show}
|
||||
title={<h4>{t('Connect a database')}</h4>}
|
||||
footer={renderModalFooter()}
|
||||
>
|
||||
<ModalHeader
|
||||
isLoading={isLoading}
|
||||
isEditMode={isEditMode}
|
||||
useSqlAlchemyForm={useSqlAlchemyForm}
|
||||
hasConnectedDb={hasConnectedDb}
|
||||
db={db}
|
||||
dbName={dbName}
|
||||
dbModel={dbModel}
|
||||
fileList={fileList}
|
||||
/>
|
||||
{passwordNeededField()}
|
||||
{confirmOverwriteField()}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
return useTabLayout ? (
|
||||
<Modal
|
||||
css={(theme: SupersetTheme) => [
|
||||
@ -1266,6 +1501,26 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
/>
|
||||
{renderPreferredSelector()}
|
||||
{renderAvailableSelector()}
|
||||
<StyledUploadWrapper>
|
||||
<Upload
|
||||
name="databaseFile"
|
||||
id="databaseFile"
|
||||
data-test="database-file-input"
|
||||
accept=".yaml,.json,.yml,.zip"
|
||||
customRequest={() => {}}
|
||||
onChange={onDbImport}
|
||||
onRemove={removeFile}
|
||||
>
|
||||
<Button
|
||||
data-test="import-database-btn"
|
||||
buttonStyle="link"
|
||||
type="link"
|
||||
css={importDbButtonLinkStyles}
|
||||
>
|
||||
{t('Import database from file')}
|
||||
</Button>
|
||||
</Upload>
|
||||
</StyledUploadWrapper>
|
||||
</SelectDatabaseStyles>
|
||||
) : (
|
||||
<>
|
||||
|
@ -218,6 +218,29 @@ export const antDErrorAlertStyles = (theme: SupersetTheme) => css`
|
||||
}
|
||||
`;
|
||||
|
||||
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;
|
||||
@ -399,6 +422,13 @@ export const buttonLinkStyles = (theme: SupersetTheme) => css`
|
||||
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;
|
||||
@ -583,3 +613,13 @@ export const StyledCatalogTable = styled.div`
|
||||
width: 95%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledUploadWrapper = styled.div`
|
||||
.ant-progress-inner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-upload-list-item-card-actions {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
@ -381,6 +381,7 @@ interface ImportResourceState {
|
||||
loading: boolean;
|
||||
passwordsNeeded: string[];
|
||||
alreadyExists: string[];
|
||||
failed: boolean;
|
||||
}
|
||||
|
||||
export function useImportResource(
|
||||
@ -392,6 +393,7 @@ export function useImportResource(
|
||||
loading: false,
|
||||
passwordsNeeded: [],
|
||||
alreadyExists: [],
|
||||
failed: false,
|
||||
});
|
||||
|
||||
function updateState(update: Partial<ImportResourceState>) {
|
||||
@ -407,6 +409,7 @@ export function useImportResource(
|
||||
// Set loading state
|
||||
updateState({
|
||||
loading: true,
|
||||
failed: false,
|
||||
});
|
||||
|
||||
const formData = new FormData();
|
||||
@ -430,9 +433,19 @@ export function useImportResource(
|
||||
body: formData,
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
.then(() => true)
|
||||
.then(() => {
|
||||
updateState({
|
||||
passwordsNeeded: [],
|
||||
alreadyExists: [],
|
||||
failed: false,
|
||||
});
|
||||
return true;
|
||||
})
|
||||
.catch(response =>
|
||||
getClientErrorObject(response).then(error => {
|
||||
updateState({
|
||||
failed: true,
|
||||
});
|
||||
if (!error.errors) {
|
||||
handleErrorMsg(
|
||||
t(
|
||||
|
Loading…
Reference in New Issue
Block a user