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..8e4321114194 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/get_top_nav_config.test.tsx @@ -0,0 +1,299 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { WizardServices } from '../../types'; +import { + addWizardDirectly, + formulateSavedWizardVis, + getTopNavConfig, + TopNavConfigParams, + validateSuccessfulSave, +} from './get_top_nav_config'; +import { createWizardServicesMock } from './mocks'; +import { ApplicationStart, IToasts } from '../../../../../core/public'; +import { EmbeddableStateTransfer } from '../../../../embeddable/public'; + +describe('getTopNavConfig', () => { + let savedWizardVis: any; + let mockServices: jest.Mocked; + let initialTopNavConfig: TopNavConfigParams; + + beforeEach(() => { + mockServices = createWizardServicesMock(); + savedWizardVis = { + id: '1', + title: 'save wizard wiz title', + description: '', + visualizationState: '', + styleState: '', + version: 0, + copyOnSave: true, + searchSourceFields: {}, + }; + }); + + describe('topNavConfig', () => { + beforeEach(() => { + initialTopNavConfig = { + visualizationIdFromUrl: '', + savedWizardVis, + visualizationState: { + searchField: '', + }, + styleState: {}, + saveDisabledReason: 'invalid field', + dispatch: jest.fn(), + }; + }); + + test('topNavConfig fields', async () => { + const returnResult = getTopNavConfig(initialTopNavConfig, mockServices); + const topNavConfig = returnResult[0]; + expect(topNavConfig.id).toBe('save'); + expect(topNavConfig.iconType).toBe('save'); + expect(topNavConfig.emphasize).toBeFalsy(); + expect(topNavConfig.description).toBe('Save Visualization'); + expect(topNavConfig.className).toBe('saveButton'); + expect(topNavConfig.label).toBe('save'); + expect(topNavConfig.testId).toBe('wizardSaveButton'); + expect(topNavConfig.disableButton).toBeTruthy(); + expect(topNavConfig.tooltip).toBe('invalid field'); + }); + + test('true emphasize', async () => { + initialTopNavConfig.savedWizardVis.id = undefined; + const returnResult = getTopNavConfig(initialTopNavConfig, mockServices); + const topNavConfig = returnResult[0]; + expect(topNavConfig.emphasize).toBeTruthy(); + }); + + test('disableButton', async () => { + initialTopNavConfig.saveDisabledReason = undefined; + const returnResult = getTopNavConfig(initialTopNavConfig, mockServices); + const topNavConfig = returnResult[0]; + expect(topNavConfig.disableButton).toBeFalsy(); + expect(topNavConfig.tooltip).toBeUndefined(); + }); + }); + + describe('formulateSavedWizardVis', () => { + let indexPatterns: any; + let visualizationState: any; + let styleState: any; + let newTitle: string; + let newDescription: string; + let newCopyOnSave: boolean; + + beforeEach(() => { + indexPatterns = mockServices.data; + visualizationState = {}; + styleState = {}; + newTitle = 'new title'; + newDescription = 'new description'; + newCopyOnSave = false; + }); + test('return null', async () => { + savedWizardVis = null; + const result = await formulateSavedWizardVis( + savedWizardVis, + indexPatterns, + visualizationState, + styleState, + newTitle, + newDescription, + newCopyOnSave + ); + expect(result).toBeUndefined(); + }); + + test('return true newlyCreated with null id', async () => { + savedWizardVis.id = null; + const result = await formulateSavedWizardVis( + savedWizardVis, + indexPatterns, + visualizationState, + styleState, + newTitle, + newDescription, + newCopyOnSave + ); + expect(result?.newlyCreated).toBeTruthy(); + }); + + test('return true newlyCreated with true copyOnSave', async () => { + newCopyOnSave = true; + const result = await formulateSavedWizardVis( + savedWizardVis, + indexPatterns, + visualizationState, + styleState, + newTitle, + newDescription, + newCopyOnSave + ); + expect(result?.newlyCreated).toBeTruthy(); + }); + + test('expect savedWizardVis has been formulated correctly', async () => { + const result = await formulateSavedWizardVis( + savedWizardVis, + indexPatterns, + visualizationState, + styleState, + newTitle, + newDescription, + newCopyOnSave + ); + expect(typeof savedWizardVis.visualizationState).toBe('string'); + expect(typeof savedWizardVis.styleState).toBe('string'); + expect(savedWizardVis.title).toBe('new title'); + expect(savedWizardVis.description).toBe('new description'); + expect(savedWizardVis.copyOnSave).toBeFalsy(); + expect(result?.currentTitle).toBe('save wizard wiz title'); + }); + }); + + describe('validateSuccessfulSave', () => { + let originatingApp: string | undefined; + let returnToOrigin: boolean; + let newlyCreated: boolean; + let stateTransfer: EmbeddableStateTransfer; + let id: string | undefined; + let application: ApplicationStart; + let toastNotifications: IToasts; + let dispatch: any; + let visualizationIdFromUrl: string | undefined; + let currentTitle: string; + let history: any; + + beforeEach(() => { + stateTransfer = mockServices.embeddable.getStateTransfer(); + application = mockServices.application; + id = '1'; + toastNotifications = mockServices.toastNotifications; + dispatch = jest.fn(); + history = mockServices.history; + }); + + test('create and add a new wizard to dashboard', () => { + originatingApp = 'dashboard'; + returnToOrigin = true; + newlyCreated = true; + const returnResult = addWizardDirectly( + originatingApp, + returnToOrigin, + newlyCreated, + stateTransfer, + id, + application + ); + expect(stateTransfer.navigateToWithEmbeddablePackage).toBeCalledTimes(1); + expect(returnResult?.skipDispatch).toBe(true); + expect(returnResult?.setOriginatingAppUndefined).toBe(false); + }); + test('edit an existing wizard from the dashboard', () => { + originatingApp = 'dashboard'; + returnToOrigin = true; + newlyCreated = false; + const returnResult = addWizardDirectly( + originatingApp, + returnToOrigin, + newlyCreated, + stateTransfer, + id, + application + ); + expect(stateTransfer.navigateToWithEmbeddablePackage).toBeCalledTimes(0); + expect(application.navigateToApp).toBeCalledWith('dashboard'); + expect(returnResult).toBeUndefined(); + }); + test('create wizard from visualization page', () => { + originatingApp = 'visualization'; + returnToOrigin = false; + newlyCreated = true; + const returnResult = addWizardDirectly( + originatingApp, + returnToOrigin, + newlyCreated, + stateTransfer, + id, + application + ); + + expect(returnResult?.skipDispatch).toBeFalsy(); + expect(returnResult?.setOriginatingAppUndefined).toBeTruthy(); + }); + test('reset title if save not successful', () => { + id = undefined; + const returnResult = validateSuccessfulSave( + id, + toastNotifications, + savedWizardVis, + originatingApp, + returnToOrigin, + newlyCreated, + stateTransfer, + application, + visualizationIdFromUrl, + history, + dispatch, + currentTitle + ); + + expect(savedWizardVis.title).toBe(currentTitle); + expect(returnResult).toBeUndefined(); + }); + + test('skip dispatch when creating directly from dashboard', () => { + originatingApp = 'dashboard'; + returnToOrigin = true; + newlyCreated = true; + id = '1'; + const returnResult = validateSuccessfulSave( + id, + toastNotifications, + savedWizardVis, + originatingApp, + returnToOrigin, + newlyCreated, + stateTransfer, + application, + visualizationIdFromUrl, + history, + dispatch, + currentTitle + ); + + expect(dispatch).toBeCalledTimes(0); + expect(returnResult).toBeFalsy(); + }); + + test('push history when id does not equal visualizatonIdFromUrl', () => { + originatingApp = 'visualization'; + returnToOrigin = false; + newlyCreated = true; + id = '1'; + visualizationIdFromUrl = '2'; + const returnResult = validateSuccessfulSave( + id, + toastNotifications, + savedWizardVis, + originatingApp, + returnToOrigin, + newlyCreated, + stateTransfer, + application, + visualizationIdFromUrl, + history, + dispatch, + currentTitle + ); + + expect(history.push).toBeCalled(); + expect(dispatch).toBeCalledTimes(1); + expect(returnResult).toBeTruthy(); + }); + }); +}); 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 dc3f5770f84d..f6099b9c6f14 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,8 @@ import { WizardVisSavedObject } from '../../types'; import { StyleState, VisualizationState, AppDispatch } from './state_management'; import { EDIT_PATH } from '../../../common'; import { setEditorState } from './state_management/metadata_slice'; -interface TopNavConfigParams { +import { LeftNav } from '../components/left_nav'; +export interface TopNavConfigParams { visualizationIdFromUrl: string; savedWizardVis: WizardVisSavedObject; visualizationState: VisualizationState; @@ -66,10 +67,11 @@ export const getTopNavConfig = ( i18n: { Context: I18nContext }, data: { indexPatterns }, embeddable, - scopedHistory + scopedHistory, }: WizardServices ) => { - const { originatingApp: originatingApp } = embeddable + let { originatingApp: originatingApp } = + embeddable .getStateTransfer(scopedHistory) .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {}; const stateTransfer = embeddable.getStateTransfer(); @@ -98,24 +100,18 @@ export const getTopNavConfig = ( newDescription, returnToOrigin, }: OnSaveProps & { returnToOrigin: boolean }) => { - if (!savedWizardVis) { - return; - } - const newlyCreated = !Boolean(savedWizardVis.id) || savedWizardVis.copyOnSave; - const currentTitle = savedWizardVis.title; - const indexPattern = await indexPatterns.get(visualizationState.indexPattern || ''); - savedWizardVis.searchSourceFields = { - index: indexPattern, - }; - const vizStateWithoutIndex = { - searchField: visualizationState.searchField, - activeVisualization: visualizationState.activeVisualization, - }; - savedWizardVis.visualizationState = JSON.stringify(vizStateWithoutIndex); - savedWizardVis.styleState = JSON.stringify(styleState); - savedWizardVis.title = newTitle; - savedWizardVis.description = newDescription; - savedWizardVis.copyOnSave = newCopyOnSave; + const savedWizardResult = await formulateSavedWizardVis( + savedWizardVis, + indexPatterns, + visualizationState, + styleState, + newTitle, + newDescription, + newCopyOnSave + ); + + const newlyCreated = savedWizardResult?.newlyCreated; + const currentTitle = savedWizardResult?.currentTitle; try { const id = await savedWizardVis.save({ @@ -125,51 +121,22 @@ export const getTopNavConfig = ( 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; - } + const setOriginatingAppUndefined = validateSuccessfulSave( + id, + toastNotifications, + savedWizardVis, + originatingApp, + returnToOrigin, + newlyCreated, + stateTransfer, + application, + visualizationIdFromUrl, + history, + dispatch, + currentTitle + ); + + originatingApp = setOriginatingAppUndefined ? undefined : originatingApp; // 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 }; @@ -212,3 +179,122 @@ export const getTopNavConfig = ( return topNavConfig; }; + +export const formulateSavedWizardVis = async ( + savedWizardVis, + indexPatterns, + visualizationState, + styleState, + newTitle, + newDescription, + newCopyOnSave +) => { + if (!savedWizardVis) { + return; + } + const newlyCreated = !Boolean(savedWizardVis.id) || savedWizardVis.copyOnSave; + const currentTitle = savedWizardVis.title; + const indexPattern = await indexPatterns.get(visualizationState.indexPattern || ''); + savedWizardVis.searchSourceFields = { + index: indexPattern, + }; + const vizStateWithoutIndex = { + searchField: visualizationState.searchField, + activeVisualization: visualizationState.activeVisualization, + }; + savedWizardVis.visualizationState = JSON.stringify(vizStateWithoutIndex); + savedWizardVis.styleState = JSON.stringify(styleState); + savedWizardVis.title = newTitle; + savedWizardVis.description = newDescription; + savedWizardVis.copyOnSave = newCopyOnSave; + + return { + newlyCreated, + currentTitle, + }; +}; + +export const validateSuccessfulSave = ( + id, + toastNotifications, + savedWizardVis, + originatingApp, + returnToOrigin, + newlyCreated, + stateTransfer, + application, + visualizationIdFromUrl, + history, + dispatch, + currentTitle +) => { + if (id) { + toastNotifications.addSuccess({ + title: i18n.translate('wizard.topNavMenu.saveVisualization.successNotificationText', { + defaultMessage: `Saved '{visTitle}'`, + values: { + visTitle: savedWizardVis.title, + }, + }), + 'data-test-subj': 'saveVisualizationSuccess', + }); + + const addToDashboardResult = addWizardDirectly( + originatingApp, + returnToOrigin, + newlyCreated, + stateTransfer, + id, + application + ); + + if (!addToDashboardResult?.skipDispatch) { + // Update URL + if (id !== visualizationIdFromUrl) { + history.push({ + ...history.location, + pathname: `${EDIT_PATH}/${id}`, + }); + } + dispatch(setEditorState({ state: 'clean' })); + } + return addToDashboardResult?.setOriginatingAppUndefined; + } else { + // reset title if save not successful + savedWizardVis.title = currentTitle; + } +}; + +export const addWizardDirectly = ( + originatingApp, + returnToOrigin, + newlyCreated, + stateTransfer, + id, + application +) => { + 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 { + skipDispatch: true, + setOriginatingAppUndefined: false, + }; + } 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) { + return { + skipDispatch: false, + setOriginatingAppUndefined: true, + }; + } + } +}; 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..451217fa8657 --- /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 indexPatternMock = dataPluginMock.createStartContract().indexPatterns; + const toastNotifications = coreStartMock.notifications.toasts; + const applicationMock = coreStartMock.application; + const i18nContextMock = coreStartMock.i18n.Context; + 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; +};