This is an automated email from the ASF dual-hosted git repository.
beto pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 36bd6d8 feat: FE: Import for Queries II (#14091)
36bd6d8 is described below
commit 36bd6d83038b1d07079987c7c8dc24946cb95d5f
Author: Lyndsi Kay Williams <[email protected]>
AuthorDate: Wed Apr 14 18:02:35 2021 -0500
feat: FE: Import for Queries II (#14091)
* Copied changes over
* Tests passing
* Import testing complete
---
.../CRUD/data/savedquery/SavedQueryList_spec.jsx | 73 +++++++++++++++++++++-
.../views/CRUD/data/savedquery/SavedQueryList.tsx | 52 +++++++++++++++
superset-frontend/src/views/CRUD/types.ts | 7 ++-
superset-frontend/src/views/CRUD/utils.tsx | 1 +
4 files changed, 130 insertions(+), 3 deletions(-)
diff --git
a/superset-frontend/spec/javascripts/views/CRUD/data/savedquery/SavedQueryList_spec.jsx
b/superset-frontend/spec/javascripts/views/CRUD/data/savedquery/SavedQueryList_spec.jsx
index ec8b80e..019c8c3 100644
---
a/superset-frontend/spec/javascripts/views/CRUD/data/savedquery/SavedQueryList_spec.jsx
+++
b/superset-frontend/spec/javascripts/views/CRUD/data/savedquery/SavedQueryList_spec.jsx
@@ -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);
+ });
});
diff --git
a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx
b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx
index fe0312e..9f90bf5 100644
--- a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx
+++ b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx
@@ -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}
+ />
</>
);
}
diff --git a/superset-frontend/src/views/CRUD/types.ts
b/superset-frontend/src/views/CRUD/types.ts
index 6d369d8..a312c40 100644
--- a/superset-frontend/src/views/CRUD/types.ts
+++ b/superset-frontend/src/views/CRUD/types.ts
@@ -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;
diff --git a/superset-frontend/src/views/CRUD/utils.tsx
b/superset-frontend/src/views/CRUD/utils.tsx
index 4c56386..4096a96 100644
--- a/superset-frontend/src/views/CRUD/utils.tsx
+++ b/superset-frontend/src/views/CRUD/utils.tsx
@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
+
import {
t,
SupersetClient,