[Lens] move from slice to reducers/actions and simplify loading (#113324)

* 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>
This commit is contained in:
Marta Bondyra 2021-10-04 13:32:36 +02:00 committed by GitHub
parent 8e25f5cc0f
commit 6bfa2a4c2c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 845 additions and 1008 deletions

View file

@ -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<LensAppProps>;
@ -110,11 +118,11 @@ describe('Lens App', () => {
</I18nProvider>
);
};
const storeDeps = mockStoreDeps({ lensServices: services });
const { instance, lensStore } = await mountWithProvider(
<App {...props} />,
{
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<string>();
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: {

View file

@ -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<Visualization>;
let mockVisualization2: jest.Mocked<Visualization>;
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(<LayerPanels {...props} />, {
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(
<LayerPanels {...defaultProps} />,
{
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(
<LayerPanels {...defaultProps} />,
{
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(
<LayerPanels {...props} />,
{
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,
}
);
});
});
});

View file

@ -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,

View file

@ -146,7 +146,6 @@ describe('editor_frame', () => {
};
const lensStore = (
await mountWithProvider(<EditorFrame {...props} />, {
data: props.plugins.data,
preloadedState: {
activeDatasourceId: 'testDatasource',
datasourceStates: {
@ -196,7 +195,6 @@ describe('editor_frame', () => {
};
await mountWithProvider(<EditorFrame {...props} />, {
data: props.plugins.data,
preloadedState: {
visualization: { activeId: 'testVis', state: initialState },
},
@ -228,7 +226,6 @@ describe('editor_frame', () => {
};
instance = (
await mountWithProvider(<EditorFrame {...props} />, {
data: props.plugins.data,
preloadedState: {
visualization: { activeId: 'testVis', state: {} },
datasourceStates: {
@ -283,7 +280,6 @@ describe('editor_frame', () => {
instance = (
await mountWithProvider(<EditorFrame {...props} />, {
data: props.plugins.data,
preloadedState: {
visualization: { activeId: 'testVis', state: {} },
datasourceStates: {
@ -395,7 +391,6 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
await mountWithProvider(<EditorFrame {...props} />, {
data: props.plugins.data,
preloadedState: {
activeDatasourceId: 'testDatasource',
visualization: { activeId: mockVisualization.id, state: {} },
@ -437,7 +432,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
await mountWithProvider(<EditorFrame {...props} />, { data: props.plugins.data });
await mountWithProvider(<EditorFrame {...props} />);
const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1]
.setState;
@ -474,7 +469,6 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
await mountWithProvider(<EditorFrame {...props} />, {
data: props.plugins.data,
preloadedState: { visualization: { activeId: mockVisualization.id, state: {} } },
});
@ -523,7 +517,6 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
await mountWithProvider(<EditorFrame {...props} />, {
data: props.plugins.data,
preloadedState: {
datasourceStates: {
testDatasource: {
@ -587,8 +580,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
instance = (await mountWithProvider(<EditorFrame {...props} />, { data: props.plugins.data }))
.instance;
instance = (await mountWithProvider(<EditorFrame {...props} />)).instance;
// necessary to flush elements to dom synchronously
instance.update();
@ -692,7 +684,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
await mountWithProvider(<EditorFrame {...props} />, { data: props.plugins.data });
await mountWithProvider(<EditorFrame {...props} />);
expect(mockDatasource.getDatasourceSuggestionsFromCurrentState).toHaveBeenCalled();
expect(mockDatasource2.getDatasourceSuggestionsFromCurrentState).not.toHaveBeenCalled();
@ -725,7 +717,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
await mountWithProvider(<EditorFrame {...props} />, { data: props.plugins.data });
await mountWithProvider(<EditorFrame {...props} />);
expect(mockVisualization.getSuggestions).toHaveBeenCalled();
expect(mockVisualization2.getSuggestions).toHaveBeenCalled();
@ -793,8 +785,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
instance = (await mountWithProvider(<EditorFrame {...props} />, { data: props.plugins.data }))
.instance;
instance = (await mountWithProvider(<EditorFrame {...props} />)).instance;
expect(
instance
@ -840,8 +831,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
instance = (await mountWithProvider(<EditorFrame {...props} />, { data: props.plugins.data }))
.instance;
instance = (await mountWithProvider(<EditorFrame {...props} />)).instance;
act(() => {
instance.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click');
@ -898,8 +888,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
instance = (await mountWithProvider(<EditorFrame {...props} />, { data: props.plugins.data }))
.instance;
instance = (await mountWithProvider(<EditorFrame {...props} />)).instance;
act(() => {
instance.find('[data-test-subj="lnsWorkspace"]').last().simulate('drop');
@ -968,7 +957,6 @@ describe('editor_frame', () => {
} as EditorFrameProps;
instance = (
await mountWithProvider(<EditorFrame {...props} />, {
data: props.plugins.data,
preloadedState: {
datasourceStates: {
testDatasource: {
@ -1080,11 +1068,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
} as EditorFrameProps;
instance = (
await mountWithProvider(<EditorFrame {...props} />, {
data: props.plugins.data,
})
).instance;
instance = (await mountWithProvider(<EditorFrame {...props} />)).instance;
act(() => {
instance.find(DragDrop).filter('[dataTestSubj="lnsWorkspace"]').prop('onDrop')!(

View file

@ -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]

View file

@ -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,
};

View file

@ -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,
})
);
}

View file

@ -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',
},
},
})
);

View file

@ -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,
},
});
});

View file

@ -166,7 +166,7 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
...selection,
visualizationState: selection.getVisualizationState(),
},
'SWITCH_VISUALIZATION'
true
);
if (

View file

@ -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,
},
});
});

View file

@ -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]);

View file

@ -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<Visualization> {
export function createMockVisualization(id = 'testVis'): jest.Mocked<Visualization> {
return {
id,
clearLayer: jest.fn((state, _layerId) => state),
@ -75,11 +82,12 @@ export function createMockVisualization(id = 'vis1'): jest.Mocked<Visualization>
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<string>()) {
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<string>(),
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<string>()) {
},
};
}
function createMockQueryString() {
return {
getQuery: jest.fn(() => ({ query: '', language: 'lucene' })),
@ -328,6 +363,7 @@ export function mockDataPlugin(sessionIdSubject = new Subject<string>()) {
export function makeDefaultServices(
sessionIdSubject = new Subject<string>(),
sessionId: string | undefined = undefined,
doc = defaultDoc
): jest.Mocked<LensAppServices> {
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<LensAppState>;
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<LensState>
);
} as DeepPartial<LensState>);
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<LensAppState>;
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 };
};

View file

@ -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,

View file

@ -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,
});

View file

@ -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'
);
});
});
});

View file

@ -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<unknown>;
}
) {
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();
});
}

View file

@ -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: {

View file

@ -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<Partial<LensAppState>>) => {
export const setState = createAction<Partial<LensAppState>>('lens/setState');
export const onActiveDataChange = createAction<TableInspectorAdapter>('lens/onActiveDataChange');
export const setSaveable = createAction<boolean>('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<void>('lens/rollbackSuggestion');
export const setToggleFullscreen = createAction<void>('lens/setToggleFullscreen');
export const submitSuggestion = createAction<void>('lens/submitSuggestion');
export const switchDatasource = createAction<{
newDatasourceId: string;
}>('lens/switchDatasource');
export const navigateAway = createAction<void>('lens/navigateAway');
export const loadInitial = createAction<{
initialInput?: LensEmbeddableInput;
redirectCallback: (savedObjectId?: string) => void;
emptyState: LensAppState;
history: History<unknown>;
}>('lens/loadInitial');
export const initEmpty = createAction(
'initEmpty',
function prepare({
newState,
initialContext,
}: {
newState: Partial<LensAppState>;
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<LensAppState>(initialState, {
[setState.type]: (state, { payload }: PayloadAction<Partial<LensAppState>>) => {
return {
...state,
...payload,
};
},
onActiveDataChange: (state, { payload }: PayloadAction<TableInspectorAdapter>) => {
[onActiveDataChange.type]: (state, { payload }: PayloadAction<TableInspectorAdapter>) => {
return {
...state,
activeData: payload,
};
},
setSaveable: (state, { payload }: PayloadAction<boolean>) => {
[setSaveable.type]: (state, { payload }: PayloadAction<boolean>) => {
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: (
[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,
activeId: newVisualizationId,
state: visualizationState,
},
stagedPreview: undefined,
stagedPreview: payload.clearStagedPreview
? undefined
: state.stagedPreview || {
datasourceStates: state.datasourceStates,
visualization: state.visualization,
},
};
},
selectSuggestion: (
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: 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<unknown>;
}>
) => state,
},
});
[initEmpty.type]: (
state,
{
payload,
}: {
payload: {
newState: Partial<LensAppState>;
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,
};
}
export const reducer = {
lens: lensSlice.reducer,
const visualization = newState.visualization;
if (!visualization.activeId) {
throw new Error('Invariant: visualization state got updated without active visualization');
}
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;
},
});
};

View file

@ -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'
);
});
});
});

View file

@ -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<string>();
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<DataPublicPluginStart> {
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',