Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: FE: Import for Queries II #14091

Merged
merged 5 commits into from
Apr 14, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { render, screen, cleanup } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { QueryParamProvider } from 'use-query-params';
import { act } from 'react-dom/test-utils';
import { handleBulkSavedQueryExport } from 'src/views/CRUD/utils';
import * as featureFlags from 'src/featureFlags';
import SavedQueryList from 'src/views/CRUD/data/savedquery/SavedQueryList';
import SubMenu from 'src/components/Menu/SubMenu';
Expand Down Expand Up @@ -74,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'],
});
Expand All @@ -95,6 +132,9 @@ fetchMock.get(queriesDistinctEndpoint, {
result: [],
});

// Mock utils module
jest.mock('src/views/CRUD/utils');

describe('SavedQueryList', () => {
const wrapper = mount(
<Provider store={store}>
Expand Down Expand Up @@ -233,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
Expand All @@ -245,4 +285,45 @@ describe('RTL', () => {
});
expect(exportTooltip).toBeInTheDocument();
});

it('runs handleBulkSavedQueryExport when export is clicked', () => {
// Grab Export action button and mock mouse clicking it
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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
createFetchRelated,
createFetchDistinct,
createErrorHandler,
handleBulkDashboardExport,
handleBulkSavedQueryExport,
} from 'src/views/CRUD/utils';
import Popover from 'src/components/Popover';
import withToasts from 'src/messageToasts/enhancers/withToasts';
Expand All @@ -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;
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -306,7 +344,7 @@ function SavedQueryList({
};
const handleEdit = () => openInSqlLab(original.id);
const handleCopy = () => copyQueryLink(original.id);
const handleExport = () => handleBulkDashboardExport([original]);
const handleExport = () => handleBulkSavedQueryExport([original]);
const handleDelete = () => setQueryCurrentlyDeleting(original);

const actions = [
Expand Down Expand Up @@ -454,7 +492,7 @@ function SavedQueryList({
key: 'export',
name: t('Export'),
type: 'primary',
onSelect: handleBulkDashboardExport,
onSelect: handleBulkSavedQueryExport,
});
}
return (
Expand All @@ -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}
/>
</>
);
}
Expand Down
7 changes: 6 additions & 1 deletion superset-frontend/src/views/CRUD/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 12 additions & 1 deletion superset-frontend/src/views/CRUD/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/

import {
t,
SupersetClient,
Expand All @@ -27,7 +28,7 @@ import Chart from 'src/types/Chart';
import rison from 'rison';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
import { FetchDataConfig } from 'src/components/ListView';
import { Dashboard, Filters } from './types';
import { Dashboard, Filters, SavedQueryObject } from './types';

const createFetchResourceMethod = (method: string) => (
resource: string,
Expand Down Expand Up @@ -218,6 +219,16 @@ export function handleBulkDashboardExport(dashboardsToExport: Dashboard[]) {
);
}

export function handleBulkSavedQueryExport(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and some of the changes above have already been merged, no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I just pulled the changed and there was a small merge conflict but I just fixed it and have everything merged and up to date. So far so good!

savedQueriesToExport: SavedQueryObject[],
) {
return window.location.assign(
`/api/v1/saved_query/export/?q=${rison.encode(
savedQueriesToExport.map(({ id }) => id),
)}`,
);
}

export function handleDashboardDelete(
{ id, dashboard_title: dashboardTitle }: Dashboard,
refreshData: (config?: FetchDataConfig | null) => void,
Expand Down