diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md index 48d94b84497bd9..cc40ab8bb1173f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md @@ -9,33 +9,9 @@ Returns index pattern as saved object body for saving Signature: ```typescript -getAsSavedObjectBody(): { - fieldAttrs: string | undefined; - title: string; - timeFieldName: string | undefined; - intervalName: string | undefined; - sourceFilters: string | undefined; - fields: string | undefined; - fieldFormatMap: string | undefined; - type: string | undefined; - typeMeta: string | undefined; - allowNoIndex: true | undefined; - runtimeFieldMap: string | undefined; - }; +getAsSavedObjectBody(): IndexPatternAttributes; ``` Returns: -`{ - fieldAttrs: string | undefined; - title: string; - timeFieldName: string | undefined; - intervalName: string | undefined; - sourceFilters: string | undefined; - fields: string | undefined; - fieldFormatMap: string | undefined; - type: string | undefined; - typeMeta: string | undefined; - allowNoIndex: true | undefined; - runtimeFieldMap: string | undefined; - }` +`IndexPatternAttributes` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md index 668d563ff04c0e..f5e87638e2f1ca 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md @@ -9,33 +9,9 @@ Returns index pattern as saved object body for saving Signature: ```typescript -getAsSavedObjectBody(): { - fieldAttrs: string | undefined; - title: string; - timeFieldName: string | undefined; - intervalName: string | undefined; - sourceFilters: string | undefined; - fields: string | undefined; - fieldFormatMap: string | undefined; - type: string | undefined; - typeMeta: string | undefined; - allowNoIndex: true | undefined; - runtimeFieldMap: string | undefined; - }; +getAsSavedObjectBody(): IndexPatternAttributes; ``` Returns: -`{ - fieldAttrs: string | undefined; - title: string; - timeFieldName: string | undefined; - intervalName: string | undefined; - sourceFilters: string | undefined; - fields: string | undefined; - fieldFormatMap: string | undefined; - type: string | undefined; - typeMeta: string | undefined; - allowNoIndex: true | undefined; - runtimeFieldMap: string | undefined; - }` +`IndexPatternAttributes` diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 1552bed210e8c9..c897cdbce2309a 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -7,7 +7,7 @@ */ import _, { each, reject } from 'lodash'; -import { FieldAttrs, FieldAttrSet } from '../..'; +import { FieldAttrs, FieldAttrSet, IndexPatternAttributes } from '../..'; import type { RuntimeField } from '../types'; import { DuplicateField } from '../../../../kibana_utils/common'; @@ -318,7 +318,7 @@ export class IndexPattern implements IIndexPattern { /** * Returns index pattern as saved object body for saving */ - getAsSavedObjectBody() { + getAsSavedObjectBody(): IndexPatternAttributes { const fieldFormatMap = _.isEmpty(this.fieldFormatMap) ? undefined : JSON.stringify(this.fieldFormatMap); @@ -331,12 +331,10 @@ export class IndexPattern implements IIndexPattern { timeFieldName: this.timeFieldName, intervalName: this.intervalName, sourceFilters: this.sourceFilters ? JSON.stringify(this.sourceFilters) : undefined, - fields: this.fields - ? JSON.stringify(this.fields.filter((field) => field.scripted)) - : undefined, + fields: JSON.stringify(this.fields?.filter((field) => field.scripted) ?? []), fieldFormatMap, - type: this.type, - typeMeta: this.typeMeta ? JSON.stringify(this.typeMeta) : undefined, + type: this.type!, + typeMeta: JSON.stringify(this.typeMeta ?? {}), allowNoIndex: this.allowNoIndex ? this.allowNoIndex : undefined, runtimeFieldMap: runtimeFieldMap ? JSON.stringify(runtimeFieldMap) : undefined, }; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts index a4f37334c212ee..8715f8feb067a0 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts @@ -230,7 +230,12 @@ describe('IndexPatterns', () => { test('createAndSave', async () => { const title = 'kibana-*'; - indexPatterns.createSavedObject = jest.fn(); + + indexPatterns.createSavedObject = jest.fn(() => + Promise.resolve(({ + id: 'id', + } as unknown) as IndexPattern) + ); indexPatterns.setDefault = jest.fn(); await indexPatterns.createAndSave({ title }); expect(indexPatterns.createSavedObject).toBeCalled(); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 66e66051a6370e..e67e72f295b8ec 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -403,6 +403,12 @@ export class IndexPatternsService { throw new SavedObjectNotFound(savedObjectType, id, 'management/kibana/indexPatterns'); } + return this.initFromSavedObject(savedObject); + }; + + private initFromSavedObject = async ( + savedObject: SavedObject + ): Promise => { const spec = this.savedObjectToSpec(savedObject); const { title, type, typeMeta, runtimeFieldMap } = spec; spec.fieldAttrs = savedObject.attributes.fieldAttrs @@ -412,7 +418,7 @@ export class IndexPatternsService { try { spec.fields = await this.refreshFieldSpecMap( spec.fields || {}, - id, + savedObject.id, spec.title as string, { pattern: title as string, @@ -423,6 +429,7 @@ export class IndexPatternsService { }, spec.fieldAttrs ); + // CREATE RUNTIME FIELDS for (const [key, value] of Object.entries(runtimeFieldMap || {})) { // do not create runtime field if mapped field exists @@ -450,7 +457,7 @@ export class IndexPatternsService { this.onError(err, { title: i18n.translate('data.indexPatterns.fetchFieldErrorTitle', { defaultMessage: 'Error fetching fields for index pattern {title} (ID: {id})', - values: { id, title }, + values: { id: savedObject.id, title }, }), }); } @@ -516,9 +523,9 @@ export class IndexPatternsService { async createAndSave(spec: IndexPatternSpec, override = false, skipFetchFields = false) { const indexPattern = await this.create(spec, skipFetchFields); - await this.createSavedObject(indexPattern, override); - await this.setDefault(indexPattern.id!); - return indexPattern; + const createdIndexPattern = await this.createSavedObject(indexPattern, override); + await this.setDefault(createdIndexPattern.id!); + return createdIndexPattern; } /** @@ -538,15 +545,20 @@ export class IndexPatternsService { } const body = indexPattern.getAsSavedObjectBody(); - const response = await this.savedObjectsClient.create(savedObjectType, body, { - id: indexPattern.id, - }); - indexPattern.id = response.id; - this.indexPatternCache.set(indexPattern.id, Promise.resolve(indexPattern)); + const response: SavedObject = (await this.savedObjectsClient.create( + savedObjectType, + body, + { + id: indexPattern.id, + } + )) as SavedObject; + + const createdIndexPattern = await this.initFromSavedObject(response); + this.indexPatternCache.set(createdIndexPattern.id!, Promise.resolve(createdIndexPattern)); if (this.savedObjectsCache) { this.savedObjectsCache.push(response as SavedObject); } - return indexPattern; + return createdIndexPattern; } /** diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 069b0a21c9c774..be6e489b17290d 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1336,19 +1336,7 @@ export class IndexPattern implements IIndexPattern { delay?: string | undefined; time_zone?: string | undefined; }>> | undefined; - getAsSavedObjectBody(): { - fieldAttrs: string | undefined; - title: string; - timeFieldName: string | undefined; - intervalName: string | undefined; - sourceFilters: string | undefined; - fields: string | undefined; - fieldFormatMap: string | undefined; - type: string | undefined; - typeMeta: string | undefined; - allowNoIndex: true | undefined; - runtimeFieldMap: string | undefined; - }; + getAsSavedObjectBody(): IndexPatternAttributes; // (undocumented) getComputedFields(): { storedFields: string[]; diff --git a/src/plugins/data/server/index_patterns/routes/create_index_pattern.ts b/src/plugins/data/server/index_patterns/routes/create_index_pattern.ts index 0be300d1844598..d0767334626229 100644 --- a/src/plugins/data/server/index_patterns/routes/create_index_pattern.ts +++ b/src/plugins/data/server/index_patterns/routes/create_index_pattern.ts @@ -15,9 +15,8 @@ import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin' const indexPatternSpecSchema = schema.object({ title: schema.string(), - - id: schema.maybe(schema.string()), version: schema.maybe(schema.string()), + id: schema.maybe(schema.string()), type: schema.maybe(schema.string()), timeFieldName: schema.maybe(schema.string()), sourceFilters: schema.maybe( diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 8821db0d65f282..8ec412e69d4a10 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -780,19 +780,7 @@ export class IndexPattern implements IIndexPattern { delay?: string | undefined; time_zone?: string | undefined; }>> | undefined; - getAsSavedObjectBody(): { - fieldAttrs: string | undefined; - title: string; - timeFieldName: string | undefined; - intervalName: string | undefined; - sourceFilters: string | undefined; - fields: string | undefined; - fieldFormatMap: string | undefined; - type: string | undefined; - typeMeta: string | undefined; - allowNoIndex: true | undefined; - runtimeFieldMap: string | undefined; - }; + getAsSavedObjectBody(): IndexPatternAttributes; // (undocumented) getComputedFields(): { storedFields: string[]; diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/main.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/main.ts index 1f94e60430c6b7..91f165dbdda7cc 100644 --- a/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/main.ts +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/main.ts @@ -11,6 +11,7 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); describe('main', () => { it('can create an index_pattern with just a title', async () => { @@ -45,7 +46,6 @@ export default function ({ getService }: FtrProviderContext) { index_pattern: { title, id, - version: 'test-version', type: 'test-type', timeFieldName: 'test-timeFieldName', }, @@ -54,7 +54,6 @@ export default function ({ getService }: FtrProviderContext) { expect(response.status).to.be(200); expect(response.body.index_pattern.title).to.be(title); expect(response.body.index_pattern.id).to.be(id); - expect(response.body.index_pattern.version).to.be('test-version'); expect(response.body.index_pattern.type).to.be('test-type'); expect(response.body.index_pattern.timeFieldName).to.be('test-timeFieldName'); }); @@ -77,60 +76,85 @@ export default function ({ getService }: FtrProviderContext) { expect(response.body.index_pattern.sourceFilters[0].value).to.be('foo'); }); - it('can specify optional fields attribute when creating an index pattern', async () => { - const title = `foo-${Date.now()}-${Math.random()}*`; - const response = await supertest.post('/api/index_patterns/index_pattern').send({ - index_pattern: { - title, - fields: { - foo: { - name: 'foo', - type: 'string', - }, - }, - }, + describe('creating fields', () => { + before(async () => { + await esArchiver.load('index_patterns/basic_index'); }); - expect(response.status).to.be(200); - expect(response.body.index_pattern.title).to.be(title); - expect(response.body.index_pattern.fields.foo.name).to.be('foo'); - expect(response.body.index_pattern.fields.foo.type).to.be('string'); - }); + after(async () => { + await esArchiver.unload('index_patterns/basic_index'); + }); - it('can add two fields, one with all fields specified', async () => { - const title = `foo-${Date.now()}-${Math.random()}*`; - const response = await supertest.post('/api/index_patterns/index_pattern').send({ - index_pattern: { - title, - fields: { - foo: { - name: 'foo', - type: 'string', - }, - bar: { - name: 'bar', - type: 'number', - count: 123, - script: '', - esTypes: ['test-type'], - scripted: true, + it('can specify optional fields attribute when creating an index pattern', async () => { + const title = `basic_index*`; + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + fields: { + foo: { + name: 'foo', + type: 'string', + scripted: true, + script: "doc['field_name'].value", + }, }, }, - }, + }); + + expect(response.status).to.be(200); + expect(response.body.index_pattern.title).to.be(title); + expect(response.body.index_pattern.fields.foo.name).to.be('foo'); + expect(response.body.index_pattern.fields.foo.type).to.be('string'); + expect(response.body.index_pattern.fields.foo.scripted).to.be(true); + expect(response.body.index_pattern.fields.foo.script).to.be("doc['field_name'].value"); + + expect(response.body.index_pattern.fields.bar.name).to.be('bar'); // created from es index + expect(response.body.index_pattern.fields.bar.type).to.be('boolean'); }); - expect(response.status).to.be(200); - expect(response.body.index_pattern.title).to.be(title); + it('Can add scripted fields, other fields created from es index', async () => { + const title = `basic_index*`; + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + fields: { + foo: { + name: 'foo', + type: 'string', + }, + fake: { + name: 'fake', + type: 'string', + }, + bar: { + name: 'bar', + type: 'number', + count: 123, + script: '', + esTypes: ['test-type'], + scripted: true, + }, + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body.index_pattern.title).to.be(title); - expect(response.body.index_pattern.fields.foo.name).to.be('foo'); - expect(response.body.index_pattern.fields.foo.type).to.be('string'); + expect(response.body.index_pattern.fields.foo.name).to.be('foo'); + expect(response.body.index_pattern.fields.foo.type).to.be('number'); // picked up from index - expect(response.body.index_pattern.fields.bar.name).to.be('bar'); - expect(response.body.index_pattern.fields.bar.type).to.be('number'); - expect(response.body.index_pattern.fields.bar.count).to.be(123); - expect(response.body.index_pattern.fields.bar.script).to.be(''); - expect(response.body.index_pattern.fields.bar.esTypes[0]).to.be('test-type'); - expect(response.body.index_pattern.fields.bar.scripted).to.be(true); + expect(response.body.index_pattern.fields.fake).to.be(undefined); // not in index, so not created + + expect(response.body.index_pattern.fields.bar.name).to.be('bar'); + expect(response.body.index_pattern.fields.bar.type).to.be('number'); + expect(response.body.index_pattern.fields.bar.count).to.be(123); + expect(response.body.index_pattern.fields.bar.script).to.be(''); + expect(response.body.index_pattern.fields.bar.esTypes[0]).to.be('test-type'); + expect(response.body.index_pattern.fields.bar.scripted).to.be(true); + }); }); it('can specify optional typeMeta attribute when creating an index pattern', async () => { diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/main.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/main.ts index f9ab482f98b764..a5ed61d8ab9af1 100644 --- a/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/main.ts +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/main.ts @@ -25,6 +25,7 @@ export default function ({ getService }: FtrProviderContext) { it('can create a new scripted field', async () => { const title = `foo-${Date.now()}-${Math.random()}*`; const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, index_pattern: { title, }, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx index e171c457c541eb..3936fb9e1a1b12 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx @@ -119,6 +119,25 @@ describe('ConfigPanel', () => { expect(component.find(LayerPanel).exists()).toBe(false); }); + it('allow datasources and visualizations to use setters', () => { + const props = getDefaultProps(); + const component = mountWithIntl(); + const { updateDatasource, updateAll } = component.find(LayerPanel).props(); + + const updater = () => 'updated'; + updateDatasource('ds1', updater); + expect(props.dispatch).toHaveBeenCalledTimes(1); + expect(props.dispatch.mock.calls[0][0].updater(props.datasourceStates.ds1.state)).toEqual( + 'updated' + ); + + updateAll('ds1', updater, props.visualizationState); + expect(props.dispatch).toHaveBeenCalledTimes(2); + expect(props.dispatch.mock.calls[0][0].updater(props.datasourceStates.ds1.state)).toEqual( + 'updated' + ); + }); + describe('focus behavior when adding or removing layers', () => { it('should focus the only layer when resetting the layer', () => { const component = mountWithIntl(, { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index d52fd29e7233a4..c1ab2b4586ab33 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -63,7 +63,8 @@ export function LayerPanels( () => (datasourceId: string, newState: unknown) => { dispatch({ type: 'UPDATE_DATASOURCE_STATE', - updater: () => newState, + updater: (prevState: unknown) => + typeof newState === 'function' ? newState(prevState) : newState, datasourceId, clearStagedPreview: false, }); @@ -76,12 +77,16 @@ export function LayerPanels( type: 'UPDATE_STATE', subType: 'UPDATE_ALL_STATES', updater: (prevState) => { + const updatedDatasourceState = + typeof newDatasourceState === 'function' + ? newDatasourceState(prevState.datasourceStates[datasourceId].state) + : newDatasourceState; return { ...prevState, datasourceStates: { ...prevState.datasourceStates, [datasourceId]: { - state: newDatasourceState, + state: updatedDatasourceState, isLoading: false, }, }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index fcca4a41581c26..7732b53db62fb9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -100,15 +100,26 @@ export function DimensionEditor(props: DimensionEditorProps) { }; const { fieldByOperation, operationWithoutField } = operationSupportMatrix; - const setStateWrapper = (layer: IndexPatternLayer) => { - const hasIncompleteColumns = Boolean(layer.incompleteColumns?.[columnId]); + const setStateWrapper = ( + setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer) + ) => { + const hypotheticalLayer = typeof setter === 'function' ? setter(state.layers[layerId]) : setter; + const hasIncompleteColumns = Boolean(hypotheticalLayer.incompleteColumns?.[columnId]); const prevOperationType = - operationDefinitionMap[state.layers[layerId].columns[columnId]?.operationType]?.input; - setState(mergeLayer({ state, layerId, newLayer: layer }), { - shouldReplaceDimension: Boolean(layer.columns[columnId]), - // clear the dimension if there's an incomplete column pending && previous operation was a fullReference operation - shouldRemoveDimension: Boolean(hasIncompleteColumns && prevOperationType === 'fullReference'), - }); + operationDefinitionMap[hypotheticalLayer.columns[columnId]?.operationType]?.input; + setState( + (prevState) => { + const layer = typeof setter === 'function' ? setter(prevState.layers[layerId]) : setter; + return mergeLayer({ state: prevState, layerId, newLayer: layer }); + }, + { + shouldReplaceDimension: Boolean(hypotheticalLayer.columns[columnId]), + // clear the dimension if there's an incomplete column pending && previous operation was a fullReference operation + shouldRemoveDimension: Boolean( + hasIncompleteColumns && prevOperationType === 'fullReference' + ), + } + ); }; const selectedOperationDefinition = @@ -337,8 +348,19 @@ export function DimensionEditor(props: DimensionEditorProps) { key={index} layer={state.layers[layerId]} columnId={referenceId} - updateLayer={(newLayer: IndexPatternLayer) => { - setState(mergeLayer({ state, layerId, newLayer })); + updateLayer={( + setter: + | IndexPatternLayer + | ((prevLayer: IndexPatternLayer) => IndexPatternLayer) + ) => { + setState( + mergeLayer({ + state, + layerId, + newLayer: + typeof setter === 'function' ? setter(state.layers[layerId]) : setter, + }) + ); }} validation={validation} currentIndexPattern={currentIndexPattern} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 333caf259fe2f7..25cf20e304daf0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -169,7 +169,9 @@ describe('IndexPatternDimensionEditorPanel', () => { setState = jest.fn().mockImplementation((newState) => { if (wrapper instanceof ReactWrapper) { - wrapper.setProps({ state: newState }); + wrapper.setProps({ + state: typeof newState === 'function' ? newState(wrapper.prop('state')) : newState, + }); } }); @@ -495,26 +497,27 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([option]); }); - expect(setState).toHaveBeenCalledWith( - { - ...initialState, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'max', - sourceField: 'memory', - params: { format: { id: 'bytes' } }, - // Other parts of this don't matter for this test - }), - }, + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](defaultProps.state)).toEqual({ + ...initialState, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'max', + sourceField: 'memory', + params: { format: { id: 'bytes' } }, + // Other parts of this don't matter for this test + }), }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should switch operations when selecting a field that requires another operation', () => { @@ -529,25 +532,26 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([option]); }); - expect(setState).toHaveBeenCalledWith( - { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'terms', - sourceField: 'source', - // Other parts of this don't matter for this test - }), - }, + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](defaultProps.state)).toEqual({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'terms', + sourceField: 'source', + // Other parts of this don't matter for this test + }), }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should keep the field when switching to another operation compatible for this field', () => { @@ -562,26 +566,27 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); }); - expect(setState).toHaveBeenCalledWith( - { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'min', - sourceField: 'bytes', - params: { format: { id: 'bytes' } }, - // Other parts of this don't matter for this test - }), - }, + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](state)).toEqual({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'min', + sourceField: 'bytes', + params: { format: { id: 'bytes' } }, + // Other parts of this don't matter for this test + }), }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should not set the state if selecting the currently active operation', () => { @@ -635,23 +640,24 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); }); - expect(setState).toHaveBeenCalledWith( - { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - label: 'Minimum of bytes', - }), - }, + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](state)).toEqual({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + label: 'Minimum of bytes', + }), }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should keep the label on operation change if it is custom', () => { @@ -672,24 +678,25 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); }); - expect(setState).toHaveBeenCalledWith( - { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - label: 'Custom label', - customLabel: true, - }), - }, + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](state)).toEqual({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + label: 'Custom label', + customLabel: true, + }), }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should remove customLabel flag if label is set to default', () => { @@ -740,23 +747,24 @@ describe('IndexPatternDimensionEditorPanel', () => { .simulate('click'); }); - expect(setState).toHaveBeenCalledWith( - { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - }, - incompleteColumns: { - col1: { operationType: 'terms' }, - }, + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](state)).toEqual({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + }, + incompleteColumns: { + col1: { operationType: 'terms' }, }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should show error message in invalid state', () => { @@ -865,20 +873,22 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper = mount(); wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); - expect(setState).toHaveBeenCalledWith( - { - ...state, - layers: { - first: { - ...state.layers.first, - incompleteColumns: { - col2: { operationType: 'average' }, - }, + + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: false }, + ]); + expect(setState.mock.calls[0][0](state)).toEqual({ + ...state, + layers: { + first: { + ...state.layers.first, + incompleteColumns: { + col2: { operationType: 'average' }, }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: false } - ); + }); const comboBox = wrapper .find(EuiComboBox) @@ -890,26 +900,23 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([options![1].options![2]]); }); - expect(setState).toHaveBeenLastCalledWith( - { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'source', - operationType: 'terms', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col2', 'col1'], + expect(setState.mock.calls[1][0](state)).toEqual({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'source', + operationType: 'terms', + // Other parts of this don't matter for this test + }), }, + columnOrder: ['col2', 'col1'], }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should clean up when transitioning from incomplete reference-based operations to field operation', () => { @@ -936,20 +943,21 @@ describe('IndexPatternDimensionEditorPanel', () => { .simulate('click'); // Now check that the dimension gets cleaned up on state update - expect(setState).toHaveBeenCalledWith( - { - ...state, - layers: { - first: { - ...state.layers.first, - incompleteColumns: { - col2: { operationType: 'average' }, - }, + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: false }, + ]); + expect(setState.mock.calls[0][0](state)).toEqual({ + ...state, + layers: { + first: { + ...state.layers.first, + incompleteColumns: { + col2: { operationType: 'average' }, }, }, }, - { shouldRemoveDimension: true, shouldReplaceDimension: false } - ); + }); }); it('should select the Records field when count is selected', () => { @@ -973,7 +981,7 @@ describe('IndexPatternDimensionEditorPanel', () => { .find('button[data-test-subj="lns-indexPatternDimension-count incompatible"]') .simulate('click'); - const newColumnState = setState.mock.calls[0][0].layers.first.columns.col2; + const newColumnState = setState.mock.calls[0][0](state).layers.first.columns.col2; expect(newColumnState.operationType).toEqual('count'); expect(newColumnState.sourceField).toEqual('Records'); }); @@ -1030,24 +1038,26 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([option]); }); - expect(setState).toHaveBeenLastCalledWith( - { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - sourceField: 'source', - operationType: 'terms', - }), - }, + expect(setState.mock.calls.length).toEqual(2); + expect(setState.mock.calls[1]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[1][0](state)).toEqual({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + sourceField: 'source', + operationType: 'terms', + }), }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); }); @@ -1130,24 +1140,25 @@ describe('IndexPatternDimensionEditorPanel', () => { .find('[data-test-subj="indexPattern-time-scaling-enable"]') .hostNodes() .simulate('click'); - expect(props.setState).toHaveBeenCalledWith( - { - ...props.state, - layers: { - first: { - ...props.state.layers.first, - columns: { - ...props.state.layers.first.columns, - col2: expect.objectContaining({ - timeScale: 's', - label: 'Count of records per second', - }), - }, + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](props.state)).toEqual({ + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + timeScale: 's', + label: 'Count of records per second', + }), }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should carry over time scaling to other operation if possible', () => { @@ -1161,24 +1172,25 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper .find('button[data-test-subj="lns-indexPatternDimension-count incompatible"]') .simulate('click'); - expect(props.setState).toHaveBeenCalledWith( - { - ...props.state, - layers: { - first: { - ...props.state.layers.first, - columns: { - ...props.state.layers.first.columns, - col2: expect.objectContaining({ - timeScale: 'h', - label: 'Count of records per hour', - }), - }, + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](props.state)).toEqual({ + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + timeScale: 'h', + label: 'Count of records per hour', + }), }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should not carry over time scaling if the other operation does not support it', () => { @@ -1190,24 +1202,25 @@ describe('IndexPatternDimensionEditorPanel', () => { }); wrapper = mount(); wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); - expect(props.setState).toHaveBeenCalledWith( - { - ...props.state, - layers: { - first: { - ...props.state.layers.first, - columns: { - ...props.state.layers.first.columns, - col2: expect.objectContaining({ - timeScale: undefined, - label: 'Average of bytes', - }), - }, + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](props.state)).toEqual({ + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + timeScale: undefined, + label: 'Average of bytes', + }), }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should allow to change time scaling', () => { @@ -1223,24 +1236,25 @@ describe('IndexPatternDimensionEditorPanel', () => { .prop('onChange')!(({ target: { value: 'h' }, } as unknown) as ChangeEvent); - expect(props.setState).toHaveBeenCalledWith( - { - ...props.state, - layers: { - first: { - ...props.state.layers.first, - columns: { - ...props.state.layers.first.columns, - col2: expect.objectContaining({ - timeScale: 'h', - label: 'Count of records per hour', - }), - }, + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](props.state)).toEqual({ + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + timeScale: 'h', + label: 'Count of records per hour', + }), }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should not adjust label if it is custom', () => { @@ -1252,24 +1266,25 @@ describe('IndexPatternDimensionEditorPanel', () => { .prop('onChange')!(({ target: { value: 'h' }, } as unknown) as ChangeEvent); - expect(props.setState).toHaveBeenCalledWith( - { - ...props.state, - layers: { - first: { - ...props.state.layers.first, - columns: { - ...props.state.layers.first.columns, - col2: expect.objectContaining({ - timeScale: 'h', - label: 'My label', - }), - }, + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](props.state)).toEqual({ + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + timeScale: 'h', + label: 'My label', + }), }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should allow to remove time scaling', () => { @@ -1282,24 +1297,25 @@ describe('IndexPatternDimensionEditorPanel', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any {} as any ); - expect(props.setState).toHaveBeenCalledWith( - { - ...props.state, - layers: { - first: { - ...props.state.layers.first, - columns: { - ...props.state.layers.first.columns, - col2: expect.objectContaining({ - timeScale: undefined, - label: 'Count of records', - }), - }, + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](props.state)).toEqual({ + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + timeScale: undefined, + label: 'Count of records', + }), }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); }); @@ -1384,26 +1400,27 @@ describe('IndexPatternDimensionEditorPanel', () => { .find('[data-test-subj="indexPattern-filter-by-enable"]') .hostNodes() .simulate('click'); - expect(props.setState).toHaveBeenCalledWith( - { - ...props.state, - layers: { - first: { - ...props.state.layers.first, - columns: { - ...props.state.layers.first.columns, - col2: expect.objectContaining({ - filter: { - language: 'kuery', - query: '', - }, - }), - }, + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](props.state)).toEqual({ + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + filter: { + language: 'kuery', + query: '', + }, + }), }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should carry over filter to other operation if possible', () => { @@ -1417,23 +1434,24 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper .find('button[data-test-subj="lns-indexPatternDimension-count incompatible"]') .simulate('click'); - expect(props.setState).toHaveBeenCalledWith( - { - ...props.state, - layers: { - first: { - ...props.state.layers.first, - columns: { - ...props.state.layers.first.columns, - col2: expect.objectContaining({ - filter: { language: 'kuery', query: 'a: b' }, - }), - }, + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](props.state)).toEqual({ + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + filter: { language: 'kuery', query: 'a: b' }, + }), }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should allow to change filter', () => { @@ -1445,23 +1463,24 @@ describe('IndexPatternDimensionEditorPanel', () => { language: 'kuery', query: 'c: d', }); - expect(props.setState).toHaveBeenCalledWith( - { - ...props.state, - layers: { - first: { - ...props.state.layers.first, - columns: { - ...props.state.layers.first.columns, - col2: expect.objectContaining({ - filter: { language: 'kuery', query: 'c: d' }, - }), - }, + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](props.state)).toEqual({ + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + filter: { language: 'kuery', query: 'c: d' }, + }), }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should allow to remove filter', () => { @@ -1476,23 +1495,24 @@ describe('IndexPatternDimensionEditorPanel', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any {} as any ); - expect(props.setState).toHaveBeenCalledWith( - { - ...props.state, - layers: { - first: { - ...props.state.layers.first, - columns: { - ...props.state.layers.first.columns, - col2: expect.objectContaining({ - filter: undefined, - }), - }, + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](props.state)).toEqual({ + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + filter: undefined, + }), }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); }); @@ -1530,22 +1550,23 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); - expect(setState).toHaveBeenCalledWith( - { - ...state, - layers: { - first: { - ...state.layers.first, - incompleteColumns: { - col2: { - operationType: 'average', - }, + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: false }, + ]); + expect(setState.mock.calls[0][0](defaultProps.state)).toEqual({ + ...state, + layers: { + first: { + ...state.layers.first, + incompleteColumns: { + col2: { + operationType: 'average', }, }, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: false } - ); + }); const comboBox = wrapper .find(EuiComboBox) @@ -1556,26 +1577,23 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([options![1].options![0]]); }); - expect(setState).toHaveBeenCalledWith( - { - ...state, - layers: { - first: { - ...state.layers.first, - columnOrder: ['col1', 'col2'], - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - operationType: 'average', - sourceField: 'bytes', - }), - }, - incompleteColumns: {}, + expect(setState.mock.calls[1][0](defaultProps.state)).toEqual({ + ...state, + layers: { + first: { + ...state.layers.first, + columnOrder: ['col1', 'col2'], + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + operationType: 'average', + sourceField: 'bytes', + }), }, + incompleteColumns: {}, }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should select operation directly if only one field is possible', () => { @@ -1599,26 +1617,27 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); - expect(setState).toHaveBeenCalledWith( - { - ...initialState, - layers: { - first: { - ...initialState.layers.first, - columns: { - ...initialState.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'average', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](initialState)).toEqual({ + ...initialState, + layers: { + first: { + ...initialState.layers.first, + columns: { + ...initialState.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'bytes', + operationType: 'average', + // Other parts of this don't matter for this test + }), }, + columnOrder: ['col1', 'col2'], }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should select operation directly if only document is possible', () => { @@ -1626,25 +1645,26 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); - expect(setState).toHaveBeenCalledWith( - { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - operationType: 'count', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](state)).toEqual({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + operationType: 'count', + // Other parts of this don't matter for this test + }), }, + columnOrder: ['col1', 'col2'], }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should indicate compatible fields when selecting the operation first', () => { @@ -1762,26 +1782,27 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([option]); }); - expect(setState).toHaveBeenCalledWith( - { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - operationType: 'range', - sourceField: 'bytes', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { shouldRemoveDimension: false, shouldReplaceDimension: true }, + ]); + expect(setState.mock.calls[0][0](defaultProps.state)).toEqual({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + operationType: 'range', + sourceField: 'bytes', + // Other parts of this don't matter for this test + }), }, + columnOrder: ['col1', 'col2'], }, }, - { shouldRemoveDimension: false, shouldReplaceDimension: true } - ); + }); }); it('should use helper function when changing the function', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx index 71de1e10300f03..c473be05ba3154 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx @@ -43,7 +43,9 @@ export interface ReferenceEditorProps { selectionStyle: 'full' | 'field' | 'hidden'; validation: RequiredReference; columnId: string; - updateLayer: (newLayer: IndexPatternLayer) => void; + updateLayer: ( + setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer) + ) => void; currentIndexPattern: IndexPattern; existingFields: IndexPatternPrivateState['existingFields']; dateRange: DateRange; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 6772432664d8cd..cbc83db7e5f376 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -149,7 +149,9 @@ export { formulaOperation } from './formula/formula'; export interface ParamEditorProps { currentColumn: C; layer: IndexPatternLayer; - updateLayer: (newLayer: IndexPatternLayer) => void; + updateLayer: ( + setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer) + ) => void; columnId: string; indexPattern: IndexPattern; uiSettings: IUiSettingsClient; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.test.tsx new file mode 100644 index 00000000000000..f4ee33446d504d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.test.tsx @@ -0,0 +1,80 @@ +/* + * 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 React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { I18nProvider } from '@kbn/i18n/react'; +import { ExceptionItemsSummary } from './exception_items_summary'; +import * as reactTestingLibrary from '@testing-library/react'; +import { getMockTheme } from '../../../../../../../../public/common/lib/kibana/kibana_react.mock'; +import { GetExceptionSummaryResponse } from '../../../../../../../../common/endpoint/types'; + +const mockTheme = getMockTheme({ + eui: { + paddingSizes: { m: '2' }, + }, +}); + +const getStatValue = (el: reactTestingLibrary.RenderResult, stat: string) => { + return el.getByText(stat)!.nextSibling?.lastChild?.textContent; +}; + +describe('Fleet event filters card', () => { + const renderComponent: ( + stats: GetExceptionSummaryResponse + ) => reactTestingLibrary.RenderResult = (stats) => { + const Wrapper: React.FC = ({ children }) => ( + + {children} + + ); + const component = reactTestingLibrary.render(, { + wrapper: Wrapper, + }); + return component; + }; + it('should renders correctly', () => { + const summary: GetExceptionSummaryResponse = { + windows: 3, + linux: 2, + macos: 2, + total: 7, + }; + const component = renderComponent(summary); + + expect(component.getByText('Windows')).not.toBeNull(); + expect(getStatValue(component, 'Windows')).toEqual(summary.windows.toString()); + + expect(component.getByText('Linux')).not.toBeNull(); + expect(getStatValue(component, 'Linux')).toEqual(summary.linux.toString()); + + expect(component.getByText('Mac')).not.toBeNull(); + expect(getStatValue(component, 'Mac')).toEqual(summary.macos.toString()); + + expect(component.getByText('Total')).not.toBeNull(); + expect(getStatValue(component, 'Total')).toEqual(summary.total.toString()); + }); + it('should renders correctly when missing some stats', () => { + const summary: Partial = { + windows: 3, + total: 3, + }; + const component = renderComponent(summary as GetExceptionSummaryResponse); + + expect(component.getByText('Windows')).not.toBeNull(); + expect(getStatValue(component, 'Windows')).toEqual('3'); + + expect(component.getByText('Linux')).not.toBeNull(); + expect(getStatValue(component, 'Linux')).toEqual('0'); + + expect(component.getByText('Mac')).not.toBeNull(); + expect(getStatValue(component, 'Mac')).toEqual('0'); + + expect(component.getByText('Total')).not.toBeNull(); + expect(getStatValue(component, 'Total')).toEqual('3'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx index f42304ffb89aed..ed3d9967f318e2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx @@ -47,7 +47,7 @@ export const ExceptionItemsSummary = memo(({ stats } {SUMMARY_KEYS.map((stat) => { return ( - + { + const originalModule = jest.requireActual( + '../../../../../../../../../../../src/plugins/kibana_react/public' + ); + const useKibana = jest.fn().mockImplementation(() => ({ + services: { + http: {}, + data: {}, + notifications: {}, + application: { + getUrlForApp: jest.fn(), + }, + }, + })); + + return { + ...originalModule, + useKibana, + }; +}); + +jest.mock('../../../../../../../common/lib/kibana'); + +const mockTheme = getMockTheme({ + eui: { + paddingSizes: { m: '2' }, + }, +}); + +const EventFiltersHttpServiceMock = EventFiltersHttpService as jest.Mock; +const useToastsMock = useToasts as jest.Mock; + +const summary: GetExceptionSummaryResponse = { + windows: 3, + linux: 2, + macos: 2, + total: 7, +}; + +describe('Fleet event filters card', () => { + let promise: Promise; + let addDanger: jest.Mock = jest.fn(); + const renderComponent: () => Promise = async () => { + const Wrapper: React.FC = ({ children }) => ( + + {children} + + ); + // @ts-ignore + const component = reactTestingLibrary.render(, { wrapper: Wrapper }); + try { + // @ts-ignore + await reactTestingLibrary.act(() => promise); + } catch (err) { + return component; + } + return component; + }; + beforeAll(() => { + useToastsMock.mockImplementation(() => { + return { + addDanger, + }; + }); + }); + beforeEach(() => { + promise = Promise.resolve(summary); + addDanger = jest.fn(); + }); + afterEach(() => { + EventFiltersHttpServiceMock.mockReset(); + }); + it('should render correctly', async () => { + EventFiltersHttpServiceMock.mockImplementationOnce(() => { + return { + getSummary: () => jest.fn(() => promise), + }; + }); + const component = await renderComponent(); + expect(component.getByText('Event Filters')).not.toBeNull(); + expect(component.getByText('Manage event filters')).not.toBeNull(); + }); + it('should render an error toast when api call fails', async () => { + expect(addDanger).toBeCalledTimes(0); + promise = Promise.reject(new Error('error test')); + EventFiltersHttpServiceMock.mockImplementationOnce(() => { + return { + getSummary: () => promise, + }; + }); + const component = await renderComponent(); + expect(component.getByText('Event Filters')).not.toBeNull(); + expect(component.getByText('Manage event filters')).not.toBeNull(); + await reactTestingLibrary.waitFor(() => expect(addDanger).toBeCalledTimes(1)); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx index 6f368a89eb5f93..40d10004788e42 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useMemo, useState, useEffect } from 'react'; +import React, { memo, useMemo, useState, useEffect, useRef } from 'react'; import { ApplicationStart, CoreStart } from 'kibana/public'; import { EuiPanel, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -36,12 +36,16 @@ export const FleetEventFiltersCard = memo( const [stats, setStats] = useState(); const eventFiltersListUrlPath = getEventFiltersListPath(); const eventFiltersApi = useMemo(() => new EventFiltersHttpService(http), [http]); + const isMounted = useRef(); useEffect(() => { + isMounted.current = true; const fetchStats = async () => { try { const summary = await eventFiltersApi.getSummary(); - setStats(summary); + if (isMounted.current) { + setStats(summary); + } } catch (error) { toasts.addDanger( i18n.translate( @@ -55,6 +59,9 @@ export const FleetEventFiltersCard = memo( } }; fetchStats(); + return () => { + isMounted.current = false; + }; }, [eventFiltersApi, toasts]); const eventFiltersRouteState = useMemo(() => { @@ -79,7 +86,7 @@ export const FleetEventFiltersCard = memo( return ( - +

(

- + - + <> { + const originalModule = jest.requireActual( + '../../../../../../../../../../../src/plugins/kibana_react/public' + ); + const useKibana = jest.fn().mockImplementation(() => ({ + services: { + http: {}, + data: {}, + notifications: {}, + application: { + getUrlForApp: jest.fn(), + }, + }, + })); + + return { + ...originalModule, + useKibana, + }; +}); + +jest.mock('../../../../../../../common/lib/kibana'); + +const mockTheme = getMockTheme({ + eui: { + paddingSizes: { m: '2' }, + }, +}); + +const TrustedAppsHttpServiceMock = TrustedAppsHttpService as jest.Mock; +const useToastsMock = useToasts as jest.Mock; + +const summary: GetExceptionSummaryResponse = { + windows: 3, + linux: 2, + macos: 2, + total: 7, +}; + +describe('Fleet trusted apps card', () => { + let promise: Promise; + let addDanger: jest.Mock = jest.fn(); + const renderComponent: () => Promise = async () => { + const Wrapper: React.FC = ({ children }) => ( + + {children} + + ); + // @ts-ignore + const component = reactTestingLibrary.render(, { wrapper: Wrapper }); + try { + // @ts-ignore + await reactTestingLibrary.act(() => promise); + } catch (err) { + return component; + } + return component; + }; + + beforeAll(() => { + useToastsMock.mockImplementation(() => { + return { + addDanger, + }; + }); + }); + beforeEach(() => { + promise = Promise.resolve(summary); + addDanger = jest.fn(); + }); + afterEach(() => { + TrustedAppsHttpServiceMock.mockReset(); + }); + it('should render correctly', async () => { + TrustedAppsHttpServiceMock.mockImplementationOnce(() => { + return { + getTrustedAppsSummary: () => jest.fn(() => promise), + }; + }); + const component = await renderComponent(); + expect(component.getByText('Trusted Applications')).not.toBeNull(); + expect(component.getByText('Manage trusted applications')).not.toBeNull(); + }); + it('should render an error toast when api call fails', async () => { + expect(addDanger).toBeCalledTimes(0); + promise = Promise.reject(new Error('error test')); + TrustedAppsHttpServiceMock.mockImplementationOnce(() => { + return { + getTrustedAppsSummary: () => promise, + }; + }); + const component = await renderComponent(); + expect(component.getByText('Trusted Applications')).not.toBeNull(); + expect(component.getByText('Manage trusted applications')).not.toBeNull(); + await reactTestingLibrary.waitFor(() => expect(addDanger).toBeCalledTimes(1)); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx index ec1479643999a9..b1464d23e00fbd 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useMemo, useState, useEffect } from 'react'; +import React, { memo, useMemo, useState, useEffect, useRef } from 'react'; import { ApplicationStart, CoreStart } from 'kibana/public'; import { EuiPanel, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -38,12 +38,16 @@ export const FleetTrustedAppsCard = memo(( const toasts = useToasts(); const [stats, setStats] = useState(); const trustedAppsApi = useMemo(() => new TrustedAppsHttpService(http), [http]); + const isMounted = useRef(); useEffect(() => { + isMounted.current = true; const fetchStats = async () => { try { const response = await trustedAppsApi.getTrustedAppsSummary(); - setStats(response); + if (isMounted) { + setStats(response); + } } catch (error) { toasts.addDanger( i18n.translate( @@ -57,6 +61,9 @@ export const FleetTrustedAppsCard = memo(( } }; fetchStats(); + return () => { + isMounted.current = false; + }; }, [toasts, trustedAppsApi]); const trustedAppsListUrlPath = getTrustedAppsListPath(); @@ -82,7 +89,7 @@ export const FleetTrustedAppsCard = memo(( return ( - +

((

- + - + <> ` - grid-area: ${({ gridArea }) => gridArea}; - align-items: ${({ alignItems }) => alignItems ?? 'center'}; + grid-area: ${({ gridarea }) => gridarea}; + align-items: ${({ alignitems }) => alignitems ?? 'center'}; margin: 0px; padding: 12px; `;