From d5cb31afe02ddd66ccd275ec93e2af9fb1f9d957 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 4 Oct 2021 10:03:17 -0400 Subject: [PATCH] [Lens] move from slice to reducers/actions and simplify loading (#113324) (#113740) * structure changes * tests & fix for sessionId * share mocks in time_range_middleware * make switchVisualization and selectSuggestion one reducer as it's very similar * CR Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Marta Bondyra --- .../lens/public/app_plugin/app.test.tsx | 48 +- .../config_panel/config_panel.test.tsx | 129 ++---- .../config_panel/layer_actions.test.ts | 10 +- .../editor_frame/editor_frame.test.tsx | 32 +- .../editor_frame/editor_frame.tsx | 2 +- .../editor_frame/suggestion_helpers.test.ts | 60 +-- .../editor_frame/suggestion_helpers.ts | 29 +- .../editor_frame/suggestion_panel.test.tsx | 13 +- .../workspace_panel/chart_switch.test.tsx | 62 ++- .../workspace_panel/chart_switch.tsx | 2 +- .../workspace_panel/workspace_panel.test.tsx | 63 +-- .../workspace_panel/workspace_panel.tsx | 2 +- x-pack/plugins/lens/public/mocks.tsx | 114 +++-- .../__snapshots__/load_initial.test.tsx.snap | 4 +- .../lens/public/state_management/index.ts | 14 +- .../init_middleware/load_initial.test.tsx | 410 ------------------ .../init_middleware/load_initial.ts | 119 ++--- .../state_management/lens_slice.test.ts | 26 +- .../public/state_management/lens_slice.ts | 276 ++++++++---- .../state_management/load_initial.test.tsx | 323 ++++++++++++++ .../time_range_middleware.test.ts | 115 +---- 21 files changed, 845 insertions(+), 1008 deletions(-) rename x-pack/plugins/lens/public/state_management/{init_middleware => }/__snapshots__/load_initial.test.tsx.snap (94%) delete mode 100644 x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx create mode 100644 x-pack/plugins/lens/public/state_management/load_initial.test.tsx diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index d10fe42feb322..a2c7c67e1fc77 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -13,7 +13,13 @@ import { App } from './app'; import { LensAppProps, LensAppServices } from './types'; import { EditorFrameInstance, EditorFrameProps } from '../types'; import { Document } from '../persistence'; -import { visualizationMap, datasourceMap, makeDefaultServices, mountWithProvider } from '../mocks'; +import { + visualizationMap, + datasourceMap, + makeDefaultServices, + mountWithProvider, + mockStoreDeps, +} from '../mocks'; import { I18nProvider } from '@kbn/i18n/react'; import { SavedObjectSaveModal, @@ -92,9 +98,11 @@ describe('Lens App', () => { }; } + const makeDefaultServicesForApp = () => makeDefaultServices(sessionIdSubject, 'sessionId-1'); + async function mountWith({ props = makeDefaultProps(), - services = makeDefaultServices(sessionIdSubject), + services = makeDefaultServicesForApp(), preloadedState, }: { props?: jest.Mocked; @@ -110,11 +118,11 @@ describe('Lens App', () => { ); }; - + const storeDeps = mockStoreDeps({ lensServices: services }); const { instance, lensStore } = await mountWithProvider( , { - data: services.data, + storeDeps, preloadedState, }, { wrappingComponent } @@ -144,7 +152,7 @@ describe('Lens App', () => { }); it('updates global filters with store state', async () => { - const services = makeDefaultServices(sessionIdSubject); + const services = makeDefaultServicesForApp(); const indexPattern = { id: 'index1' } as unknown as IndexPattern; const pinnedField = { name: 'pinnedField' } as unknown as FieldSpec; const pinnedFilter = esFilters.buildExistsFilter(pinnedField, indexPattern); @@ -216,7 +224,7 @@ describe('Lens App', () => { it('sets originatingApp breadcrumb when the document title changes', async () => { const props = makeDefaultProps(); - const services = makeDefaultServices(sessionIdSubject); + const services = makeDefaultServicesForApp(); props.incomingState = { originatingApp: 'coolContainer' }; services.getOriginatingAppName = jest.fn(() => 'The Coolest Container Ever Made'); @@ -262,7 +270,7 @@ describe('Lens App', () => { describe('TopNavMenu#showDatePicker', () => { it('shows date picker if any used index pattern isTimeBased', async () => { - const customServices = makeDefaultServices(sessionIdSubject); + const customServices = makeDefaultServicesForApp(); customServices.data.indexPatterns.get = jest .fn() .mockImplementation((id) => @@ -275,7 +283,7 @@ describe('Lens App', () => { ); }); it('shows date picker if active datasource isTimeBased', async () => { - const customServices = makeDefaultServices(sessionIdSubject); + const customServices = makeDefaultServicesForApp(); customServices.data.indexPatterns.get = jest .fn() .mockImplementation((id) => @@ -290,7 +298,7 @@ describe('Lens App', () => { ); }); it('does not show date picker if index pattern nor active datasource is not time based', async () => { - const customServices = makeDefaultServices(sessionIdSubject); + const customServices = makeDefaultServicesForApp(); customServices.data.indexPatterns.get = jest .fn() .mockImplementation((id) => @@ -337,7 +345,7 @@ describe('Lens App', () => { ); }); it('handles rejected index pattern', async () => { - const customServices = makeDefaultServices(sessionIdSubject); + const customServices = makeDefaultServicesForApp(); customServices.data.indexPatterns.get = jest .fn() .mockImplementation((id) => Promise.reject({ reason: 'Could not locate that data view' })); @@ -385,7 +393,7 @@ describe('Lens App', () => { : undefined, }; - const services = makeDefaultServices(sessionIdSubject); + const services = makeDefaultServicesForApp(); services.attributeService.wrapAttributes = jest .fn() .mockImplementation(async ({ savedObjectId }) => ({ @@ -419,7 +427,7 @@ describe('Lens App', () => { } it('shows a disabled save button when the user does not have permissions', async () => { - const services = makeDefaultServices(sessionIdSubject); + const services = makeDefaultServicesForApp(); services.application = { ...services.application, capabilities: { @@ -469,7 +477,7 @@ describe('Lens App', () => { it('Shows Save and Return and Save As buttons in create by value mode with originating app', async () => { const props = makeDefaultProps(); - const services = makeDefaultServices(sessionIdSubject); + const services = makeDefaultServicesForApp(); services.dashboardFeatureFlag = { allowByValueEmbeddables: true }; props.incomingState = { originatingApp: 'ultraDashboard', @@ -618,7 +626,7 @@ describe('Lens App', () => { const mockedConsoleDir = jest.spyOn(console, 'dir'); // mocked console.dir to avoid messages in the console when running tests mockedConsoleDir.mockImplementation(() => {}); - const services = makeDefaultServices(sessionIdSubject); + const services = makeDefaultServicesForApp(); services.attributeService.wrapAttributes = jest .fn() .mockRejectedValue({ message: 'failed' }); @@ -692,7 +700,7 @@ describe('Lens App', () => { }); it('checks for duplicate title before saving', async () => { - const services = makeDefaultServices(sessionIdSubject); + const services = makeDefaultServicesForApp(); services.attributeService.wrapAttributes = jest .fn() .mockReturnValue(Promise.resolve({ savedObjectId: '123' })); @@ -759,7 +767,7 @@ describe('Lens App', () => { }); it('should still be enabled even if the user is missing save permissions', async () => { - const services = makeDefaultServices(sessionIdSubject); + const services = makeDefaultServicesForApp(); services.application = { ...services.application, capabilities: { @@ -799,7 +807,7 @@ describe('Lens App', () => { }); it('should open inspect panel', async () => { - const services = makeDefaultServices(sessionIdSubject); + const services = makeDefaultServicesForApp(); const { instance } = await mountWith({ services, preloadedState: { isSaveable: true } }); await runInspect(instance); @@ -943,7 +951,7 @@ describe('Lens App', () => { describe('saved query handling', () => { it('does not allow saving when the user is missing the saveQuery permission', async () => { - const services = makeDefaultServices(sessionIdSubject); + const services = makeDefaultServicesForApp(); services.application = { ...services.application, capabilities: { @@ -1136,7 +1144,7 @@ describe('Lens App', () => { it('updates the state if session id changes from the outside', async () => { const sessionIdS = new Subject(); - const services = makeDefaultServices(sessionIdS); + const services = makeDefaultServices(sessionIdS, 'sessionId-1'); const { lensStore } = await mountWith({ props: undefined, services }); act(() => { @@ -1180,7 +1188,7 @@ describe('Lens App', () => { }); it('does not confirm if the user is missing save permissions', async () => { - const services = makeDefaultServices(sessionIdSubject); + const services = makeDefaultServicesForApp(); services.application = { ...services.application, capabilities: { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx index 2668a31d70754..61d37d4cc9fed 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx @@ -7,12 +7,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { - createMockVisualization, - createMockFramePublicAPI, - createMockDatasource, - DatasourceMock, -} from '../../../mocks'; +import { createMockFramePublicAPI, visualizationMap, datasourceMap } from '../../../mocks'; import { Visualization } from '../../../types'; import { LayerPanels } from './config_panel'; import { LayerPanel } from './layer_panel'; @@ -43,32 +38,23 @@ afterEach(() => { }); describe('ConfigPanel', () => { - let mockVisualization: jest.Mocked; - let mockVisualization2: jest.Mocked; - let mockDatasource: DatasourceMock; const frame = createMockFramePublicAPI(); function getDefaultProps() { frame.datasourceLayers = { - first: mockDatasource.publicAPIMock, + first: datasourceMap.testDatasource.publicAPIMock, }; return { - activeVisualizationId: 'vis1', - visualizationMap: { - vis1: mockVisualization, - vis2: mockVisualization2, - }, - activeDatasourceId: 'mockindexpattern', - datasourceMap: { - mockindexpattern: mockDatasource, - }, + activeVisualizationId: 'testVis', + visualizationMap, + activeDatasourceId: 'testDatasource', + datasourceMap, activeVisualization: { - ...mockVisualization, + ...visualizationMap.testVis, getLayerIds: () => Object.keys(frame.datasourceLayers), - appendLayer: jest.fn(), } as unknown as Visualization, datasourceStates: { - mockindexpattern: { + testDatasource: { isLoading: false, state: 'state', }, @@ -85,38 +71,6 @@ describe('ConfigPanel', () => { }; } - beforeEach(() => { - mockVisualization = { - ...createMockVisualization(), - id: 'testVis', - visualizationTypes: [ - { - icon: 'empty', - id: 'testVis', - label: 'TEST1', - groupLabel: 'testVisGroup', - }, - ], - }; - - mockVisualization2 = { - ...createMockVisualization(), - - id: 'testVis2', - visualizationTypes: [ - { - icon: 'empty', - id: 'testVis2', - label: 'TEST2', - groupLabel: 'testVis2Group', - }, - ], - }; - - mockVisualization.getLayerIds.mockReturnValue(Object.keys(frame.datasourceLayers)); - mockDatasource = createMockDatasource('mockindexpattern'); - }); - // in what case is this test needed? it('should fail to render layerPanels if the public API is out of date', async () => { const props = getDefaultProps(); @@ -130,7 +84,7 @@ describe('ConfigPanel', () => { const { instance, lensStore } = await mountWithProvider(, { preloadedState: { datasourceStates: { - mockindexpattern: { + testDatasource: { isLoading: false, state: 'state', }, @@ -140,22 +94,22 @@ describe('ConfigPanel', () => { const { updateDatasource, updateAll } = instance.find(LayerPanel).props(); const updater = () => 'updated'; - updateDatasource('mockindexpattern', updater); + updateDatasource('testDatasource', updater); await waitMs(0); expect(lensStore.dispatch).toHaveBeenCalledTimes(1); expect( (lensStore.dispatch as jest.Mock).mock.calls[0][0].payload.updater( - props.datasourceStates.mockindexpattern.state + props.datasourceStates.testDatasource.state ) ).toEqual('updated'); - updateAll('mockindexpattern', updater, props.visualizationState); + updateAll('testDatasource', updater, props.visualizationState); // wait for one tick so async updater has a chance to trigger await waitMs(0); expect(lensStore.dispatch).toHaveBeenCalledTimes(2); expect( (lensStore.dispatch as jest.Mock).mock.calls[0][0].payload.updater( - props.datasourceStates.mockindexpattern.state + props.datasourceStates.testDatasource.state ) ).toEqual('updated'); }); @@ -167,7 +121,7 @@ describe('ConfigPanel', () => { { preloadedState: { datasourceStates: { - mockindexpattern: { + testDatasource: { isLoading: false, state: 'state', }, @@ -195,15 +149,15 @@ describe('ConfigPanel', () => { const defaultProps = getDefaultProps(); // overwriting datasourceLayers to test two layers frame.datasourceLayers = { - first: mockDatasource.publicAPIMock, - second: mockDatasource.publicAPIMock, + first: datasourceMap.testDatasource.publicAPIMock, + second: datasourceMap.testDatasource.publicAPIMock, }; const { instance } = await mountWithProvider( , { preloadedState: { datasourceStates: { - mockindexpattern: { + testDatasource: { isLoading: false, state: 'state', }, @@ -232,15 +186,15 @@ describe('ConfigPanel', () => { const defaultProps = getDefaultProps(); // overwriting datasourceLayers to test two layers frame.datasourceLayers = { - first: mockDatasource.publicAPIMock, - second: mockDatasource.publicAPIMock, + first: datasourceMap.testDatasource.publicAPIMock, + second: datasourceMap.testDatasource.publicAPIMock, }; const { instance } = await mountWithProvider( , { preloadedState: { datasourceStates: { - mockindexpattern: { + testDatasource: { isLoading: false, state: 'state', }, @@ -273,16 +227,16 @@ describe('ConfigPanel', () => { { preloadedState: { datasourceStates: { - mockindexpattern: { + testDatasource: { isLoading: false, state: 'state', }, }, - activeDatasourceId: 'mockindexpattern', + activeDatasourceId: 'testDatasource', }, dispatch: jest.fn((x) => { if (x.payload.subType === 'ADD_LAYER') { - frame.datasourceLayers.second = mockDatasource.publicAPIMock; + frame.datasourceLayers.second = datasourceMap.testDatasource.publicAPIMock; } }), }, @@ -303,16 +257,15 @@ describe('ConfigPanel', () => { (generateId as jest.Mock).mockReturnValue(`newId`); return mountWithProvider( , - { preloadedState: { datasourceStates: { - mockindexpattern: { + testDatasource: { isLoading: false, state: 'state', }, }, - activeDatasourceId: 'mockindexpattern', + activeDatasourceId: 'testDatasource', }, }, { @@ -352,13 +305,13 @@ describe('ConfigPanel', () => { label: 'Threshold layer', }, ]); - mockDatasource.initializeDimension = jest.fn(); + datasourceMap.testDatasource.initializeDimension = jest.fn(); const { instance, lensStore } = await prepareAndMountComponent(props); await clickToAddLayer(instance); expect(lensStore.dispatch).toHaveBeenCalledTimes(1); - expect(mockDatasource.initializeDimension).not.toHaveBeenCalled(); + expect(datasourceMap.testDatasource.initializeDimension).not.toHaveBeenCalled(); }); it('should not add an initial dimension when initialDimensions are not available for the given layer type', async () => { @@ -382,13 +335,13 @@ describe('ConfigPanel', () => { label: 'Threshold layer', }, ]); - mockDatasource.initializeDimension = jest.fn(); + datasourceMap.testDatasource.initializeDimension = jest.fn(); const { instance, lensStore } = await prepareAndMountComponent(props); await clickToAddLayer(instance); expect(lensStore.dispatch).toHaveBeenCalledTimes(1); - expect(mockDatasource.initializeDimension).not.toHaveBeenCalled(); + expect(datasourceMap.testDatasource.initializeDimension).not.toHaveBeenCalled(); }); it('should use group initial dimension value when adding a new layer if available', async () => { @@ -409,13 +362,13 @@ describe('ConfigPanel', () => { ], }, ]); - mockDatasource.initializeDimension = jest.fn(); + datasourceMap.testDatasource.initializeDimension = jest.fn(); const { instance, lensStore } = await prepareAndMountComponent(props); await clickToAddLayer(instance); expect(lensStore.dispatch).toHaveBeenCalledTimes(1); - expect(mockDatasource.initializeDimension).toHaveBeenCalledWith(undefined, 'newId', { + expect(datasourceMap.testDatasource.initializeDimension).toHaveBeenCalledWith({}, 'newId', { columnId: 'myColumn', dataType: 'number', groupId: 'testGroup', @@ -441,20 +394,24 @@ describe('ConfigPanel', () => { ], }, ]); - mockDatasource.initializeDimension = jest.fn(); + datasourceMap.testDatasource.initializeDimension = jest.fn(); const { instance, lensStore } = await prepareAndMountComponent(props); await clickToAddDimension(instance); expect(lensStore.dispatch).toHaveBeenCalledTimes(1); - expect(mockDatasource.initializeDimension).toHaveBeenCalledWith('state', 'first', { - groupId: 'a', - columnId: 'newId', - dataType: 'number', - label: 'Initial value', - staticValue: 100, - }); + expect(datasourceMap.testDatasource.initializeDimension).toHaveBeenCalledWith( + 'state', + 'first', + { + groupId: 'a', + columnId: 'newId', + dataType: 'number', + label: 'Initial value', + staticValue: 100, + } + ); }); }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts index 967e6e47c55f0..44cefb0bf8ec4 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts @@ -4,9 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { layerTypes } from '../../../../common'; -import { initialState } from '../../../state_management/lens_slice'; +import { LensAppState } from '../../../state_management/types'; import { removeLayer, appendLayer } from './layer_actions'; function createTestArgs(initialLayerIds: string[]) { @@ -44,15 +43,14 @@ function createTestArgs(initialLayerIds: string[]) { return { state: { - ...initialState, activeDatasourceId: 'ds1', datasourceStates, title: 'foo', visualization: { - activeId: 'vis1', + activeId: 'testVis', state: initialLayerIds, }, - }, + } as unknown as LensAppState, activeVisualization, datasourceMap: { ds1: testDatasource('ds1'), @@ -61,7 +59,7 @@ function createTestArgs(initialLayerIds: string[]) { trackUiEvent, stagedPreview: { visualization: { - activeId: 'vis1', + activeId: 'testVis', state: initialLayerIds, }, datasourceStates, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 4be9de78dedce..d289b69f4105e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -146,7 +146,6 @@ describe('editor_frame', () => { }; const lensStore = ( await mountWithProvider(, { - data: props.plugins.data, preloadedState: { activeDatasourceId: 'testDatasource', datasourceStates: { @@ -196,7 +195,6 @@ describe('editor_frame', () => { }; await mountWithProvider(, { - data: props.plugins.data, preloadedState: { visualization: { activeId: 'testVis', state: initialState }, }, @@ -228,7 +226,6 @@ describe('editor_frame', () => { }; instance = ( await mountWithProvider(, { - data: props.plugins.data, preloadedState: { visualization: { activeId: 'testVis', state: {} }, datasourceStates: { @@ -283,7 +280,6 @@ describe('editor_frame', () => { instance = ( await mountWithProvider(, { - data: props.plugins.data, preloadedState: { visualization: { activeId: 'testVis', state: {} }, datasourceStates: { @@ -395,7 +391,6 @@ describe('editor_frame', () => { ExpressionRenderer: expressionRendererMock, }; await mountWithProvider(, { - data: props.plugins.data, preloadedState: { activeDatasourceId: 'testDatasource', visualization: { activeId: mockVisualization.id, state: {} }, @@ -437,7 +432,7 @@ describe('editor_frame', () => { ExpressionRenderer: expressionRendererMock, }; - await mountWithProvider(, { data: props.plugins.data }); + await mountWithProvider(); const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1] .setState; @@ -474,7 +469,6 @@ describe('editor_frame', () => { ExpressionRenderer: expressionRendererMock, }; await mountWithProvider(, { - data: props.plugins.data, preloadedState: { visualization: { activeId: mockVisualization.id, state: {} } }, }); @@ -523,7 +517,6 @@ describe('editor_frame', () => { ExpressionRenderer: expressionRendererMock, }; await mountWithProvider(, { - data: props.plugins.data, preloadedState: { datasourceStates: { testDatasource: { @@ -587,8 +580,7 @@ describe('editor_frame', () => { ExpressionRenderer: expressionRendererMock, }; - instance = (await mountWithProvider(, { data: props.plugins.data })) - .instance; + instance = (await mountWithProvider()).instance; // necessary to flush elements to dom synchronously instance.update(); @@ -692,7 +684,7 @@ describe('editor_frame', () => { ExpressionRenderer: expressionRendererMock, }; - await mountWithProvider(, { data: props.plugins.data }); + await mountWithProvider(); expect(mockDatasource.getDatasourceSuggestionsFromCurrentState).toHaveBeenCalled(); expect(mockDatasource2.getDatasourceSuggestionsFromCurrentState).not.toHaveBeenCalled(); @@ -725,7 +717,7 @@ describe('editor_frame', () => { ExpressionRenderer: expressionRendererMock, }; - await mountWithProvider(, { data: props.plugins.data }); + await mountWithProvider(); expect(mockVisualization.getSuggestions).toHaveBeenCalled(); expect(mockVisualization2.getSuggestions).toHaveBeenCalled(); @@ -793,8 +785,7 @@ describe('editor_frame', () => { ExpressionRenderer: expressionRendererMock, }; - instance = (await mountWithProvider(, { data: props.plugins.data })) - .instance; + instance = (await mountWithProvider()).instance; expect( instance @@ -840,8 +831,7 @@ describe('editor_frame', () => { ExpressionRenderer: expressionRendererMock, }; - instance = (await mountWithProvider(, { data: props.plugins.data })) - .instance; + instance = (await mountWithProvider()).instance; act(() => { instance.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click'); @@ -898,8 +888,7 @@ describe('editor_frame', () => { ExpressionRenderer: expressionRendererMock, }; - instance = (await mountWithProvider(, { data: props.plugins.data })) - .instance; + instance = (await mountWithProvider()).instance; act(() => { instance.find('[data-test-subj="lnsWorkspace"]').last().simulate('drop'); @@ -968,7 +957,6 @@ describe('editor_frame', () => { } as EditorFrameProps; instance = ( await mountWithProvider(, { - data: props.plugins.data, preloadedState: { datasourceStates: { testDatasource: { @@ -1080,11 +1068,7 @@ describe('editor_frame', () => { ExpressionRenderer: expressionRendererMock, } as EditorFrameProps; - instance = ( - await mountWithProvider(, { - data: props.plugins.data, - }) - ).instance; + instance = (await mountWithProvider()).instance; act(() => { instance.find(DragDrop).filter('[dataTestSubj="lnsWorkspace"]').prop('onDrop')!( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 3b55c4923f967..c68c04b4b3e21 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -76,7 +76,7 @@ export function EditorFrame(props: EditorFrameProps) { const suggestion = getSuggestionForField.current!(field); if (suggestion) { trackUiEvent('drop_onto_workspace'); - switchToSuggestion(dispatchLens, suggestion, 'SWITCH_VISUALIZATION'); + switchToSuggestion(dispatchLens, suggestion, true); } }, [getSuggestionForField, dispatchLens] diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index 90fa2ab080dd2..0d68e2d72e73b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -46,7 +46,7 @@ describe('suggestion helpers', () => { ]); const suggestedState = {}; const visualizationMap = { - vis1: { + testVis: { ...mockVisualization, getSuggestions: () => [ { @@ -60,7 +60,7 @@ describe('suggestion helpers', () => { }; const suggestions = getSuggestions({ visualizationMap, - activeVisualization: visualizationMap.vis1, + activeVisualization: visualizationMap.testVis, visualizationState: {}, datasourceMap, datasourceStates, @@ -76,7 +76,7 @@ describe('suggestion helpers', () => { generateSuggestion(), ]); const visualizationMap = { - vis1: { + testVis: { ...mockVisualization1, getSuggestions: () => [ { @@ -107,7 +107,7 @@ describe('suggestion helpers', () => { }; const suggestions = getSuggestions({ visualizationMap, - activeVisualization: visualizationMap.vis1, + activeVisualization: visualizationMap.testVis, visualizationState: {}, datasourceMap, datasourceStates, @@ -119,11 +119,11 @@ describe('suggestion helpers', () => { datasourceMap.mock.getDatasourceSuggestionsForField.mockReturnValue([generateSuggestion()]); const droppedField = {}; const visualizationMap = { - vis1: createMockVisualization(), + testVis: createMockVisualization(), }; getSuggestions({ visualizationMap, - activeVisualization: visualizationMap.vis1, + activeVisualization: visualizationMap.testVis, visualizationState: {}, datasourceMap, datasourceStates, @@ -153,12 +153,12 @@ describe('suggestion helpers', () => { mock3: createMockDatasource('a'), }; const visualizationMap = { - vis1: createMockVisualization(), + testVis: createMockVisualization(), }; const droppedField = {}; getSuggestions({ visualizationMap, - activeVisualization: visualizationMap.vis1, + activeVisualization: visualizationMap.testVis, visualizationState: {}, datasourceMap: multiDatasourceMap, datasourceStates: multiDatasourceStates, @@ -183,12 +183,12 @@ describe('suggestion helpers', () => { ]); const visualizationMap = { - vis1: createMockVisualization(), + testVis: createMockVisualization(), }; getSuggestions({ visualizationMap, - activeVisualization: visualizationMap.vis1, + activeVisualization: visualizationMap.testVis, visualizationState: {}, datasourceMap, datasourceStates, @@ -226,11 +226,11 @@ describe('suggestion helpers', () => { }; const visualizationMap = { - vis1: createMockVisualization(), + testVis: createMockVisualization(), }; getSuggestions({ visualizationMap, - activeVisualization: visualizationMap.vis1, + activeVisualization: visualizationMap.testVis, visualizationState: {}, datasourceMap: multiDatasourceMap, datasourceStates: multiDatasourceStates, @@ -258,7 +258,7 @@ describe('suggestion helpers', () => { generateSuggestion(), ]); const visualizationMap = { - vis1: { + testVis: { ...mockVisualization1, getSuggestions: () => [ { @@ -289,7 +289,7 @@ describe('suggestion helpers', () => { }; const suggestions = getSuggestions({ visualizationMap, - activeVisualization: visualizationMap.vis1, + activeVisualization: visualizationMap.testVis, visualizationState: {}, datasourceMap, datasourceStates, @@ -319,12 +319,12 @@ describe('suggestion helpers', () => { { state: {}, table: table2, keptLayerIds: ['first'] }, ]); const visualizationMap = { - vis1: mockVisualization1, + testVis: mockVisualization1, vis2: mockVisualization2, }; getSuggestions({ visualizationMap, - activeVisualization: visualizationMap.vis1, + activeVisualization: visualizationMap.testVis, visualizationState: {}, datasourceMap, datasourceStates, @@ -372,7 +372,7 @@ describe('suggestion helpers', () => { }, ]); const visualizationMap = { - vis1: { + testVis: { ...mockVisualization1, getSuggestions: vis1Suggestions, }, @@ -384,7 +384,7 @@ describe('suggestion helpers', () => { const suggestions = getSuggestions({ visualizationMap, - activeVisualization: visualizationMap.vis1, + activeVisualization: visualizationMap.testVis, visualizationState: {}, datasourceMap, datasourceStates, @@ -407,13 +407,13 @@ describe('suggestion helpers', () => { ]); const visualizationMap = { - vis1: mockVisualization1, + testVis: mockVisualization1, vis2: mockVisualization2, }; getSuggestions({ visualizationMap, - activeVisualization: visualizationMap.vis1, + activeVisualization: visualizationMap.testVis, visualizationState: {}, datasourceMap, datasourceStates, @@ -439,12 +439,12 @@ describe('suggestion helpers', () => { generateSuggestion(1), ]); const visualizationMap = { - vis1: mockVisualization1, + testVis: mockVisualization1, vis2: mockVisualization2, }; getSuggestions({ visualizationMap, - activeVisualization: visualizationMap.vis1, + activeVisualization: visualizationMap.testVis, visualizationState: {}, datasourceMap, datasourceStates, @@ -472,13 +472,13 @@ describe('suggestion helpers', () => { generateSuggestion(1), ]); const visualizationMap = { - vis1: mockVisualization1, + testVis: mockVisualization1, vis2: mockVisualization2, }; getSuggestions({ visualizationMap, - activeVisualization: visualizationMap.vis1, + activeVisualization: visualizationMap.testVis, visualizationState: {}, datasourceMap, datasourceStates, @@ -542,9 +542,9 @@ describe('suggestion helpers', () => { getOperationForColumnId: jest.fn(), }, }, - { activeId: 'vis1', state: {} }, - { mockindexpattern: { state: mockDatasourceState, isLoading: false } }, - { vis1: mockVisualization1 }, + { activeId: 'testVis', state: {} }, + { testDatasource: { state: mockDatasourceState, isLoading: false } }, + { testVis: mockVisualization1 }, datasourceMap.mock, { id: 'myfield', humanData: { label: 'myfieldLabel' } }, ]; @@ -574,7 +574,7 @@ describe('suggestion helpers', () => { it('should return nothing if datasource does not produce suggestions', () => { datasourceMap.mock.getDatasourceSuggestionsForField.mockReturnValue([]); defaultParams[3] = { - vis1: { ...mockVisualization1, getSuggestions: () => [] }, + testVis: { ...mockVisualization1, getSuggestions: () => [] }, vis2: mockVisualization2, }; const result = getTopSuggestionForField(...defaultParams); @@ -583,7 +583,7 @@ describe('suggestion helpers', () => { it('should not consider suggestion from other visualization if there is data', () => { defaultParams[3] = { - vis1: { ...mockVisualization1, getSuggestions: () => [] }, + testVis: { ...mockVisualization1, getSuggestions: () => [] }, vis2: mockVisualization2, }; const result = getTopSuggestionForField(...defaultParams); @@ -609,7 +609,7 @@ describe('suggestion helpers', () => { }, ]); defaultParams[3] = { - vis1: mockVisualization1, + testVis: mockVisualization1, vis2: mockVisualization2, vis3: mockVisualization3, }; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index a5c7871f33dfc..7f1e4aa58dba3 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -25,7 +25,6 @@ import { LayerType, layerTypes } from '../../../common'; import { getLayerType } from './config_panel/add_layer'; import { LensDispatch, - selectSuggestion, switchVisualization, DatasourceStates, VisualizationState, @@ -164,24 +163,21 @@ export function getVisualizeFieldSuggestions({ datasourceMap, datasourceStates, visualizationMap, - activeVisualization, - visualizationState, visualizeTriggerFieldContext, }: { datasourceMap: DatasourceMap; datasourceStates: DatasourceStates; visualizationMap: VisualizationMap; - activeVisualization: Visualization; subVisualizationId?: string; - visualizationState: unknown; visualizeTriggerFieldContext?: VisualizeFieldContext; }): Suggestion | undefined { + const activeVisualization = visualizationMap?.[Object.keys(visualizationMap)[0]] || null; const suggestions = getSuggestions({ datasourceMap, datasourceStates, visualizationMap, activeVisualization, - visualizationState, + visualizationState: undefined, visualizeTriggerFieldContext, }); if (suggestions.length) { @@ -230,19 +226,18 @@ export function switchToSuggestion( Suggestion, 'visualizationId' | 'visualizationState' | 'datasourceState' | 'datasourceId' >, - type: 'SWITCH_VISUALIZATION' | 'SELECT_SUGGESTION' = 'SELECT_SUGGESTION' + clearStagedPreview?: boolean ) { - const pickedSuggestion = { - newVisualizationId: suggestion.visualizationId, - initialState: suggestion.visualizationState, - datasourceState: suggestion.datasourceState, - datasourceId: suggestion.datasourceId!, - }; - dispatchLens( - type === 'SELECT_SUGGESTION' - ? selectSuggestion(pickedSuggestion) - : switchVisualization(pickedSuggestion) + switchVisualization({ + suggestion: { + newVisualizationId: suggestion.visualizationId, + visualizationState: suggestion.visualizationState, + datasourceState: suggestion.datasourceState, + datasourceId: suggestion.datasourceId!, + }, + clearStagedPreview, + }) ); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx index b63d2956cfe6b..26e0be3555714 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx @@ -214,16 +214,17 @@ describe('suggestion_panel', () => { act(() => { instance.find('button[data-test-subj="lnsSuggestion"]').at(1).simulate('click'); }); - // instance.update(); expect(lensStore.dispatch).toHaveBeenCalledWith( expect.objectContaining({ - type: 'lens/selectSuggestion', + type: 'lens/switchVisualization', payload: { - datasourceId: undefined, - datasourceState: {}, - initialState: { suggestion1: true }, - newVisualizationId: 'testVis', + suggestion: { + datasourceId: undefined, + datasourceState: {}, + visualizationState: { suggestion1: true }, + newVisualizationId: 'testVis', + }, }, }) ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx index e7abf291b6eba..7cb97882a5e03 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx @@ -200,10 +200,13 @@ describe('chart_switch', () => { expect(lensStore.dispatch).toHaveBeenCalledWith({ type: 'lens/switchVisualization', payload: { - initialState: 'suggestion visB', - newVisualizationId: 'visB', - datasourceId: 'testDatasource', - datasourceState: {}, + suggestion: { + visualizationState: 'suggestion visB', + newVisualizationId: 'visB', + datasourceId: 'testDatasource', + datasourceState: {}, + }, + clearStagedPreview: true, }, }); }); @@ -238,8 +241,11 @@ describe('chart_switch', () => { expect(lensStore.dispatch).toHaveBeenCalledWith({ type: 'lens/switchVisualization', payload: { - initialState: 'visB initial state', - newVisualizationId: 'visB', + suggestion: { + visualizationState: 'visB initial state', + newVisualizationId: 'visB', + }, + clearStagedPreview: true, }, }); expect(lensStore.dispatch).toHaveBeenCalledWith({ @@ -522,10 +528,13 @@ describe('chart_switch', () => { expect(lensStore.dispatch).toHaveBeenCalledWith({ type: 'lens/switchVisualization', payload: { - datasourceId: undefined, - datasourceState: undefined, - initialState: 'visB initial state', - newVisualizationId: 'visB', + suggestion: { + datasourceId: undefined, + datasourceState: undefined, + visualizationState: 'visB initial state', + newVisualizationId: 'visB', + }, + clearStagedPreview: true, }, }); }); @@ -598,10 +607,13 @@ describe('chart_switch', () => { expect(lensStore.dispatch).toHaveBeenCalledWith({ type: 'lens/switchVisualization', payload: { - datasourceId: 'testDatasource', - datasourceState: {}, - initialState: 'switched', - newVisualizationId: 'visC', + suggestion: { + datasourceId: 'testDatasource', + datasourceState: {}, + visualizationState: 'switched', + newVisualizationId: 'visC', + }, + clearStagedPreview: true, }, }); expect(datasourceMap.testDatasource.removeLayer).not.toHaveBeenCalled(); @@ -694,10 +706,13 @@ describe('chart_switch', () => { expect(lensStore.dispatch).toHaveBeenCalledWith({ type: 'lens/switchVisualization', payload: { - newVisualizationId: 'visB', - datasourceId: 'testDatasource', - datasourceState: 'testDatasource suggestion', - initialState: 'suggestion visB', + suggestion: { + newVisualizationId: 'visB', + datasourceId: 'testDatasource', + datasourceState: 'testDatasource suggestion', + visualizationState: 'suggestion visB', + }, + clearStagedPreview: true, }, }); }); @@ -731,10 +746,13 @@ describe('chart_switch', () => { expect(lensStore.dispatch).toHaveBeenCalledWith({ type: 'lens/switchVisualization', payload: { - initialState: 'suggestion visB visB', - newVisualizationId: 'visB', - datasourceId: 'testDatasource', - datasourceState: {}, + suggestion: { + visualizationState: 'suggestion visB visB', + newVisualizationId: 'visB', + datasourceId: 'testDatasource', + datasourceState: {}, + }, + clearStagedPreview: true, }, }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index 51d4f2955a52b..a5ba12941cf7f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -166,7 +166,7 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) { ...selection, visualizationState: selection.getVisualizationState(), }, - 'SWITCH_VISUALIZATION' + true ); if ( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index 4df3632c7f7da..2ed65d3b0f146 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -103,7 +103,6 @@ describe('workspace_panel', () => { />, { - data: defaultProps.plugins.data, preloadedState: { visualization: { activeId: null, state: {} }, datasourceStates: {} }, } ); @@ -121,7 +120,7 @@ describe('workspace_panel', () => { }} />, - { data: defaultProps.plugins.data, preloadedState: { datasourceStates: {} } } + { preloadedState: { datasourceStates: {} } } ); instance = mounted.instance; @@ -138,7 +137,7 @@ describe('workspace_panel', () => { }} />, - { data: defaultProps.plugins.data, preloadedState: { datasourceStates: {} } } + { preloadedState: { datasourceStates: {} } } ); instance = mounted.instance; @@ -165,8 +164,7 @@ describe('workspace_panel', () => { testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} - />, - { data: defaultProps.plugins.data } + /> ); instance = mounted.instance; @@ -199,9 +197,7 @@ describe('workspace_panel', () => { }} ExpressionRenderer={expressionRendererMock} plugins={{ ...props.plugins, uiActions: uiActionsMock }} - />, - - { data: defaultProps.plugins.data } + /> ); instance = mounted.instance; @@ -233,9 +229,7 @@ describe('workspace_panel', () => { testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} - />, - - { data: defaultProps.plugins.data } + /> ); instance = mounted.instance; @@ -279,7 +273,6 @@ describe('workspace_panel', () => { />, { - data: defaultProps.plugins.data, preloadedState: { datasourceStates: { testDatasource: { @@ -360,9 +353,7 @@ describe('workspace_panel', () => { testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} - />, - - { data: defaultProps.plugins.data } + /> ); instance = mounted.instance; }); @@ -408,9 +399,7 @@ describe('workspace_panel', () => { testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} - />, - - { data: defaultProps.plugins.data } + /> ); instance = mounted.instance; }); @@ -456,7 +445,6 @@ describe('workspace_panel', () => { />, { - data: defaultProps.plugins.data, preloadedState: { datasourceStates: { testDatasource: { @@ -499,7 +487,6 @@ describe('workspace_panel', () => { />, { - data: defaultProps.plugins.data, preloadedState: { datasourceStates: { testDatasource: { @@ -543,7 +530,6 @@ describe('workspace_panel', () => { />, { - data: defaultProps.plugins.data, preloadedState: { datasourceStates: { testDatasource: { @@ -582,9 +568,7 @@ describe('workspace_panel', () => { visualizationMap={{ testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} - />, - - { data: defaultProps.plugins.data } + /> ); instance = mounted.instance; @@ -614,9 +598,7 @@ describe('workspace_panel', () => { visualizationMap={{ testVis: mockVisualization, }} - />, - - { data: defaultProps.plugins.data } + /> ); instance = mounted.instance; @@ -648,9 +630,7 @@ describe('workspace_panel', () => { visualizationMap={{ testVis: mockVisualization, }} - />, - - { data: defaultProps.plugins.data } + /> ); instance = mounted.instance; @@ -679,9 +659,7 @@ describe('workspace_panel', () => { visualizationMap={{ testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} - />, - - { data: defaultProps.plugins.data } + /> ); instance = mounted.instance; @@ -709,9 +687,7 @@ describe('workspace_panel', () => { testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} - />, - - { data: defaultProps.plugins.data } + /> ); instance = mounted.instance; }); @@ -745,9 +721,7 @@ describe('workspace_panel', () => { testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} - />, - - { data: defaultProps.plugins.data } + /> ); instance = mounted.instance; lensStore = mounted.lensStore; @@ -832,10 +806,13 @@ describe('workspace_panel', () => { expect(lensStore.dispatch).toHaveBeenCalledWith({ type: 'lens/switchVisualization', payload: { - newVisualizationId: 'testVis', - initialState: {}, - datasourceState: {}, - datasourceId: 'testDatasource', + suggestion: { + newVisualizationId: 'testVis', + visualizationState: {}, + datasourceState: {}, + datasourceId: 'testDatasource', + }, + clearStagedPreview: true, }, }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index c34e3c4137368..e4816870b4380 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -275,7 +275,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ if (suggestionForDraggedField) { trackUiEvent('drop_onto_workspace'); trackUiEvent(expressionExists ? 'drop_non_empty' : 'drop_empty'); - switchToSuggestion(dispatchLens, suggestionForDraggedField, 'SWITCH_VISUALIZATION'); + switchToSuggestion(dispatchLens, suggestionForDraggedField, true); } }, [suggestionForDraggedField, expressionExists, dispatchLens]); diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx index 402440f3302f6..cb9ab11998105 100644 --- a/x-pack/plugins/lens/public/mocks.tsx +++ b/x-pack/plugins/lens/public/mocks.tsx @@ -39,7 +39,12 @@ import { fieldFormatsServiceMock } from '../../../../src/plugins/field_formats/p import type { LensAttributeService } from './lens_attribute_service'; import type { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/public'; -import { makeConfigureStore, LensAppState, LensState } from './state_management/index'; +import { + makeConfigureStore, + LensAppState, + LensState, + LensStoreDeps, +} from './state_management/index'; import { getResolvedDateRange } from './utils'; import { presentationUtilPluginMock } from '../../../../src/plugins/presentation_util/public/mocks'; import { @@ -48,6 +53,8 @@ import { Visualization, FramePublicAPI, FrameDatasourceAPI, + DatasourceMap, + VisualizationMap, } from './types'; export function mockDatasourceStates() { @@ -59,7 +66,7 @@ export function mockDatasourceStates() { }; } -export function createMockVisualization(id = 'vis1'): jest.Mocked { +export function createMockVisualization(id = 'testVis'): jest.Mocked { return { id, clearLayer: jest.fn((state, _layerId) => state), @@ -75,11 +82,12 @@ export function createMockVisualization(id = 'vis1'): jest.Mocked groupLabel: `${id}Group`, }, ], + appendLayer: jest.fn(), getVisualizationTypeId: jest.fn((_state) => 'empty'), getDescription: jest.fn((_state) => ({ label: '' })), switchVisualizationType: jest.fn((_, x) => x), getSuggestions: jest.fn((_options) => []), - initialize: jest.fn((_frame, _state?) => ({})), + initialize: jest.fn((_frame, _state?) => ({ newState: 'newState' })), getConfiguration: jest.fn((props) => ({ groups: [ { @@ -120,7 +128,7 @@ export function createMockDatasource(id: string): DatasourceMock { }; return { - id: 'mockindexpattern', + id: 'testDatasource', clearLayer: jest.fn((state, _layerId) => state), getDatasourceSuggestionsForField: jest.fn((_state, _item, filterFn) => []), getDatasourceSuggestionsForVisualizeField: jest.fn((_state, _indexpatternId, _fieldName) => []), @@ -134,7 +142,7 @@ export function createMockDatasource(id: string): DatasourceMock { renderDataPanel: jest.fn(), renderLayerPanel: jest.fn(), toExpression: jest.fn((_frame, _state) => null), - insertLayer: jest.fn((_state, _newLayerId) => {}), + insertLayer: jest.fn((_state, _newLayerId) => ({})), removeLayer: jest.fn((_state, _layerId) => {}), removeColumn: jest.fn((props) => {}), getLayers: jest.fn((_state) => []), @@ -153,8 +161,9 @@ export function createMockDatasource(id: string): DatasourceMock { }; } -const mockDatasource: DatasourceMock = createMockDatasource('testDatasource'); -const mockDatasource2: DatasourceMock = createMockDatasource('testDatasource2'); +export const mockDatasource: DatasourceMock = createMockDatasource('testDatasource'); +export const mockDatasource2: DatasourceMock = createMockDatasource('testDatasource2'); + export const datasourceMap = { testDatasource2: mockDatasource2, testDatasource: mockDatasource, @@ -251,14 +260,41 @@ export function createMockTimefilter() { }; } -export function mockDataPlugin(sessionIdSubject = new Subject()) { +export const exactMatchDoc = { + ...defaultDoc, + sharingSavedObjectProps: { + outcome: 'exactMatch', + }, +}; + +export const mockStoreDeps = (deps?: { + lensServices?: LensAppServices; + datasourceMap?: DatasourceMap; + visualizationMap?: VisualizationMap; +}) => { + return { + datasourceMap: deps?.datasourceMap || datasourceMap, + visualizationMap: deps?.visualizationMap || visualizationMap, + lensServices: deps?.lensServices || makeDefaultServices(), + }; +}; + +export function mockDataPlugin( + sessionIdSubject = new Subject(), + initialSessionId?: string +) { function createMockSearchService() { - let sessionIdCounter = 1; + let sessionIdCounter = initialSessionId ? 1 : 0; + let currentSessionId: string | undefined = initialSessionId; + const start = () => { + currentSessionId = `sessionId-${++sessionIdCounter}`; + return currentSessionId; + }; return { session: { - start: jest.fn(() => `sessionId-${sessionIdCounter++}`), + start: jest.fn(start), clear: jest.fn(), - getSessionId: jest.fn(() => `sessionId-${sessionIdCounter}`), + getSessionId: jest.fn(() => currentSessionId), getSession$: jest.fn(() => sessionIdSubject.asObservable()), }, }; @@ -296,7 +332,6 @@ export function mockDataPlugin(sessionIdSubject = new Subject()) { }, }; } - function createMockQueryString() { return { getQuery: jest.fn(() => ({ query: '', language: 'lucene' })), @@ -328,6 +363,7 @@ export function mockDataPlugin(sessionIdSubject = new Subject()) { export function makeDefaultServices( sessionIdSubject = new Subject(), + sessionId: string | undefined = undefined, doc = defaultDoc ): jest.Mocked { const core = coreMock.createStart({ basePath: '/testbasepath' }); @@ -365,13 +401,7 @@ export function makeDefaultServices( }, core ); - - attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue({ - ...doc, - sharingSavedObjectProps: { - outcome: 'exactMatch', - }, - }); + attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue(exactMatchDoc); attributeServiceMock.wrapAttributes = jest.fn().mockResolvedValue({ savedObjectId: (doc as unknown as LensByReferenceInput).savedObjectId, }); @@ -402,7 +432,7 @@ export function makeDefaultServices( }, getUrlForApp: jest.fn((appId: string) => `/testbasepath/app/${appId}#/`), }, - data: mockDataPlugin(sessionIdSubject), + data: mockDataPlugin(sessionIdSubject, sessionId), fieldFormats: fieldFormatsServiceMock.createStartContract(), storage: { get: jest.fn(), @@ -432,44 +462,34 @@ export const defaultState = { }; export function makeLensStore({ - data, preloadedState, dispatch, + storeDeps = mockStoreDeps(), }: { - data?: DataPublicPluginStart; + storeDeps?: LensStoreDeps; preloadedState?: Partial; dispatch?: jest.Mock; }) { - if (!data) { - data = mockDataPlugin(); - } - const lensStore = makeConfigureStore( - { - lensServices: { ...makeDefaultServices(), data }, - datasourceMap, - visualizationMap, + const data = storeDeps.lensServices.data; + const store = makeConfigureStore(storeDeps, { + lens: { + ...defaultState, + query: data.query.queryString.getQuery(), + filters: data.query.filterManager.getGlobalFilters(), + resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter), + ...preloadedState, }, - { - lens: { - ...defaultState, - searchSessionId: data.search.session.start(), - query: data.query.queryString.getQuery(), - filters: data.query.filterManager.getGlobalFilters(), - resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter), - ...preloadedState, - }, - } as DeepPartial - ); + } as DeepPartial); - const origDispatch = lensStore.dispatch; - lensStore.dispatch = jest.fn(dispatch || origDispatch); - return lensStore; + const origDispatch = store.dispatch; + store.dispatch = jest.fn(dispatch || origDispatch); + return { store, deps: storeDeps }; } export const mountWithProvider = async ( component: React.ReactElement, store?: { - data?: DataPublicPluginStart; + storeDeps?: LensStoreDeps; preloadedState?: Partial; dispatch?: jest.Mock; }, @@ -480,7 +500,7 @@ export const mountWithProvider = async ( attachTo?: HTMLElement; } ) => { - const lensStore = makeLensStore(store || {}); + const { store: lensStore, deps } = makeLensStore(store || {}); let wrappingComponent: React.FC<{ children: React.ReactNode; @@ -510,5 +530,5 @@ export const mountWithProvider = async ( ...restOptions, } as unknown as ReactWrapper); }); - return { instance, lensStore }; + return { instance, lensStore, deps }; }; diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/__snapshots__/load_initial.test.tsx.snap b/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap similarity index 94% rename from x-pack/plugins/lens/public/state_management/init_middleware/__snapshots__/load_initial.test.tsx.snap rename to x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap index 32d221e14730b..57da18d9dc92f 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/__snapshots__/load_initial.test.tsx.snap +++ b/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`init_middleware should initialize all datasources with state from doc 1`] = ` +exports[`Initializing the store should initialize all datasources with state from doc 1`] = ` Object { "lens": Object { "activeDatasourceId": "testDatasource", @@ -82,7 +82,7 @@ Object { "fromDate": "2021-01-10T04:00:00.000Z", "toDate": "2021-01-10T08:00:00.000Z", }, - "searchSessionId": "sessionId-2", + "searchSessionId": "sessionId-1", "sharingSavedObjectProps": Object { "aliasTargetId": undefined, "outcome": undefined, diff --git a/x-pack/plugins/lens/public/state_management/index.ts b/x-pack/plugins/lens/public/state_management/index.ts index 1d8f4fdffa730..cc83cc612f32d 100644 --- a/x-pack/plugins/lens/public/state_management/index.ts +++ b/x-pack/plugins/lens/public/state_management/index.ts @@ -8,7 +8,7 @@ import { configureStore, getDefaultMiddleware, DeepPartial } from '@reduxjs/toolkit'; import { createLogger } from 'redux-logger'; import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'; -import { lensSlice } from './lens_slice'; +import { makeLensReducer, lensActions } from './lens_slice'; import { timeRangeMiddleware } from './time_range_middleware'; import { optimizingMiddleware } from './optimizing_middleware'; import { LensState, LensStoreDeps } from './types'; @@ -16,10 +16,6 @@ import { initMiddleware } from './init_middleware'; export * from './types'; export * from './selectors'; -export const reducer = { - lens: lensSlice.reducer, -}; - export const { loadInitial, navigateAway, @@ -31,12 +27,12 @@ export const { updateVisualizationState, updateLayer, switchVisualization, - selectSuggestion, rollbackSuggestion, submitSuggestion, switchDatasource, setToggleFullscreen, -} = lensSlice.actions; + initEmpty, +} = lensActions; export const makeConfigureStore = ( storeDeps: LensStoreDeps, @@ -60,7 +56,9 @@ export const makeConfigureStore = ( } return configureStore({ - reducer, + reducer: { + lens: makeLensReducer(storeDeps), + }, middleware, preloadedState, }); diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx deleted file mode 100644 index 342490e5360a5..0000000000000 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx +++ /dev/null @@ -1,410 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { - makeDefaultServices, - makeLensStore, - defaultDoc, - createMockVisualization, - createMockDatasource, -} from '../../mocks'; -import { Location, History } from 'history'; -import { act } from 'react-dom/test-utils'; -import { loadInitial } from './load_initial'; -import { LensEmbeddableInput } from '../../embeddable'; -import { getPreloadedState } from '../lens_slice'; -import { LensAppState } from '..'; -import { LensAppServices } from '../../app_plugin/types'; -import { DatasourceMap, VisualizationMap } from '../../types'; - -const defaultSavedObjectId = '1234'; -const preloadedState = { - isLoading: true, - visualization: { - state: null, - activeId: 'testVis', - }, -}; - -const exactMatchDoc = { - ...defaultDoc, - sharingSavedObjectProps: { - outcome: 'exactMatch', - }, -}; - -const getDefaultLensServices = () => { - const lensServices = makeDefaultServices(); - lensServices.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(exactMatchDoc); - return lensServices; -}; - -const getStoreDeps = (deps?: { - lensServices?: LensAppServices; - datasourceMap?: DatasourceMap; - visualizationMap?: VisualizationMap; -}) => { - const lensServices = deps?.lensServices || getDefaultLensServices(); - const datasourceMap = deps?.datasourceMap || { - testDatasource2: createMockDatasource('testDatasource2'), - testDatasource: createMockDatasource('testDatasource'), - }; - const visualizationMap = deps?.visualizationMap || { - testVis: { - ...createMockVisualization(), - id: 'testVis', - visualizationTypes: [ - { - icon: 'empty', - id: 'testVis', - label: 'TEST1', - groupLabel: 'testVisGroup', - }, - ], - }, - testVis2: { - ...createMockVisualization(), - id: 'testVis2', - visualizationTypes: [ - { - icon: 'empty', - id: 'testVis2', - label: 'TEST2', - groupLabel: 'testVis2Group', - }, - ], - }, - }; - return { - datasourceMap, - visualizationMap, - lensServices, - }; -}; - -describe('init_middleware', () => { - it('should initialize initial datasource', async () => { - const storeDeps = getStoreDeps(); - const { lensServices, datasourceMap } = storeDeps; - - const lensStore = await makeLensStore({ - data: lensServices.data, - preloadedState, - }); - await act(async () => { - await loadInitial(lensStore, storeDeps, { - redirectCallback: jest.fn(), - initialInput: { savedObjectId: defaultSavedObjectId } as unknown as LensEmbeddableInput, - }); - }); - expect(datasourceMap.testDatasource.initialize).toHaveBeenCalled(); - }); - - it('should have initialized the initial datasource and visualization', async () => { - const storeDeps = getStoreDeps(); - const { lensServices, datasourceMap, visualizationMap } = storeDeps; - - const lensStore = await makeLensStore({ data: lensServices.data, preloadedState }); - await act(async () => { - await loadInitial(lensStore, storeDeps, { redirectCallback: jest.fn() }); - }); - expect(datasourceMap.testDatasource.initialize).toHaveBeenCalled(); - expect(datasourceMap.testDatasource2.initialize).not.toHaveBeenCalled(); - expect(visualizationMap.testVis.initialize).toHaveBeenCalled(); - expect(visualizationMap.testVis2.initialize).not.toHaveBeenCalled(); - }); - - it('should initialize all datasources with state from doc', async () => { - const datasource1State = { datasource1: '' }; - const datasource2State = { datasource2: '' }; - const services = makeDefaultServices(); - services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ - exactMatchDoc, - visualizationType: 'testVis', - title: '', - state: { - datasourceStates: { - testDatasource: datasource1State, - testDatasource2: datasource2State, - }, - visualization: {}, - query: { query: '', language: 'lucene' }, - filters: [], - }, - references: [], - }); - - const storeDeps = getStoreDeps({ - lensServices: services, - visualizationMap: { - testVis: { - ...createMockVisualization(), - id: 'testVis', - visualizationTypes: [ - { - icon: 'empty', - id: 'testVis', - label: 'TEST1', - groupLabel: 'testVisGroup', - }, - ], - }, - }, - datasourceMap: { - testDatasource: createMockDatasource('testDatasource'), - testDatasource2: createMockDatasource('testDatasource2'), - testDatasource3: createMockDatasource('testDatasource3'), - }, - }); - const { datasourceMap } = storeDeps; - - const lensStore = await makeLensStore({ - data: services.data, - preloadedState, - }); - - await act(async () => { - await loadInitial(lensStore, storeDeps, { - redirectCallback: jest.fn(), - initialInput: { savedObjectId: defaultSavedObjectId } as unknown as LensEmbeddableInput, - }); - }); - expect(datasourceMap.testDatasource.initialize).toHaveBeenCalled(); - - expect(datasourceMap.testDatasource.initialize).toHaveBeenCalledWith( - datasource1State, - [], - undefined, - { - isFullEditor: true, - } - ); - expect(datasourceMap.testDatasource2.initialize).toHaveBeenCalledWith( - datasource2State, - [], - undefined, - { - isFullEditor: true, - } - ); - expect(datasourceMap.testDatasource3.initialize).not.toHaveBeenCalled(); - expect(lensStore.getState()).toMatchSnapshot(); - }); - - describe('loadInitial', () => { - it('does not load a document if there is no initial input', async () => { - const storeDeps = getStoreDeps(); - const { lensServices } = storeDeps; - - const lensStore = makeLensStore({ data: lensServices.data, preloadedState }); - await loadInitial(lensStore, storeDeps, { redirectCallback: jest.fn() }); - expect(lensServices.attributeService.unwrapAttributes).not.toHaveBeenCalled(); - }); - - it('cleans datasource and visualization state properly when reloading', async () => { - const storeDeps = getStoreDeps(); - const lensStore = await makeLensStore({ - data: storeDeps.lensServices.data, - preloadedState: { - ...preloadedState, - visualization: { - activeId: 'testVis', - state: {}, - }, - datasourceStates: { testDatasource: { isLoading: false, state: {} } }, - }, - }); - - expect(lensStore.getState()).toEqual({ - lens: expect.objectContaining({ - visualization: { - activeId: 'testVis', - state: {}, - }, - activeDatasourceId: 'testDatasource', - datasourceStates: { - testDatasource: { isLoading: false, state: {} }, - }, - }), - }); - - const emptyState = getPreloadedState(storeDeps) as LensAppState; - storeDeps.lensServices.attributeService.unwrapAttributes = jest.fn(); - await act(async () => { - await loadInitial(lensStore, storeDeps, { - redirectCallback: jest.fn(), - initialInput: undefined, - emptyState, - }); - }); - - expect(lensStore.getState()).toEqual({ - lens: expect.objectContaining({ - visualization: { - activeId: 'testVis', - state: null, // resets to null - }, - activeDatasourceId: 'testDatasource2', // resets to first on the list - datasourceStates: { - testDatasource: { isLoading: false, state: undefined }, // state resets to undefined - }, - }), - }); - }); - - it('loads a document and uses query and filters if initial input is provided', async () => { - const storeDeps = getStoreDeps(); - const { lensServices } = storeDeps; - const emptyState = getPreloadedState(storeDeps) as LensAppState; - - const lensStore = await makeLensStore({ data: lensServices.data, preloadedState }); - await act(async () => { - await loadInitial(lensStore, storeDeps, { - redirectCallback: jest.fn(), - initialInput: { - savedObjectId: defaultSavedObjectId, - } as unknown as LensEmbeddableInput, - emptyState, - }); - }); - - expect(lensServices.attributeService.unwrapAttributes).toHaveBeenCalledWith({ - savedObjectId: defaultSavedObjectId, - }); - - expect(lensServices.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([ - { query: { match_phrase: { src: 'test' } } }, - ]); - - expect(lensStore.getState()).toEqual({ - lens: expect.objectContaining({ - persistedDoc: { ...defaultDoc, type: 'lens' }, - query: 'kuery', - isLoading: false, - activeDatasourceId: 'testDatasource', - }), - }); - }); - - it('does not load documents on sequential renders unless the id changes', async () => { - const storeDeps = getStoreDeps(); - const { lensServices } = storeDeps; - - const lensStore = makeLensStore({ data: lensServices.data, preloadedState }); - await act(async () => { - await loadInitial(lensStore, storeDeps, { - redirectCallback: jest.fn(), - initialInput: { - savedObjectId: defaultSavedObjectId, - } as unknown as LensEmbeddableInput, - }); - }); - - await act(async () => { - await loadInitial(lensStore, storeDeps, { - redirectCallback: jest.fn(), - initialInput: { - savedObjectId: defaultSavedObjectId, - } as unknown as LensEmbeddableInput, - }); - }); - - expect(lensServices.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1); - - await act(async () => { - await loadInitial(lensStore, storeDeps, { - redirectCallback: jest.fn(), - initialInput: { savedObjectId: '5678' } as unknown as LensEmbeddableInput, - }); - }); - - expect(lensServices.attributeService.unwrapAttributes).toHaveBeenCalledTimes(2); - }); - - it('handles document load errors', async () => { - const services = makeDefaultServices(); - services.attributeService.unwrapAttributes = jest.fn().mockRejectedValue('failed to load'); - - const storeDeps = getStoreDeps({ lensServices: services }); - const { lensServices } = storeDeps; - - const redirectCallback = jest.fn(); - - const lensStore = makeLensStore({ data: lensServices.data, preloadedState }); - - await act(async () => { - await loadInitial(lensStore, storeDeps, { - redirectCallback, - initialInput: { - savedObjectId: defaultSavedObjectId, - } as unknown as LensEmbeddableInput, - }); - }); - expect(lensServices.attributeService.unwrapAttributes).toHaveBeenCalledWith({ - savedObjectId: defaultSavedObjectId, - }); - expect(lensServices.notifications.toasts.addDanger).toHaveBeenCalled(); - expect(redirectCallback).toHaveBeenCalled(); - }); - - it('redirects if saved object is an aliasMatch', async () => { - const services = makeDefaultServices(); - services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ - ...defaultDoc, - sharingSavedObjectProps: { - outcome: 'aliasMatch', - aliasTargetId: 'id2', - }, - }); - - const storeDeps = getStoreDeps({ lensServices: services }); - const lensStore = makeLensStore({ data: storeDeps.lensServices.data, preloadedState }); - - await act(async () => { - await loadInitial(lensStore, storeDeps, { - redirectCallback: jest.fn(), - initialInput: { - savedObjectId: defaultSavedObjectId, - } as unknown as LensEmbeddableInput, - history: { - location: { - search: '?search', - } as Location, - } as History, - }); - }); - expect(storeDeps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledWith({ - savedObjectId: defaultSavedObjectId, - }); - - expect(storeDeps.lensServices.spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith( - '#/edit/id2?search', - 'Lens visualization' - ); - }); - - it('adds to the recently accessed list on load', async () => { - const storeDeps = getStoreDeps(); - const { lensServices } = storeDeps; - - const lensStore = makeLensStore({ data: lensServices.data, preloadedState }); - await act(async () => { - await loadInitial(lensStore, storeDeps, { - redirectCallback: jest.fn(), - initialInput: { - savedObjectId: defaultSavedObjectId, - } as unknown as LensEmbeddableInput, - }); - }); - - expect(lensServices.chrome.recentlyAccessed.add).toHaveBeenCalledWith( - '/app/lens#/edit/1234', - 'An extremely cool default document!', - '1234' - ); - }); - }); -}); diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts index 7db03a17a3a8f..314434a16af8c 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts @@ -9,17 +9,11 @@ import { MiddlewareAPI } from '@reduxjs/toolkit'; import { isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; import { History } from 'history'; -import { LensAppState, setState } from '..'; -import { updateLayer, updateVisualizationState, LensStoreDeps } from '..'; +import { LensAppState, setState, initEmpty, LensStoreDeps } from '..'; import { SharingSavedObjectProps } from '../../types'; import { LensEmbeddableInput, LensByReferenceInput } from '../../embeddable/embeddable'; import { getInitialDatasourceId } from '../../utils'; import { initializeDatasources } from '../../editor_frame_service/editor_frame'; -import { generateId } from '../../id_generator'; -import { - getVisualizeFieldSuggestions, - switchToSuggestion, -} from '../../editor_frame_service/editor_frame/suggestion_helpers'; import { LensAppServices } from '../../app_plugin/types'; import { getEditPath, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../../common/constants'; import { Document, injectFilterReferences } from '../../persistence'; @@ -89,13 +83,7 @@ export const getPersisted = async ({ export function loadInitial( store: MiddlewareAPI, - { - lensServices, - datasourceMap, - visualizationMap, - embeddableEditorIncomingState, - initialContext, - }: LensStoreDeps, + { lensServices, datasourceMap, embeddableEditorIncomingState, initialContext }: LensStoreDeps, { redirectCallback, initialInput, @@ -108,78 +96,39 @@ export function loadInitial( history?: History; } ) { - const { getState, dispatch } = store; const { attributeService, notifications, data, dashboardFeatureFlag } = lensServices; - const { persistedDoc } = getState().lens; + const currentSessionId = data.search.session.getSessionId(); + const { lens } = store.getState(); if ( !initialInput || (attributeService.inputIsRefType(initialInput) && - initialInput.savedObjectId === persistedDoc?.savedObjectId) + initialInput.savedObjectId === lens.persistedDoc?.savedObjectId) ) { - return initializeDatasources( - datasourceMap, - getState().lens.datasourceStates, - undefined, - initialContext, - { - isFullEditor: true, - } - ) + return initializeDatasources(datasourceMap, lens.datasourceStates, undefined, initialContext, { + isFullEditor: true, + }) .then((result) => { - const datasourceStates = Object.entries(result).reduce( - (state, [datasourceId, datasourceState]) => ({ - ...state, - [datasourceId]: { - ...datasourceState, + store.dispatch( + initEmpty({ + newState: { + ...emptyState, + searchSessionId: currentSessionId || data.search.session.start(), + datasourceStates: Object.entries(result).reduce( + (state, [datasourceId, datasourceState]) => ({ + ...state, + [datasourceId]: { + ...datasourceState, + isLoading: false, + }, + }), + {} + ), isLoading: false, }, - }), - {} - ); - dispatch( - setState({ - ...emptyState, - datasourceStates, - isLoading: false, + initialContext, }) ); - if (initialContext) { - const selectedSuggestion = getVisualizeFieldSuggestions({ - datasourceMap, - datasourceStates, - visualizationMap, - activeVisualization: visualizationMap?.[Object.keys(visualizationMap)[0]] || null, - visualizationState: null, - visualizeTriggerFieldContext: initialContext, - }); - if (selectedSuggestion) { - switchToSuggestion(dispatch, selectedSuggestion, 'SWITCH_VISUALIZATION'); - } - } - const activeDatasourceId = getInitialDatasourceId(datasourceMap); - const visualization = getState().lens.visualization; - const activeVisualization = - visualization.activeId && visualizationMap[visualization.activeId]; - - if (visualization.state === null && activeVisualization) { - const newLayerId = generateId(); - - const initialVisualizationState = activeVisualization.initialize(() => newLayerId); - dispatch( - updateLayer({ - datasourceId: activeDatasourceId!, - layerId: newLayerId, - updater: datasourceMap[activeDatasourceId!].insertLayer, - }) - ); - dispatch( - updateVisualizationState({ - visualizationId: activeVisualization.id, - updater: initialVisualizationState, - }) - ); - } }) .catch((e: { message: string }) => { notifications.toasts.addDanger({ @@ -188,6 +137,7 @@ export function loadInitial( redirectCallback(); }); } + getPersisted({ initialInput, lensServices, history }) .then( (persisted) => { @@ -226,11 +176,7 @@ export function loadInitial( } ) .then((result) => { - const activeDatasourceId = getInitialDatasourceId(datasourceMap, doc); - - const currentSessionId = data.search.session.getSessionId(); - - dispatch( + store.dispatch( setState({ sharingSavedObjectProps, query: doc.state.query, @@ -241,8 +187,8 @@ export function loadInitial( currentSessionId ? currentSessionId : data.search.session.start(), - ...(!isEqual(persistedDoc, doc) ? { persistedDoc: doc } : null), - activeDatasourceId, + ...(!isEqual(lens.persistedDoc, doc) ? { persistedDoc: doc } : null), + activeDatasourceId: getInitialDatasourceId(datasourceMap, doc), visualization: { activeId: doc.visualizationType, state: doc.state.visualization, @@ -271,7 +217,7 @@ export function loadInitial( } }, () => { - dispatch( + store.dispatch( setState({ isLoading: false, }) @@ -279,9 +225,10 @@ export function loadInitial( redirectCallback(); } ) - .catch((e: { message: string }) => + .catch((e: { message: string }) => { notifications.toasts.addDanger({ title: e.message, - }) - ); + }); + redirectCallback(); + }); } diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.test.ts b/x-pack/plugins/lens/public/state_management/lens_slice.test.ts index cce0376707143..7d88e6ceb616c 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.test.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.test.ts @@ -17,13 +17,9 @@ import { import { makeLensStore, defaultState } from '../mocks'; describe('lensSlice', () => { - const store = makeLensStore({}); + const { store } = makeLensStore({}); const customQuery = { query: 'custom' } as Query; - // TODO: need to move some initialization logic from mounter - // describe('initialization', () => { - // }) - describe('state update', () => { it('setState: updates state ', () => { const lensState = store.getState().lens; @@ -79,8 +75,11 @@ describe('lensSlice', () => { const newVisState = {}; store.dispatch( switchVisualization({ - newVisualizationId: 'testVis2', - initialState: newVisState, + suggestion: { + newVisualizationId: 'testVis2', + visualizationState: newVisState, + }, + clearStagedPreview: true, }) ); @@ -93,10 +92,13 @@ describe('lensSlice', () => { store.dispatch( switchVisualization({ - newVisualizationId: 'testVis2', - initialState: newVisState, - datasourceState: newDatasourceState, - datasourceId: 'testDatasource', + suggestion: { + newVisualizationId: 'testVis2', + visualizationState: newVisState, + datasourceState: newDatasourceState, + datasourceId: 'testDatasource', + }, + clearStagedPreview: true, }) ); @@ -117,7 +119,7 @@ describe('lensSlice', () => { it('not initialize already initialized datasource on switch', () => { const datasource2State = {}; - const customStore = makeLensStore({ + const { store: customStore } = makeLensStore({ preloadedState: { datasourceStates: { testDatasource: { diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 6cf0529b34575..0461070020055 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -5,12 +5,18 @@ * 2.0. */ -import { createSlice, current, PayloadAction } from '@reduxjs/toolkit'; +import { createAction, createReducer, current, PayloadAction } from '@reduxjs/toolkit'; +import { VisualizeFieldContext } from 'src/plugins/ui_actions/public'; import { History } from 'history'; import { LensEmbeddableInput } from '..'; import { TableInspectorAdapter } from '../editor_frame_service/types'; import { getInitialDatasourceId, getResolvedDateRange } from '../utils'; import { LensAppState, LensStoreDeps } from './types'; +import { generateId } from '../id_generator'; +import { + getVisualizeFieldSuggestions, + Suggestion, +} from '../editor_frame_service/editor_frame/suggestion_helpers'; export const initialState: LensAppState = { persistedDoc: undefined, @@ -68,29 +74,105 @@ export const getPreloadedState = ({ return state; }; -export const lensSlice = createSlice({ - name: 'lens', - initialState, - reducers: { - setState: (state, { payload }: PayloadAction>) => { +export const setState = createAction>('lens/setState'); +export const onActiveDataChange = createAction('lens/onActiveDataChange'); +export const setSaveable = createAction('lens/setSaveable'); +export const updateState = createAction<{ + subType: string; + updater: (prevState: LensAppState) => LensAppState; +}>('lens/updateState'); +export const updateDatasourceState = createAction<{ + updater: unknown | ((prevState: unknown) => unknown); + datasourceId: string; + clearStagedPreview?: boolean; +}>('lens/updateDatasourceState'); +export const updateVisualizationState = createAction<{ + visualizationId: string; + updater: unknown; + clearStagedPreview?: boolean; +}>('lens/updateVisualizationState'); + +export const updateLayer = createAction<{ + layerId: string; + datasourceId: string; + updater: (state: unknown, layerId: string) => unknown; +}>('lens/updateLayer'); + +export const switchVisualization = createAction<{ + suggestion: { + newVisualizationId: string; + visualizationState: unknown; + datasourceState?: unknown; + datasourceId?: string; + }; + clearStagedPreview?: boolean; +}>('lens/switchVisualization'); +export const rollbackSuggestion = createAction('lens/rollbackSuggestion'); +export const setToggleFullscreen = createAction('lens/setToggleFullscreen'); +export const submitSuggestion = createAction('lens/submitSuggestion'); +export const switchDatasource = createAction<{ + newDatasourceId: string; +}>('lens/switchDatasource'); +export const navigateAway = createAction('lens/navigateAway'); +export const loadInitial = createAction<{ + initialInput?: LensEmbeddableInput; + redirectCallback: (savedObjectId?: string) => void; + emptyState: LensAppState; + history: History; +}>('lens/loadInitial'); +export const initEmpty = createAction( + 'initEmpty', + function prepare({ + newState, + initialContext, + }: { + newState: Partial; + initialContext?: VisualizeFieldContext; + }) { + return { payload: { layerId: generateId(), newState, initialContext } }; + } +); + +export const lensActions = { + setState, + onActiveDataChange, + setSaveable, + updateState, + updateDatasourceState, + updateVisualizationState, + updateLayer, + switchVisualization, + rollbackSuggestion, + setToggleFullscreen, + submitSuggestion, + switchDatasource, + navigateAway, + loadInitial, + initEmpty, +}; + +export const makeLensReducer = (storeDeps: LensStoreDeps) => { + const { datasourceMap, visualizationMap } = storeDeps; + return createReducer(initialState, { + [setState.type]: (state, { payload }: PayloadAction>) => { return { ...state, ...payload, }; }, - onActiveDataChange: (state, { payload }: PayloadAction) => { + [onActiveDataChange.type]: (state, { payload }: PayloadAction) => { return { ...state, activeData: payload, }; }, - setSaveable: (state, { payload }: PayloadAction) => { + [setSaveable.type]: (state, { payload }: PayloadAction) => { return { ...state, isSaveable: payload, }; }, - updateState: ( + [updateState.type]: ( state, action: { payload: { @@ -101,7 +183,7 @@ export const lensSlice = createSlice({ ) => { return action.payload.updater(current(state) as LensAppState); }, - updateDatasourceState: ( + [updateDatasourceState.type]: ( state, { payload, @@ -128,7 +210,7 @@ export const lensSlice = createSlice({ stagedPreview: payload.clearStagedPreview ? undefined : state.stagedPreview, }; }, - updateVisualizationState: ( + [updateVisualizationState.type]: ( state, { payload, @@ -161,7 +243,7 @@ export const lensSlice = createSlice({ stagedPreview: payload.clearStagedPreview ? undefined : state.stagedPreview, }; }, - updateLayer: ( + [updateLayer.type]: ( state, { payload, @@ -188,92 +270,65 @@ export const lensSlice = createSlice({ }; }, - switchVisualization: ( - state, - { - payload, - }: { - payload: { - newVisualizationId: string; - initialState: unknown; - datasourceState?: unknown; - datasourceId?: string; - }; - } - ) => { - return { - ...state, - datasourceStates: - 'datasourceId' in payload && payload.datasourceId - ? { - ...state.datasourceStates, - [payload.datasourceId]: { - ...state.datasourceStates[payload.datasourceId], - state: payload.datasourceState, - }, - } - : state.datasourceStates, - visualization: { - ...state.visualization, - activeId: payload.newVisualizationId, - state: payload.initialState, - }, - stagedPreview: undefined, - }; - }, - selectSuggestion: ( + [switchVisualization.type]: ( state, { payload, }: { payload: { - newVisualizationId: string; - initialState: unknown; - datasourceState: unknown; - datasourceId: string; + suggestion: { + newVisualizationId: string; + visualizationState: unknown; + datasourceState?: unknown; + datasourceId?: string; + }; + clearStagedPreview?: boolean; }; } ) => { + const { newVisualizationId, visualizationState, datasourceState, datasourceId } = + payload.suggestion; return { ...state, - datasourceStates: - 'datasourceId' in payload && payload.datasourceId - ? { - ...state.datasourceStates, - [payload.datasourceId]: { - ...state.datasourceStates[payload.datasourceId], - state: payload.datasourceState, - }, - } - : state.datasourceStates, + datasourceStates: datasourceId + ? { + ...state.datasourceStates, + [datasourceId]: { + ...state.datasourceStates[datasourceId], + state: datasourceState, + }, + } + : state.datasourceStates, visualization: { ...state.visualization, - activeId: payload.newVisualizationId, - state: payload.initialState, - }, - stagedPreview: state.stagedPreview || { - datasourceStates: state.datasourceStates, - visualization: state.visualization, + activeId: newVisualizationId, + state: visualizationState, }, + stagedPreview: payload.clearStagedPreview + ? undefined + : state.stagedPreview || { + datasourceStates: state.datasourceStates, + visualization: state.visualization, + }, }; }, - rollbackSuggestion: (state) => { + [rollbackSuggestion.type]: (state) => { return { ...state, ...(state.stagedPreview || {}), stagedPreview: undefined, }; }, - setToggleFullscreen: (state) => { + [setToggleFullscreen.type]: (state) => { return { ...state, isFullscreenDatasource: !state.isFullscreenDatasource }; }, - submitSuggestion: (state) => { + [submitSuggestion.type]: (state) => { return { ...state, stagedPreview: undefined, }; }, - switchDatasource: ( + [switchDatasource.type]: ( state, { payload, @@ -295,8 +350,8 @@ export const lensSlice = createSlice({ activeDatasourceId: payload.newDatasourceId, }; }, - navigateAway: (state) => state, - loadInitial: ( + [navigateAway.type]: (state) => state, + [loadInitial.type]: ( state, payload: PayloadAction<{ initialInput?: LensEmbeddableInput; @@ -305,9 +360,78 @@ export const lensSlice = createSlice({ history: History; }> ) => state, - }, -}); + [initEmpty.type]: ( + state, + { + payload, + }: { + payload: { + newState: Partial; + initialContext: VisualizeFieldContext | undefined; + layerId: string; + }; + } + ) => { + const newState = { + ...state, + ...payload.newState, + }; + const suggestion: Suggestion | undefined = getVisualizeFieldSuggestions({ + datasourceMap, + datasourceStates: newState.datasourceStates, + visualizationMap, + visualizeTriggerFieldContext: payload.initialContext, + }); + if (suggestion) { + return { + ...newState, + datasourceStates: { + ...newState.datasourceStates, + [suggestion.datasourceId!]: { + ...newState.datasourceStates[suggestion.datasourceId!], + state: suggestion.datasourceState, + }, + }, + visualization: { + ...newState.visualization, + activeId: suggestion.visualizationId, + state: suggestion.visualizationState, + }, + stagedPreview: undefined, + }; + } + + const visualization = newState.visualization; + + if (!visualization.activeId) { + throw new Error('Invariant: visualization state got updated without active visualization'); + } -export const reducer = { - lens: lensSlice.reducer, + const activeVisualization = visualizationMap[visualization.activeId]; + if (visualization.state === null && activeVisualization) { + const activeDatasourceId = getInitialDatasourceId(datasourceMap)!; + const newVisState = activeVisualization.initialize(() => payload.layerId); + const activeDatasource = datasourceMap[activeDatasourceId]; + return { + ...newState, + activeDatasourceId, + datasourceStates: { + ...newState.datasourceStates, + [activeDatasourceId]: { + ...newState.datasourceStates[activeDatasourceId], + state: activeDatasource.insertLayer( + newState.datasourceStates[activeDatasourceId]?.state, + payload.layerId + ), + }, + }, + visualization: { + ...visualization, + state: newVisState, + }, + }; + } + return newState; + }, + }); }; diff --git a/x-pack/plugins/lens/public/state_management/load_initial.test.tsx b/x-pack/plugins/lens/public/state_management/load_initial.test.tsx new file mode 100644 index 0000000000000..fe4c553ce4bd7 --- /dev/null +++ b/x-pack/plugins/lens/public/state_management/load_initial.test.tsx @@ -0,0 +1,323 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + makeDefaultServices, + makeLensStore, + defaultDoc, + createMockVisualization, + createMockDatasource, + mockStoreDeps, + exactMatchDoc, +} from '../mocks'; +import { Location, History } from 'history'; +import { act } from 'react-dom/test-utils'; +import { LensEmbeddableInput } from '../embeddable'; +import { getPreloadedState, initialState, loadInitial } from './lens_slice'; +import { LensAppState } from '.'; + +const history = { + location: { + search: '?search', + } as Location, +} as History; + +const defaultSavedObjectId = '1234'; +const preloadedState = { + isLoading: true, + visualization: { + state: null, + activeId: 'testVis', + }, +}; + +const defaultProps = { + redirectCallback: jest.fn(), + initialInput: { savedObjectId: defaultSavedObjectId } as unknown as LensEmbeddableInput, + history, + emptyState: initialState, +}; + +describe('Initializing the store', () => { + it('should initialize initial datasource', async () => { + const { store, deps } = await makeLensStore({ preloadedState }); + await act(async () => { + await store.dispatch(loadInitial(defaultProps)); + }); + expect(deps.datasourceMap.testDatasource.initialize).toHaveBeenCalled(); + }); + + it('should have initialized the initial datasource and visualization', async () => { + const { store, deps } = await makeLensStore({ preloadedState }); + const emptyState = getPreloadedState(deps) as LensAppState; + await act(async () => { + await store.dispatch(loadInitial({ ...defaultProps, initialInput: undefined, emptyState })); + }); + expect(deps.datasourceMap.testDatasource.initialize).toHaveBeenCalled(); + expect(deps.datasourceMap.testDatasource2.initialize).not.toHaveBeenCalled(); + expect(deps.visualizationMap.testVis.initialize).toHaveBeenCalled(); + expect(deps.visualizationMap.testVis2.initialize).not.toHaveBeenCalled(); + }); + + it('should initialize all datasources with state from doc', async () => { + const datasource1State = { datasource1: '' }; + const datasource2State = { datasource2: '' }; + const services = makeDefaultServices(); + services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + exactMatchDoc, + visualizationType: 'testVis', + title: '', + state: { + datasourceStates: { + testDatasource: datasource1State, + testDatasource2: datasource2State, + }, + visualization: {}, + query: { query: '', language: 'lucene' }, + filters: [], + }, + references: [], + }); + + const storeDeps = mockStoreDeps({ + lensServices: services, + visualizationMap: { + testVis: { + ...createMockVisualization(), + id: 'testVis', + visualizationTypes: [ + { + icon: 'empty', + id: 'testVis', + label: 'TEST1', + groupLabel: 'testVisGroup', + }, + ], + }, + }, + datasourceMap: { + testDatasource: createMockDatasource('testDatasource'), + testDatasource2: createMockDatasource('testDatasource2'), + testDatasource3: createMockDatasource('testDatasource3'), + }, + }); + + const { store, deps } = await makeLensStore({ + storeDeps, + preloadedState, + }); + + await act(async () => { + await store.dispatch(loadInitial(defaultProps)); + }); + const { datasourceMap } = deps; + expect(datasourceMap.testDatasource.initialize).toHaveBeenCalled(); + + expect(datasourceMap.testDatasource.initialize).toHaveBeenCalledWith( + datasource1State, + [], + undefined, + { + isFullEditor: true, + } + ); + expect(datasourceMap.testDatasource2.initialize).toHaveBeenCalledWith( + datasource2State, + [], + undefined, + { + isFullEditor: true, + } + ); + expect(datasourceMap.testDatasource3.initialize).not.toHaveBeenCalled(); + expect(store.getState()).toMatchSnapshot(); + }); + + describe('loadInitial', () => { + it('does not load a document if there is no initial input', async () => { + const { deps, store } = makeLensStore({ preloadedState }); + await act(async () => { + await store.dispatch( + loadInitial({ + ...defaultProps, + initialInput: undefined, + }) + ); + }); + expect(deps.lensServices.attributeService.unwrapAttributes).not.toHaveBeenCalled(); + }); + + it('starts new searchSessionId', async () => { + const { store } = await makeLensStore({ preloadedState }); + await act(async () => { + await store.dispatch(loadInitial(defaultProps)); + }); + expect(store.getState()).toEqual({ + lens: expect.objectContaining({ + searchSessionId: 'sessionId-1', + }), + }); + }); + + it('cleans datasource and visualization state properly when reloading', async () => { + const { store, deps } = await makeLensStore({ + preloadedState: { + ...preloadedState, + visualization: { + activeId: 'testVis', + state: {}, + }, + datasourceStates: { testDatasource: { isLoading: false, state: {} } }, + }, + }); + + expect(store.getState()).toEqual({ + lens: expect.objectContaining({ + visualization: { + activeId: 'testVis', + state: {}, + }, + activeDatasourceId: 'testDatasource', + datasourceStates: { + testDatasource: { isLoading: false, state: {} }, + }, + }), + }); + + const emptyState = getPreloadedState(deps) as LensAppState; + + await act(async () => { + await store.dispatch( + loadInitial({ + ...defaultProps, + emptyState, + initialInput: undefined, + }) + ); + }); + + expect(deps.visualizationMap.testVis.initialize).toHaveBeenCalled(); + expect(store.getState()).toEqual({ + lens: expect.objectContaining({ + visualization: { + state: { newState: 'newState' }, // new vis gets initialized + activeId: 'testVis', + }, + activeDatasourceId: 'testDatasource2', // resets to first on the list + datasourceStates: { + testDatasource: { isLoading: false, state: undefined }, // state resets to undefined + testDatasource2: { + state: {}, // initializes first in the map + }, + }, + }), + }); + }); + + it('loads a document and uses query and filters if initial input is provided', async () => { + const { store, deps } = await makeLensStore({ preloadedState }); + await act(async () => { + await store.dispatch(loadInitial(defaultProps)); + }); + + expect(deps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledWith({ + savedObjectId: defaultSavedObjectId, + }); + + expect(deps.lensServices.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([ + { query: { match_phrase: { src: 'test' } } }, + ]); + + expect(store.getState()).toEqual({ + lens: expect.objectContaining({ + persistedDoc: { ...defaultDoc, type: 'lens' }, + query: 'kuery', + isLoading: false, + activeDatasourceId: 'testDatasource', + }), + }); + }); + + it('does not load documents on sequential renders unless the id changes', async () => { + const { store, deps } = makeLensStore({ preloadedState }); + + await act(async () => { + await store.dispatch(loadInitial(defaultProps)); + }); + + await act(async () => { + await store.dispatch(loadInitial(defaultProps)); + }); + + expect(deps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1); + + await act(async () => { + await store.dispatch( + loadInitial({ + ...defaultProps, + initialInput: { savedObjectId: '5678' } as unknown as LensEmbeddableInput, + }) + ); + }); + + expect(deps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledTimes(2); + }); + + it('handles document load errors', async () => { + const { store, deps } = makeLensStore({ preloadedState }); + + deps.lensServices.attributeService.unwrapAttributes = jest + .fn() + .mockRejectedValue('failed to load'); + const redirectCallback = jest.fn(); + await act(async () => { + await store.dispatch(loadInitial({ ...defaultProps, redirectCallback })); + }); + + expect(deps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledWith({ + savedObjectId: defaultSavedObjectId, + }); + expect(deps.lensServices.notifications.toasts.addDanger).toHaveBeenCalled(); + expect(redirectCallback).toHaveBeenCalled(); + }); + + it('redirects if saved object is an aliasMatch', async () => { + const { store, deps } = makeLensStore({ preloadedState }); + deps.lensServices.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + ...defaultDoc, + sharingSavedObjectProps: { + outcome: 'aliasMatch', + aliasTargetId: 'id2', + }, + }); + + await act(async () => { + await store.dispatch(loadInitial(defaultProps)); + }); + + expect(deps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledWith({ + savedObjectId: defaultSavedObjectId, + }); + expect(deps.lensServices.spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith( + '#/edit/id2?search', + 'Lens visualization' + ); + }); + + it('adds to the recently accessed list on load', async () => { + const { store, deps } = makeLensStore({ preloadedState }); + await act(async () => { + await store.dispatch(loadInitial(defaultProps)); + }); + + expect(deps.lensServices.chrome.recentlyAccessed.add).toHaveBeenCalledWith( + '/app/lens#/edit/1234', + 'An extremely cool default document!', + '1234' + ); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts b/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts index ddf50f6fd0d82..8ad6a300beaa4 100644 --- a/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts +++ b/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts @@ -14,117 +14,12 @@ import { timeRangeMiddleware } from './time_range_middleware'; -import { Observable, Subject } from 'rxjs'; -import { DataPublicPluginStart, esFilters } from '../../../../../src/plugins/data/public'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import moment from 'moment'; import { initialState } from './lens_slice'; import { LensAppState } from './types'; import { PayloadAction } from '@reduxjs/toolkit'; - -const sessionIdSubject = new Subject(); - -function createMockSearchService() { - let sessionIdCounter = 1; - return { - session: { - start: jest.fn(() => `sessionId-${sessionIdCounter++}`), - clear: jest.fn(), - getSessionId: jest.fn(() => `sessionId-${sessionIdCounter}`), - getSession$: jest.fn(() => sessionIdSubject.asObservable()), - }, - }; -} - -function createMockFilterManager() { - const unsubscribe = jest.fn(); - - let subscriber: () => void; - let filters: unknown = []; - - return { - getUpdates$: () => ({ - subscribe: ({ next }: { next: () => void }) => { - subscriber = next; - return unsubscribe; - }, - }), - setFilters: jest.fn((newFilters: unknown[]) => { - filters = newFilters; - if (subscriber) subscriber(); - }), - setAppFilters: jest.fn((newFilters: unknown[]) => { - filters = newFilters; - if (subscriber) subscriber(); - }), - getFilters: () => filters, - getGlobalFilters: () => { - // @ts-ignore - return filters.filter(esFilters.isFilterPinned); - }, - removeAll: () => { - filters = []; - subscriber(); - }, - }; -} - -function createMockQueryString() { - return { - getQuery: jest.fn(() => ({ query: '', language: 'kuery' })), - setQuery: jest.fn(), - getDefaultQuery: jest.fn(() => ({ query: '', language: 'kuery' })), - }; -} - -function createMockTimefilter() { - const unsubscribe = jest.fn(); - - let timeFilter = { from: 'now-7d', to: 'now' }; - let subscriber: () => void; - return { - getTime: jest.fn(() => timeFilter), - setTime: jest.fn((newTimeFilter) => { - timeFilter = newTimeFilter; - if (subscriber) { - subscriber(); - } - }), - getTimeUpdate$: () => ({ - subscribe: ({ next }: { next: () => void }) => { - subscriber = next; - return unsubscribe; - }, - }), - calculateBounds: jest.fn(() => ({ - min: moment('2021-01-10T04:00:00.000Z'), - max: moment('2021-01-10T08:00:00.000Z'), - })), - getBounds: jest.fn(() => timeFilter), - getRefreshInterval: () => {}, - getRefreshIntervalDefaults: () => {}, - getAutoRefreshFetch$: () => new Observable(), - }; -} - -function makeDefaultData(): jest.Mocked { - return { - query: { - filterManager: createMockFilterManager(), - timefilter: { - timefilter: createMockTimefilter(), - }, - queryString: createMockQueryString(), - state$: new Observable(), - }, - indexPatterns: { - get: jest.fn().mockImplementation((id) => Promise.resolve({ id, isTimeBased: () => true })), - }, - search: createMockSearchService(), - nowProvider: { - get: jest.fn(), - }, - } as unknown as DataPublicPluginStart; -} +import { mockDataPlugin } from '../mocks'; const createMiddleware = (data: DataPublicPluginStart) => { const middleware = timeRangeMiddleware(data); @@ -142,7 +37,7 @@ const createMiddleware = (data: DataPublicPluginStart) => { describe('timeRangeMiddleware', () => { describe('time update', () => { it('does update the searchSessionId when the state changes and too much time passed', () => { - const data = makeDefaultData(); + const data = mockDataPlugin(); (data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 30000)); (data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({ from: 'now-2m', @@ -176,7 +71,7 @@ describe('timeRangeMiddleware', () => { expect(next).toHaveBeenCalledWith(action); }); it('does not update the searchSessionId when the state changes and too little time has passed', () => { - const data = makeDefaultData(); + const data = mockDataPlugin(); // time range is 100,000ms ago to 300ms ago (that's a lag of .3 percent, not enough to trigger a session update) (data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 300)); (data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({ @@ -202,7 +97,7 @@ describe('timeRangeMiddleware', () => { expect(next).toHaveBeenCalledWith(action); }); it('does not trigger another update when the update already contains searchSessionId', () => { - const data = makeDefaultData(); + const data = mockDataPlugin(); (data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 30000)); (data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({ from: 'now-2m',