From dd04578e48e948ea4e900e508fd4950dd6305f6f Mon Sep 17 00:00:00 2001 From: lyndsiWilliams Date: Mon, 12 Apr 2021 17:25:21 -0500 Subject: [PATCH 1/3] Copied changes over --- .../data/savedquery/SavedQueryList_spec.jsx | 32 +++++++++- .../CRUD/data/savedquery/SavedQueryList.tsx | 58 ++++++++++++++++++- superset-frontend/src/views/CRUD/types.ts | 7 ++- superset-frontend/src/views/CRUD/utils.tsx | 13 ++++- 4 files changed, 104 insertions(+), 6 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 e1e5f6e10bf67..02170d22e29af 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 @@ -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'; @@ -95,6 +96,9 @@ fetchMock.get(queriesDistinctEndpoint, { result: [], }); +// Mock utils module +jest.mock('src/views/CRUD/utils'); + describe('SavedQueryList', () => { const wrapper = mount( @@ -233,7 +237,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 @@ -245,4 +249,30 @@ 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')[17]; + 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(); + }); }); diff --git a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx index 352aa8eec3f10..1b83c63ab4c2e 100644 --- a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx +++ b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx @@ -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'; @@ -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(null); + const [importingSavedQuery, showImportModal] = useState(false); + const [passwordFields, setPasswordFields] = useState([]); + + 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: , + buttonStyle: 'link', + onClick: openSavedQueryImportModal, + 'data-test': 'import-button', + }); + } + menuData.buttons = subMenuButtons; // Action methods @@ -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 = [ @@ -454,7 +492,7 @@ function SavedQueryList({ key: 'export', name: t('Export'), type: 'primary', - onSelect: handleBulkDashboardExport, + onSelect: handleBulkSavedQueryExport, }); } return ( @@ -476,6 +514,20 @@ function SavedQueryList({ ); }} + + ); } diff --git a/superset-frontend/src/views/CRUD/types.ts b/superset-frontend/src/views/CRUD/types.ts index 6d369d864c871..a312c40004589 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 e70416149f70c..4096a96534be5 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, @@ -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, @@ -218,6 +219,16 @@ export function handleBulkDashboardExport(dashboardsToExport: Dashboard[]) { ); } +export function handleBulkSavedQueryExport( + 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, From 4b74b2c2f4c7aa77508580a20dc8430911b7f381 Mon Sep 17 00:00:00 2001 From: lyndsiWilliams Date: Mon, 12 Apr 2021 17:45:27 -0500 Subject: [PATCH 2/3] Tests passing --- .../views/CRUD/data/savedquery/SavedQueryList_spec.jsx | 2 +- .../src/views/CRUD/data/savedquery/SavedQueryList.tsx | 2 +- 2 files changed, 2 insertions(+), 2 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 02170d22e29af..2304c0ab8d59e 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 @@ -252,7 +252,7 @@ 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(); diff --git a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx index 1b83c63ab4c2e..9f90bf559be0e 100644 --- a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx +++ b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx @@ -517,7 +517,7 @@ function SavedQueryList({ Date: Tue, 13 Apr 2021 09:30:19 -0500 Subject: [PATCH 3/3] Import testing complete --- .../data/savedquery/SavedQueryList_spec.jsx | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) 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 2304c0ab8d59e..019c8c3fa8cee 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'], }); @@ -275,4 +311,19 @@ describe('RTL', () => { }); 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); + }); });