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,

Reply via email to