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;
`;