From 9f88aaa4a3b6537d2cea33036017d25bb806841e Mon Sep 17 00:00:00 2001 From: abbyhu2000 Date: Tue, 27 Sep 2022 23:01:39 +0000 Subject: [PATCH] Move onsave out from getTopNavConfig function and added unit tests Signed-off-by: abbyhu2000 --- config/opensearch_dashboards.yml | 2 +- .../utils/get_top_nav_config.test.tsx | 155 +++++++++++++ .../application/utils/get_top_nav_config.tsx | 213 ++++++++++-------- .../wizard/public/application/utils/mocks.ts | 46 ++++ 4 files changed, 317 insertions(+), 99 deletions(-) create mode 100644 src/plugins/wizard/public/application/utils/get_top_nav_config.test.tsx create mode 100644 src/plugins/wizard/public/application/utils/mocks.ts diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 69bd26c18d20..7c3c8332fc06 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -222,4 +222,4 @@ # Set the value of this setting to true to start exploring wizard # functionality in Visualization. -# wizard.enabled: false + wizard.enabled: true diff --git a/src/plugins/wizard/public/application/utils/get_top_nav_config.test.tsx b/src/plugins/wizard/public/application/utils/get_top_nav_config.test.tsx new file mode 100644 index 000000000000..456297d99a59 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/get_top_nav_config.test.tsx @@ -0,0 +1,155 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { WizardServices } from '../../types'; +import { getOnSave } from './get_top_nav_config'; +import { createWizardServicesMock } from './mocks'; + +describe('getOnSave', () => { + let savedWizardVis: any; + let originatingApp: string | undefined; + let visualizationIdFromUrl: string; + let dispatch: any; + let mockServices: jest.Mocked; + let onSaveProps: { + newTitle: string; + newCopyOnSave: boolean; + isTitleDuplicateConfirmed: boolean; + onTitleDuplicate: any; + newDescription: string; + returnToOrigin: boolean; + }; + + beforeEach(() => { + savedWizardVis = { + id: '1', + title: 'save wizard wiz title', + description: '', + visualizationState: '', + styleState: '', + version: 0, + copyOnSave: true, + searchSourceFields: {}, + save: jest.fn().mockReturnValue('1'), + }; + originatingApp = ''; + visualizationIdFromUrl = ''; + dispatch = jest.fn(); + mockServices = createWizardServicesMock(); + + onSaveProps = { + newTitle: 'new title', + newCopyOnSave: false, + isTitleDuplicateConfirmed: false, + onTitleDuplicate: jest.fn(), + newDescription: 'new description', + returnToOrigin: true, + }; + }); + + test('return undefined when savedWizardVis is null', async () => { + savedWizardVis = null; + const onSave = getOnSave( + savedWizardVis, + originatingApp, + visualizationIdFromUrl, + dispatch, + mockServices + ); + const onSaveResult = await onSave(onSaveProps); + + expect(onSaveResult).toBeUndefined(); + }); + + test('savedWizardVis get saved correctly', async () => { + const onSave = getOnSave( + savedWizardVis, + originatingApp, + visualizationIdFromUrl, + dispatch, + mockServices + ); + const onSaveReturn = await onSave(onSaveProps); + expect(savedWizardVis).toMatchInlineSnapshot(` + Object { + "copyOnSave": false, + "description": "new description", + "id": "1", + "save": [MockFunction] { + "calls": Array [ + Array [ + Object { + "confirmOverwrite": false, + "isTitleDuplicateConfirmed": false, + "onTitleDuplicate": [MockFunction], + "returnToOrigin": true, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": "1", + }, + ], + }, + "searchSourceFields": Object {}, + "styleState": "", + "title": "new title", + "version": 0, + "visualizationState": "", + } + `); + expect(onSaveReturn?.id).toBe('1'); + }); + + test('savedWizardVis does not change title with a null id', async () => { + savedWizardVis.save = jest.fn().mockReturnValue(null); + const onSave = getOnSave( + savedWizardVis, + originatingApp, + visualizationIdFromUrl, + dispatch, + mockServices + ); + const onSaveResult = await onSave(onSaveProps); + expect(savedWizardVis.title).toBe('save wizard wiz title'); + expect(onSaveResult?.id).toBeNull(); + }); + + test('create a new wizard from dashboard', async () => { + savedWizardVis.id = null; + savedWizardVis.save = jest.fn().mockReturnValue('2'); + originatingApp = 'dashboard'; + onSaveProps.returnToOrigin = true; + + const onSave = getOnSave( + savedWizardVis, + originatingApp, + visualizationIdFromUrl, + dispatch, + mockServices + ); + const onSaveResult = await onSave(onSaveProps); + expect(onSaveResult?.id).toBe('2'); + expect(dispatch).toBeCalledTimes(0); + }); + + test('edit an exising wizard from dashboard', async () => { + savedWizardVis.copyOnSave = false; + originatingApp = 'dashboard'; + onSaveProps.returnToOrigin = true; + const onSave = getOnSave( + savedWizardVis, + originatingApp, + visualizationIdFromUrl, + dispatch, + mockServices + ); + const onSaveResult = await onSave(onSaveProps); + expect(onSaveResult?.id).toBe('1'); + expect(mockServices.application.navigateToApp).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx b/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx index 99e02cda6f25..529d91f76f86 100644 --- a/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx @@ -41,7 +41,7 @@ import { WizardVisSavedObject } from '../../types'; import { AppDispatch } from './state_management'; import { EDIT_PATH } from '../../../common'; import { setEditorState } from './state_management/metadata_slice'; -interface TopNavConfigParams { +export interface TopNavConfigParams { visualizationIdFromUrl: string; savedWizardVis: WizardVisSavedObject; saveDisabledReason?: string; @@ -50,9 +50,16 @@ interface TopNavConfigParams { export const getTopNavConfig = ( { visualizationIdFromUrl, savedWizardVis, saveDisabledReason, dispatch }: TopNavConfigParams, - { application, history, toastNotifications, i18n: { Context: I18nContext }, embeddable, scopedHistory }: WizardServices + services: WizardServices ) => { - const { originatingApp: originatingApp } = embeddable + const { + i18n: { Context: I18nContext }, + embeddable, + scopedHistory, + } = services; + + const { originatingApp: originatingApp } = + embeddable .getStateTransfer(scopedHistory) .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {}; const stateTransfer = embeddable.getStateTransfer(); @@ -73,104 +80,16 @@ export const getTopNavConfig = ( disableButton: !!saveDisabledReason, tooltip: saveDisabledReason, run: (_anchorElement) => { - const onSave = async ({ - newTitle, - newCopyOnSave, - isTitleDuplicateConfirmed, - onTitleDuplicate, - newDescription, - returnToOrigin, - }: OnSaveProps & { returnToOrigin: boolean }) => { - if (!savedWizardVis) { - return; - } - const newlyCreated = !Boolean(savedWizardVis.id) || savedWizardVis.copyOnSave; - const currentTitle = savedWizardVis.title; - savedWizardVis.title = newTitle; - savedWizardVis.description = newDescription; - savedWizardVis.copyOnSave = newCopyOnSave; - - try { - const id = await savedWizardVis.save({ - confirmOverwrite: false, - isTitleDuplicateConfirmed, - onTitleDuplicate, - returnToOrigin, - }); - - if (id) { - toastNotifications.addSuccess({ - title: i18n.translate( - 'wizard.topNavMenu.saveVisualization.successNotificationText', - { - defaultMessage: `Saved '{visTitle}'`, - values: { - visTitle: savedWizardVis.title, - }, - } - ), - 'data-test-subj': 'saveVisualizationSuccess', - }); - - if (originatingApp && returnToOrigin) { - // create or edit wizard directly from a dashboard - if (newlyCreated && stateTransfer) { - // create and add a new wizard to the dashboard - stateTransfer.navigateToWithEmbeddablePackage(originatingApp, { - state: { type: 'wizard', input: { savedObjectId: id } }, - }); - return {id}; - } else { - // edit an existing wizard from the dashboard - application.navigateToApp(originatingApp); - } - } else { - // create wizard from creating visualization page, not related to any dashboard - if ( originatingApp && newlyCreated) { - //setOriginatingApp(undefined); - } - } - - // Update URL - if (id !== visualizationIdFromUrl) { - history.push({ - ...history.location, - pathname: `${EDIT_PATH}/${id}`, - }); - } - dispatch(setEditorState({ state: 'clean' })); - } else { - // reset title if save not successful - savedWizardVis.title = currentTitle; - } - - // Even if id='', which it will be for a duplicate title warning, we still want to return it, to avoid closing the modal - return { id }; - } catch (error: any) { - // eslint-disable-next-line no-console - console.error(error); - - toastNotifications.addDanger({ - title: i18n.translate('wizard.topNavMenu.saveVisualization.failureNotificationText', { - defaultMessage: `Error on saving '{visTitle}'`, - values: { - visTitle: newTitle, - }, - }), - text: error.message, - 'data-test-subj': 'saveVisualizationError', - }); - - // reset title if save not successful - savedWizardVis.title = currentTitle; - return { error }; - } - }; - const saveModal = ( {}} originatingApp={originatingApp} @@ -185,3 +104,101 @@ export const getTopNavConfig = ( return topNavConfig; }; + +export const getOnSave = ( + savedWizardVis, + originatingApp, + visualizationIdFromUrl, + dispatch, + services +) => { + const onSave = async ({ + newTitle, + newCopyOnSave, + isTitleDuplicateConfirmed, + onTitleDuplicate, + newDescription, + returnToOrigin, + }: OnSaveProps & { returnToOrigin: boolean }) => { + const { embeddable, toastNotifications, application, history } = services; + const stateTransfer = embeddable.getStateTransfer(); + + if (!savedWizardVis) { + return; + } + const newlyCreated = !Boolean(savedWizardVis.id) || savedWizardVis.copyOnSave; + const currentTitle = savedWizardVis.title; + savedWizardVis.title = newTitle; + savedWizardVis.description = newDescription; + savedWizardVis.copyOnSave = newCopyOnSave; + + try { + const id = await savedWizardVis.save({ + confirmOverwrite: false, + isTitleDuplicateConfirmed, + onTitleDuplicate, + returnToOrigin, + }); + + if (id) { + toastNotifications.addSuccess({ + title: i18n.translate('wizard.topNavMenu.saveVisualization.successNotificationText', { + defaultMessage: `Saved '{visTitle}'`, + values: { + visTitle: savedWizardVis.title, + }, + }), + 'data-test-subj': 'saveVisualizationSuccess', + }); + + if (originatingApp && returnToOrigin) { + // create or edit wizard directly from a dashboard + if (newlyCreated && stateTransfer) { + // create and add a new wizard to the dashboard + stateTransfer.navigateToWithEmbeddablePackage(originatingApp, { + state: { type: 'wizard', input: { savedObjectId: id } }, + }); + return { id }; + } else { + // edit an existing wizard from the dashboard + application.navigateToApp(originatingApp); + } + } + + // Update URL + if (id !== visualizationIdFromUrl) { + history.push({ + ...history.location, + pathname: `${EDIT_PATH}/${id}`, + }); + } + dispatch(setEditorState({ state: 'clean' })); + } else { + // reset title if save not successful + savedWizardVis.title = currentTitle; + } + + // Even if id='', which it will be for a duplicate title warning, we still want to return it, to avoid closing the modal + return { id }; + } catch (error: any) { + // eslint-disable-next-line no-console + console.error(error); + + toastNotifications.addDanger({ + title: i18n.translate('wizard.topNavMenu.saveVisualization.failureNotificationText', { + defaultMessage: `Error on saving '{visTitle}'`, + values: { + visTitle: newTitle, + }, + }), + text: error.message, + 'data-test-subj': 'saveVisualizationError', + }); + + // reset title if save not successful + savedWizardVis.title = currentTitle; + return { error }; + } + }; + return onSave; +}; diff --git a/src/plugins/wizard/public/application/utils/mocks.ts b/src/plugins/wizard/public/application/utils/mocks.ts new file mode 100644 index 000000000000..574531bf6adc --- /dev/null +++ b/src/plugins/wizard/public/application/utils/mocks.ts @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ScopedHistory } from '../../../../../core/public'; +import { coreMock, scopedHistoryMock } from '../../../../../core/public/mocks'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { embeddablePluginMock } from '../../../../embeddable/public/mocks'; +import { expressionsPluginMock } from '../../../../expressions/public/mocks'; +import { navigationPluginMock } from '../../../../navigation/public/mocks'; +import { WizardServices } from '../../types'; + +export const createWizardServicesMock = () => { + const coreStartMock = coreMock.createStart(); + const toastNotifications = coreStartMock.notifications.toasts; + const applicationMock = coreStartMock.application; + const i18nContextMock = coreStartMock.i18n.Context; + const indexPatternMock = dataPluginMock.createStartContract().indexPatterns; + const embeddableMock = embeddablePluginMock.createStartContract(); + const scopedhistoryMock = (scopedHistoryMock.create() as unknown) as ScopedHistory; + const navigationMock = navigationPluginMock.createStartContract(); + const expressionMock = expressionsPluginMock.createStartContract(); + + const wizardServicesMock = { + ...coreStartMock, + navigation: navigationMock, + expression: expressionMock, + savedWizardLoader: { + get: jest.fn(), + } as any, + setHeaderActionMenu: () => {}, + applicationMock, + history: { + push: jest.fn(), + location: { pathname: '' }, + }, + toastNotifications, + i18n: i18nContextMock, + data: indexPatternMock, + embeddable: embeddableMock, + scopedHistory: scopedhistoryMock, + }; + + return (wizardServicesMock as unknown) as jest.Mocked; +};