diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx index 787c6766592ab..c127e9f1130d3 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx @@ -13,6 +13,7 @@ import { LensByReferenceInput, LensSavedObjectAttributes, LensUnwrapResult, + LensEmbeddableDeps, } from './embeddable'; import { ReactExpressionRendererProps } from '@kbn/expressions-plugin/public'; import { spacesPluginMock } from '@kbn/spaces-plugin/public/mocks'; @@ -149,49 +150,53 @@ describe('embeddable', () => { mountpoint.remove(); }); + function getEmbeddableProps(props: Partial = {}): LensEmbeddableDeps { + return { + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService, + data: dataMock, + uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, + inspector: inspectorPluginMock.createStartContract(), + expressionRenderer, + coreStart: {} as CoreStart, + basePath, + dataViews: { + get: (id: string) => Promise.resolve({ id, isTimeBased: () => false }), + } as unknown as DataViewsContract, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + discover: {}, + navLinks: {}, + }, + getTrigger, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, + injectFilterReferences: jest.fn(mockInjectFilterReferences), + theme: themeServiceMock.createStartContract(), + documentToExpression: () => + Promise.resolve({ + ast: { + type: 'expression', + chain: [ + { type: 'function', function: 'my', arguments: {} }, + { type: 'function', function: 'expression', arguments: {} }, + ], + }, + indexPatterns: {}, + indexPatternRefs: [], + }), + ...props, + }; + } + it('should render expression once with expression renderer', async () => { - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - inspector: inspectorPluginMock.createStartContract(), - getTrigger, - theme: themeServiceMock.createStartContract(), - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, + const embeddable = new Embeddable(getEmbeddableProps(), { + timeRange: { + from: 'now-15m', + to: 'now', }, - { - timeRange: { - from: 'now-15m', - to: 'now', - }, - } as LensEmbeddableInput - ); + } as LensEmbeddableInput); embeddable.render(mountpoint); // wait one tick to give embeddable time to initialize @@ -203,48 +208,12 @@ describe('embeddable', () => { }); it('should not throw if render is called after destroy', async () => { - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - inspector: inspectorPluginMock.createStartContract(), - getTrigger, - theme: themeServiceMock.createStartContract(), - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, + const embeddable = new Embeddable(getEmbeddableProps(), { + timeRange: { + from: 'now-15m', + to: 'now', }, - { - timeRange: { - from: 'now-15m', - to: 'now', - }, - } as LensEmbeddableInput - ); + } as LensEmbeddableInput); let renderCalled = false; let renderThrew = false; // destroying completes output synchronously which might make a synchronous render call - this shouldn't throw @@ -263,48 +232,12 @@ describe('embeddable', () => { }); it('should render once even if reload is called before embeddable is fully initialized', async () => { - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - dataViews: {} as DataViewsContract, - inspector: inspectorPluginMock.createStartContract(), - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - theme: themeServiceMock.createStartContract(), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), + const embeddable = new Embeddable(getEmbeddableProps(), { + timeRange: { + from: 'now-15m', + to: 'now', }, - { - timeRange: { - from: 'now-15m', - to: 'now', - }, - } as LensEmbeddableInput - ); + } as LensEmbeddableInput); await embeddable.reload(); expect(expressionRenderer).toHaveBeenCalledTimes(0); embeddable.render(mountpoint); @@ -317,43 +250,7 @@ describe('embeddable', () => { }); it('should not render the visualization if any error arises', async () => { - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - theme: themeServiceMock.createStartContract(), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), - }, - {} as LensEmbeddableInput - ); + const embeddable = new Embeddable(getEmbeddableProps(), {} as LensEmbeddableInput); jest.spyOn(embeddable, 'getUserMessages').mockReturnValue([ { @@ -393,41 +290,10 @@ describe('embeddable', () => { <>getEmbeddableLegacyUrlConflict )); const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - inspector: inspectorPluginMock.createStartContract(), - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - dataViews: {} as DataViewsContract, + getEmbeddableProps({ spaces: spacesPluginStart, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - theme: themeServiceMock.createStartContract(), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), - }, + attributeService, + }), {} as LensEmbeddableInput ); await embeddable.initializeSavedVis({} as LensEmbeddableInput); @@ -437,14 +303,50 @@ describe('embeddable', () => { }); it('should not render if timeRange prop is not passed when a referenced data view is time based', async () => { - attributeService = attributeServiceMockFromSavedVis({ - ...savedVis, - references: [ - { type: 'index-pattern', id: '123', name: 'abc' }, - { type: 'index-pattern', id: '123', name: 'def' }, - { type: 'index-pattern', id: '456', name: 'ghi' }, - ], - }); + const embeddable = new Embeddable( + getEmbeddableProps({ + attributeService: attributeServiceMockFromSavedVis({ + ...savedVis, + references: [ + { type: 'index-pattern', id: '123', name: 'abc' }, + { type: 'index-pattern', id: '123', name: 'def' }, + { type: 'index-pattern', id: '456', name: 'ghi' }, + ], + }), + dataViews: { + get: (id: string) => Promise.resolve({ id, isTimeBased: () => true }), + } as unknown as DataViewsContract, + }), + {} as LensEmbeddableInput + ); + await embeddable.initializeSavedVis({} as LensEmbeddableInput); + embeddable.render(mountpoint); + expect(expressionRenderer).toHaveBeenCalledTimes(0); + }); + + it('should initialize output with deduped list of index patterns', async () => { + const embeddable = new Embeddable( + getEmbeddableProps({ + attributeService: attributeServiceMockFromSavedVis({ + ...savedVis, + references: [ + { type: 'index-pattern', id: '123', name: 'abc' }, + { type: 'index-pattern', id: '123', name: 'def' }, + { type: 'index-pattern', id: '456', name: 'ghi' }, + ], + }), + }), + {} as LensEmbeddableInput + ); + + await embeddable.initializeSavedVis({} as LensEmbeddableInput); + const outputIndexPatterns = embeddable.getOutput().indexPatterns!; + expect(outputIndexPatterns.length).toEqual(2); + expect(outputIndexPatterns[0].id).toEqual('123'); + expect(outputIndexPatterns[1].id).toEqual('456'); + }); + + it('should re-render once on filter change', async () => { const embeddable = new Embeddable( { timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, @@ -455,9 +357,7 @@ describe('embeddable', () => { coreStart: {} as CoreStart, basePath, inspector: inspectorPluginMock.createStartContract(), - dataViews: { - get: (id: string) => Promise.resolve({ id, isTimeBased: jest.fn(() => true) }), - } as unknown as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, @@ -482,22 +382,23 @@ describe('embeddable', () => { indexPatternRefs: [], }), }, - {} as LensEmbeddableInput + { id: '123' } as LensEmbeddableInput ); - await embeddable.initializeSavedVis({} as LensEmbeddableInput); + await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); embeddable.render(mountpoint); - expect(expressionRenderer).toHaveBeenCalledTimes(0); - }); - it('should initialize output with deduped list of index patterns', async () => { - attributeService = attributeServiceMockFromSavedVis({ - ...savedVis, - references: [ - { type: 'index-pattern', id: '123', name: 'abc' }, - { type: 'index-pattern', id: '123', name: 'def' }, - { type: 'index-pattern', id: '456', name: 'ghi' }, - ], + expect(expressionRenderer).toHaveBeenCalledTimes(1); + + embeddable.updateInput({ + filters: [{ meta: { alias: 'test', negate: false, disabled: false } }], }); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(expressionRenderer).toHaveBeenCalledTimes(2); + }); + + it('should re-render once on search session change', async () => { const embeddable = new Embeddable( { timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, @@ -508,9 +409,7 @@ describe('embeddable', () => { coreStart: {} as CoreStart, basePath, inspector: inspectorPluginMock.createStartContract(), - dataViews: { - get: (id: string) => Promise.resolve({ id, isTimeBased: () => false }), - } as unknown as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, @@ -535,104 +434,7 @@ describe('embeddable', () => { indexPatternRefs: [], }), }, - {} as LensEmbeddableInput - ); - await embeddable.initializeSavedVis({} as LensEmbeddableInput); - const outputIndexPatterns = embeddable.getOutput().indexPatterns!; - expect(outputIndexPatterns.length).toEqual(2); - expect(outputIndexPatterns[0].id).toEqual('123'); - expect(outputIndexPatterns[1].id).toEqual('456'); - }); - - it('should re-render once on filter change', async () => { - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - theme: themeServiceMock.createStartContract(), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), - }, - { id: '123' } as LensEmbeddableInput - ); - await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); - embeddable.render(mountpoint); - - expect(expressionRenderer).toHaveBeenCalledTimes(1); - - embeddable.updateInput({ - filters: [{ meta: { alias: 'test', negate: false, disabled: false } }], - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(expressionRenderer).toHaveBeenCalledTimes(2); - }); - - it('should re-render once on search session change', async () => { - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - theme: themeServiceMock.createStartContract(), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), - }, - { id: '123', searchSessionId: 'firstSession' } as LensEmbeddableInput + { id: '123', searchSessionId: 'firstSession' } as LensEmbeddableInput ); await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); embeddable.render(mountpoint); @@ -659,43 +461,7 @@ describe('embeddable', () => { dynamicActions: {}, }, } as unknown as LensEmbeddableInput; - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - theme: themeServiceMock.createStartContract(), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), - }, - { id: '123' } as LensEmbeddableInput - ); + const embeddable = new Embeddable(getEmbeddableProps(), { id: '123' } as LensEmbeddableInput); await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); embeddable.render(mountpoint); @@ -716,43 +482,7 @@ describe('embeddable', () => { }); it('should re-render when dynamic actions input changes', async () => { - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - theme: themeServiceMock.createStartContract(), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), - }, - { id: '123' } as LensEmbeddableInput - ); + const embeddable = new Embeddable(getEmbeddableProps(), { id: '123' } as LensEmbeddableInput); await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); embeddable.render(mountpoint); @@ -780,43 +510,7 @@ describe('embeddable', () => { searchSessionId: 'searchSessionId', } as LensEmbeddableInput; - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - theme: themeServiceMock.createStartContract(), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), - }, - input - ); + const embeddable = new Embeddable(getEmbeddableProps(), input); await embeddable.initializeSavedVis(input); embeddable.render(mountpoint); @@ -845,43 +539,7 @@ describe('embeddable', () => { disableTriggers: true, } as LensEmbeddableInput; - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - theme: themeServiceMock.createStartContract(), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), - }, - input - ); + const embeddable = new Embeddable(getEmbeddableProps(), input); await embeddable.initializeSavedVis(input); embeddable.render(mountpoint); @@ -909,45 +567,11 @@ describe('embeddable', () => { }, references: [{ type: 'index-pattern', name: 'filter-0', id: 'my-index-pattern-id' }], }; - attributeService = attributeServiceMockFromSavedVis(newSavedVis); const input = { savedObjectId: '123', timeRange, query, filters } as LensEmbeddableInput; const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: { get: jest.fn() } as unknown as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - theme: themeServiceMock.createStartContract(), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), - }, + getEmbeddableProps({ attributeService: attributeServiceMockFromSavedVis(newSavedVis) }), input ); await embeddable.initializeSavedVis(input); @@ -966,43 +590,7 @@ describe('embeddable', () => { }); it('should execute trigger on event from expression renderer', async () => { - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - theme: themeServiceMock.createStartContract(), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), - }, - { id: '123' } as LensEmbeddableInput - ); + const embeddable = new Embeddable(getEmbeddableProps(), { id: '123' } as LensEmbeddableInput); await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); embeddable.render(mountpoint); @@ -1012,104 +600,37 @@ describe('embeddable', () => { onEvent({ name: 'brush', data: eventData }); expect(getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.brush); - expect(trigger.exec).toHaveBeenCalledWith( - expect.objectContaining({ - data: { ...eventData, timeFieldName: undefined }, - embeddable: expect.anything(), - }) - ); - }); - - it('should execute trigger on row click event from expression renderer', async () => { - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - theme: themeServiceMock.createStartContract(), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), - }, - { id: '123' } as LensEmbeddableInput - ); - await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); - embeddable.render(mountpoint); - - const onEvent = expressionRenderer.mock.calls[0][0].onEvent!; - - onEvent({ name: 'tableRowContextMenuClick', data: {} }); - - expect(getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.tableRowContextMenuClick); - }); - - it('should not re-render if only change is in disabled filter', async () => { - const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; - const query: Query = { language: 'kquery', query: '' }; - const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: true } }]; - - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - theme: themeServiceMock.createStartContract(), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), - }, - { id: '123', timeRange, query, filters } as LensEmbeddableInput + expect(trigger.exec).toHaveBeenCalledWith( + expect.objectContaining({ + data: { ...eventData, timeFieldName: undefined }, + embeddable: expect.anything(), + }) ); + }); + + it('should execute trigger on row click event from expression renderer', async () => { + const embeddable = new Embeddable(getEmbeddableProps(), { id: '123' } as LensEmbeddableInput); + await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); + embeddable.render(mountpoint); + + const onEvent = expressionRenderer.mock.calls[0][0].onEvent!; + + onEvent({ name: 'tableRowContextMenuClick', data: {} }); + + expect(getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.tableRowContextMenuClick); + }); + + it('should not re-render if only change is in disabled filter', async () => { + const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; + const query: Query = { language: 'kquery', query: '' }; + const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: true } }]; + + const embeddable = new Embeddable(getEmbeddableProps(), { + id: '123', + timeRange, + query, + filters, + } as LensEmbeddableInput); await embeddable.initializeSavedVis({ id: '123', timeRange, @@ -1142,43 +663,10 @@ describe('embeddable', () => { return null; }); - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - theme: themeServiceMock.createStartContract(), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), - }, - { id: '123', onLoad } as unknown as LensEmbeddableInput - ); + const embeddable = new Embeddable(getEmbeddableProps({ expressionRenderer }), { + id: '123', + onLoad, + } as unknown as LensEmbeddableInput); await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); embeddable.render(mountpoint); @@ -1226,43 +714,10 @@ describe('embeddable', () => { return null; }); - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - theme: themeServiceMock.createStartContract(), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), - }, - { id: '123', onFilter } as unknown as LensEmbeddableInput - ); + const embeddable = new Embeddable(getEmbeddableProps({ expressionRenderer }), { + id: '123', + onFilter, + } as unknown as LensEmbeddableInput); await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); embeddable.render(mountpoint); @@ -1273,6 +728,33 @@ describe('embeddable', () => { expect(onFilter).toHaveBeenCalledTimes(1); }); + it('should prevent the onFilter trigger when calling preventDefault', async () => { + const onFilter = jest.fn(({ preventDefault }) => preventDefault()); + + expressionRenderer = jest.fn(({ onEvent }) => { + setTimeout(() => { + onEvent?.({ + name: 'filter', + data: { pings: false, table: { rows: [], columns: [] }, column: 0 }, + }); + }, 10); + + return null; + }); + + const embeddable = new Embeddable(getEmbeddableProps({ expressionRenderer }), { + id: '123', + onFilter, + } as unknown as LensEmbeddableInput); + + await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); + embeddable.render(mountpoint); + + await new Promise((resolve) => setTimeout(resolve, 20)); + + expect(getTrigger).not.toHaveBeenCalled(); + }); + it('should call onBrush event on brushing', async () => { const onBrushEnd = jest.fn(); @@ -1287,43 +769,10 @@ describe('embeddable', () => { return null; }); - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - theme: themeServiceMock.createStartContract(), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), - }, - { id: '123', onBrushEnd } as unknown as LensEmbeddableInput - ); + const embeddable = new Embeddable(getEmbeddableProps({ expressionRenderer }), { + id: '123', + onBrushEnd, + } as unknown as LensEmbeddableInput); await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); embeddable.render(mountpoint); @@ -1334,6 +783,33 @@ describe('embeddable', () => { expect(onBrushEnd).toHaveBeenCalledTimes(1); }); + it('should prevent the onBrush trigger when calling preventDefault', async () => { + const onBrushEnd = jest.fn(({ preventDefault }) => preventDefault()); + + expressionRenderer = jest.fn(({ onEvent }) => { + setTimeout(() => { + onEvent?.({ + name: 'brush', + data: { range: [0, 1], table: { rows: [], columns: [] }, column: 0 }, + }); + }, 10); + + return null; + }); + + const embeddable = new Embeddable(getEmbeddableProps({ expressionRenderer }), { + id: '123', + onBrushEnd, + } as unknown as LensEmbeddableInput); + + await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); + embeddable.render(mountpoint); + + await new Promise((resolve) => setTimeout(resolve, 20)); + + expect(getTrigger).not.toHaveBeenCalled(); + }); + it('should call onTableRowClick event ', async () => { const onTableRowClick = jest.fn(); @@ -1345,53 +821,44 @@ describe('embeddable', () => { return null; }); - const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService, - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - visualizationMap: defaultVisualizationMap, - datasourceMap: defaultDatasourceMap, - injectFilterReferences: jest.fn(mockInjectFilterReferences), - theme: themeServiceMock.createStartContract(), - documentToExpression: () => - Promise.resolve({ - ast: { - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }, - indexPatterns: {}, - indexPatternRefs: [], - }), - }, - { id: '123', onTableRowClick } as unknown as LensEmbeddableInput - ); + const embeddable = new Embeddable(getEmbeddableProps({ expressionRenderer }), { + id: '123', + onTableRowClick, + } as unknown as LensEmbeddableInput); await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); embeddable.render(mountpoint); await new Promise((resolve) => setTimeout(resolve, 20)); - expect(onTableRowClick).toHaveBeenCalledWith({ name: 'test' }); + expect(onTableRowClick).toHaveBeenCalledWith(expect.objectContaining({ name: 'test' })); expect(onTableRowClick).toHaveBeenCalledTimes(1); }); + it('should prevent onTableRowClick trigger when calling preventDefault ', async () => { + const onTableRowClick = jest.fn(({ preventDefault }) => preventDefault()); + + expressionRenderer = jest.fn(({ onEvent }) => { + setTimeout(() => { + onEvent?.({ name: 'tableRowContextMenuClick', data: { name: 'test' } }); + }, 10); + + return null; + }); + + const embeddable = new Embeddable(getEmbeddableProps({ expressionRenderer }), { + id: '123', + onTableRowClick, + } as unknown as LensEmbeddableInput); + + await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); + embeddable.render(mountpoint); + + await new Promise((resolve) => setTimeout(resolve, 20)); + + expect(getTrigger).not.toHaveBeenCalled(); + }); + it('handles edit actions ', async () => { const editedVisualizationState = { value: 'edited' }; const onEditActionMock = jest.fn().mockReturnValue(editedVisualizationState); @@ -1426,34 +893,16 @@ describe('embeddable', () => { }; const embeddable = new Embeddable( - { - timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + getEmbeddableProps({ attributeService: attributeServiceMockFromSavedVis(visDocument), - data: dataMock, - uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, - expressionRenderer, - coreStart: {} as CoreStart, - basePath, - inspector: inspectorPluginMock.createStartContract(), - dataViews: {} as DataViewsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - discover: {}, - navLinks: {}, - }, - getTrigger, - theme: themeServiceMock.createStartContract(), - injectFilterReferences: jest.fn(mockInjectFilterReferences), visualizationMap: { [visDocument.visualizationType as string]: { onEditAction: onEditActionMock, initialize: () => {}, } as unknown as Visualization, }, - datasourceMap: defaultDatasourceMap, documentToExpression: documentToExpressionMock, - }, + }), { id: '123' } as unknown as LensEmbeddableInput ); diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 519db972ca855..0f8594347a34b 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -138,6 +138,12 @@ export interface LensUnwrapResult { metaInfo?: LensUnwrapMetaInfo; } +interface PreventableEvent { + preventDefault(): void; +} + +export type Simplify = { [KeyType in keyof T]: T[KeyType] } & {}; + interface LensBaseEmbeddableInput extends EmbeddableInput { filters?: Filter[]; query?: Query; @@ -148,10 +154,14 @@ interface LensBaseEmbeddableInput extends EmbeddableInput { style?: React.CSSProperties; className?: string; noPadding?: boolean; - onBrushEnd?: (data: BrushTriggerEvent['data']) => void; + onBrushEnd?: (data: Simplify) => void; onLoad?: (isLoading: boolean, adapters?: Partial) => void; - onFilter?: (data: ClickTriggerEvent['data'] | MultiClickTriggerEvent['data']) => void; - onTableRowClick?: (data: LensTableRowContextMenuEvent['data']) => void; + onFilter?: ( + data: Simplify<(ClickTriggerEvent['data'] | MultiClickTriggerEvent['data']) & PreventableEvent> + ) => void; + onTableRowClick?: ( + data: Simplify + ) => void; } export type LensByValueInput = { @@ -1102,45 +1112,68 @@ export class Embeddable return; } if (isLensBrushEvent(event)) { - this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ - data: { - ...event.data, - timeFieldName: - event.data.timeFieldName || - inferTimeField(this.deps.data.datatableUtilities, event.data), - }, - embeddable: this, - }); - + let shouldExecuteDefaultTriggers = true; if (this.input.onBrushEnd) { - this.input.onBrushEnd(event.data); + this.input.onBrushEnd({ + ...event.data, + preventDefault: () => { + shouldExecuteDefaultTriggers = false; + }, + }); + } + if (shouldExecuteDefaultTriggers) { + this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ + data: { + ...event.data, + timeFieldName: + event.data.timeFieldName || + inferTimeField(this.deps.data.datatableUtilities, event.data), + }, + embeddable: this, + }); } } if (isLensFilterEvent(event) || isLensMultiFilterEvent(event)) { - this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ - data: { - ...event.data, - timeFieldName: - event.data.timeFieldName || - inferTimeField(this.deps.data.datatableUtilities, event.data), - }, - embeddable: this, - }); + let shouldExecuteDefaultTriggers = true; if (this.input.onFilter) { - this.input.onFilter(event.data); + this.input.onFilter({ + ...event.data, + preventDefault: () => { + shouldExecuteDefaultTriggers = false; + }, + }); + } + if (shouldExecuteDefaultTriggers) { + this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ + data: { + ...event.data, + timeFieldName: + event.data.timeFieldName || + inferTimeField(this.deps.data.datatableUtilities, event.data), + }, + embeddable: this, + }); } } if (isLensTableRowContextMenuClickEvent(event)) { - this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec( - { - data: event.data, - embeddable: this, - }, - true - ); + let shouldExecuteDefaultTriggers = true; if (this.input.onTableRowClick) { - this.input.onTableRowClick(event.data as unknown as LensTableRowContextMenuEvent['data']); + this.input.onTableRowClick({ + ...event.data, + preventDefault: () => { + shouldExecuteDefaultTriggers = false; + }, + }); + } + if (shouldExecuteDefaultTriggers) { + this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec( + { + data: event.data, + embeddable: this, + }, + true + ); } } diff --git a/x-pack/plugins/lens/readme.md b/x-pack/plugins/lens/readme.md index 9fec3f154fbf3..b3768ba0b2540 100644 --- a/x-pack/plugins/lens/readme.md +++ b/x-pack/plugins/lens/readme.md @@ -98,6 +98,22 @@ Filters and query `state.filters`/`state.query` define the visualization-global The `EmbeddableComponent` also takes a set of callbacks to react to user interactions with the embedded Lens visualization to integrate the visualization with the surrounding app: `onLoad`, `onBrushEnd`, `onFilter`, `onTableRowClick`. A common pattern is to keep state in the solution app which is updated within these callbacks - re-rendering the surrounding application will change the Lens attributes passed to the component which will re-render the visualization (including re-fetching data if necessary). +#### Preventing defaults + +In some scenarios it can be useful to customize the default behaviour and avoid the default Kibana triggers for a specific action, like add a filter on a bar chart/pie slice click. For this specific requirement the `data` object returned by the `onBrushEnd`, `onFilter`, `onTableRowClick` callbacks has a special `preventDefault()` function that will prevent other registered event triggers to execute: + +```tsx + { + // custom behaviour on "filter" event + ... + // now prevent to add a filter in Kibana + data.preventDefault(); + }} +/> +``` + ## Handling data views In most cases it makes sense to have a data view saved object to use the Lens embeddable. Use the data view service to find an existing data view for a given index pattern or create a new one if it doesn't exist yet: