feat: FE: Import for Queries II (#14091)
* Copied changes over * Tests passing * Import testing complete
This commit is contained in:
parent
21f973f0bd
commit
36bd6d8303
|
|
@ -75,6 +75,42 @@ const mockqueries = [...new Array(3)].map((_, i) => ({
|
|||
],
|
||||
}));
|
||||
|
||||
// ---------- For import testing ----------
|
||||
// Create an one more mocked query than the original mocked query array
|
||||
const mockOneMoreQuery = [...new Array(mockqueries.length + 1)].map((_, i) => ({
|
||||
created_by: {
|
||||
id: i,
|
||||
first_name: `user`,
|
||||
last_name: `${i}`,
|
||||
},
|
||||
created_on: `${i}-2020`,
|
||||
database: {
|
||||
database_name: `db ${i}`,
|
||||
id: i,
|
||||
},
|
||||
changed_on_delta_humanized: '1 day ago',
|
||||
db_id: i,
|
||||
description: `SQL for ${i}`,
|
||||
id: i,
|
||||
label: `query ${i}`,
|
||||
schema: 'public',
|
||||
sql: `SELECT ${i} FROM table`,
|
||||
sql_tables: [
|
||||
{
|
||||
catalog: null,
|
||||
schema: null,
|
||||
table: `${i}`,
|
||||
},
|
||||
],
|
||||
}));
|
||||
// Grab the last mocked query, to mock import
|
||||
const mockNewImportQuery = mockOneMoreQuery.pop();
|
||||
// Create a new file out of mocked import query to mock upload
|
||||
const mockImportFile = new File(
|
||||
[mockNewImportQuery],
|
||||
'saved_query_import_mock.json',
|
||||
);
|
||||
|
||||
fetchMock.get(queriesInfoEndpoint, {
|
||||
permissions: ['can_write', 'can_read'],
|
||||
});
|
||||
|
|
@ -237,7 +273,7 @@ describe('RTL', () => {
|
|||
|
||||
it('renders an export button in the actions bar', async () => {
|
||||
// Grab Export action button and mock mouse hovering over it
|
||||
const exportActionButton = screen.getAllByRole('button')[17];
|
||||
const exportActionButton = screen.getAllByRole('button')[18];
|
||||
userEvent.hover(exportActionButton);
|
||||
|
||||
// Wait for the tooltip to pop up
|
||||
|
|
@ -252,9 +288,42 @@ describe('RTL', () => {
|
|||
|
||||
it('runs handleBulkSavedQueryExport when export is clicked', () => {
|
||||
// Grab Export action button and mock mouse clicking it
|
||||
const exportActionButton = screen.getAllByRole('button')[17];
|
||||
const exportActionButton = screen.getAllByRole('button')[18];
|
||||
userEvent.click(exportActionButton);
|
||||
|
||||
expect(handleBulkSavedQueryExport).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders an import button in the submenu', () => {
|
||||
// Grab and assert that import saved query button is visible
|
||||
const importSavedQueryButton = screen.getAllByRole('button')[2];
|
||||
expect(importSavedQueryButton).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders an import model when import button is clicked', async () => {
|
||||
// Grab and click import saved query button to reveal modal
|
||||
const importSavedQueryButton = screen.getAllByRole('button')[2];
|
||||
userEvent.click(importSavedQueryButton);
|
||||
|
||||
// Grab and assert that saved query import modal's heading is visible
|
||||
const importSavedQueryModalHeading = screen.getByRole('heading', {
|
||||
name: /import saved query/i,
|
||||
});
|
||||
expect(importSavedQueryModalHeading).toBeVisible();
|
||||
});
|
||||
|
||||
it('imports a saved query', () => {
|
||||
// Grab and click import saved query button to reveal modal
|
||||
const importSavedQueryButton = screen.getAllByRole('button')[2];
|
||||
userEvent.click(importSavedQueryButton);
|
||||
|
||||
// Grab "Choose File" input from import modal
|
||||
const chooseFileInput = screen.getByLabelText(/file\*/i);
|
||||
// Upload mocked import file
|
||||
userEvent.upload(chooseFileInput, mockImportFile);
|
||||
|
||||
expect(chooseFileInput.files[0]).toStrictEqual(mockImportFile);
|
||||
expect(chooseFileInput.files.item(0)).toStrictEqual(mockImportFile);
|
||||
expect(chooseFileInput.files).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,9 +42,23 @@ import { commonMenuData } from 'src/views/CRUD/data/common';
|
|||
import { SavedQueryObject } from 'src/views/CRUD/types';
|
||||
import copyTextToClipboard from 'src/utils/copy';
|
||||
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
||||
import ImportModelsModal from 'src/components/ImportModal/index';
|
||||
import Icons from 'src/components/Icons';
|
||||
import SavedQueryPreviewModal from './SavedQueryPreviewModal';
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
const PASSWORDS_NEEDED_MESSAGE = t(
|
||||
'The passwords for the databases below are needed in order to ' +
|
||||
'import them together with the saved queries. 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 saved queries that already exist. ' +
|
||||
'Overwriting might cause you to lose some of your work. Are you ' +
|
||||
'sure you want to overwrite?',
|
||||
);
|
||||
|
||||
interface SavedQueryListProps {
|
||||
addDangerToast: (msg: string) => void;
|
||||
|
|
@ -96,6 +110,21 @@ function SavedQueryList({
|
|||
savedQueryCurrentlyPreviewing,
|
||||
setSavedQueryCurrentlyPreviewing,
|
||||
] = useState<SavedQueryObject | null>(null);
|
||||
const [importingSavedQuery, showImportModal] = useState<boolean>(false);
|
||||
const [passwordFields, setPasswordFields] = useState<string[]>([]);
|
||||
|
||||
const openSavedQueryImportModal = () => {
|
||||
showImportModal(true);
|
||||
};
|
||||
|
||||
const closeSavedQueryImportModal = () => {
|
||||
showImportModal(false);
|
||||
};
|
||||
|
||||
const handleSavedQueryImport = () => {
|
||||
showImportModal(false);
|
||||
refreshData();
|
||||
};
|
||||
|
||||
const canEdit = hasPerm('can_write');
|
||||
const canDelete = hasPerm('can_write');
|
||||
|
|
@ -149,6 +178,15 @@ function SavedQueryList({
|
|||
buttonStyle: 'primary',
|
||||
});
|
||||
|
||||
if (isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT)) {
|
||||
subMenuButtons.push({
|
||||
name: <Icons.Import />,
|
||||
buttonStyle: 'link',
|
||||
onClick: openSavedQueryImportModal,
|
||||
'data-test': 'import-button',
|
||||
});
|
||||
}
|
||||
|
||||
menuData.buttons = subMenuButtons;
|
||||
|
||||
// Action methods
|
||||
|
|
@ -476,6 +514,20 @@ function SavedQueryList({
|
|||
);
|
||||
}}
|
||||
</ConfirmStatusChange>
|
||||
|
||||
<ImportModelsModal
|
||||
resourceName="saved_query"
|
||||
resourceLabel={t('saved query')}
|
||||
passwordsNeededMessage={PASSWORDS_NEEDED_MESSAGE}
|
||||
confirmOverwriteMessage={CONFIRM_OVERWRITE_MESSAGE}
|
||||
addDangerToast={addDangerToast}
|
||||
addSuccessToast={addSuccessToast}
|
||||
onModelImport={handleSavedQueryImport}
|
||||
show={importingSavedQuery}
|
||||
onHide={closeSavedQueryImportModal}
|
||||
passwordFields={passwordFields}
|
||||
setPasswordFields={setPasswordFields}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,7 +123,12 @@ export enum QueryObjectColumns {
|
|||
tracking_url = 'tracking_url',
|
||||
}
|
||||
|
||||
export type ImportResourceName = 'chart' | 'dashboard' | 'database' | 'dataset';
|
||||
export type ImportResourceName =
|
||||
| 'chart'
|
||||
| 'dashboard'
|
||||
| 'database'
|
||||
| 'dataset'
|
||||
| 'saved_query';
|
||||
|
||||
export type DatabaseObject = {
|
||||
allow_run_async?: boolean;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
t,
|
||||
SupersetClient,
|
||||
|
|
|
|||
Loading…
Reference in New Issue