diff --git a/docs/canvas/canvas-function-reference.asciidoc b/docs/canvas/canvas-function-reference.asciidoc
index 6a6c840074f02..eaaf68eb06195 100644
--- a/docs/canvas/canvas-function-reference.asciidoc
+++ b/docs/canvas/canvas-function-reference.asciidoc
@@ -1697,6 +1697,16 @@ Aliases: `column`, `name`
Aliases: `exp`, `fn`, `function`
|`boolean`, `number`, `string`, `null`
|A Canvas expression that is passed to each row as a single row `datatable`.
+
+|`id`
+
+|`string`, `null`
+|An optional id of the resulting column. When not specified or `null` the name argument is used as id.
+
+|`copyMetaFrom`
+
+|`string`, `null`
+|If set, the meta object from the specified column id is copied over to the specified target column. Throws an exception if the column doesn't exist
|===
*Returns:* `datatable`
@@ -1755,9 +1765,16 @@ Interprets a `TinyMath` math expression using a `number` or `datatable` as _cont
Alias: `expression`
|`string`
|An evaluated `TinyMath` expression. See https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html.
+
+|`onError`
+
+|`string`
+|In case the `TinyMath` evaluation fails or returns NaN, the return value is specified by onError. For example, `"null"`, `"zero"`, `"false"`, `"throw"`. When `"throw"`, it will throw an exception, terminating expression execution.
+
+Default: `"throw"`
|===
-*Returns:* `number`
+*Returns:* `number` | `boolean` | `null`
[float]
diff --git a/packages/kbn-docs-utils/package.json b/packages/kbn-docs-utils/package.json
index 6571f8c87dbfa..089732e9e6b40 100644
--- a/packages/kbn-docs-utils/package.json
+++ b/packages/kbn-docs-utils/package.json
@@ -13,6 +13,7 @@
},
"dependencies": {
"@kbn/utils": "link:../kbn-utils",
+ "@kbn/config": "link:../kbn-config",
"@kbn/dev-utils": "link:../kbn-dev-utils"
}
}
\ No newline at end of file
diff --git a/src/core/server/elasticsearch/default_headers.test.ts b/src/core/server/elasticsearch/default_headers.test.ts
new file mode 100644
index 0000000000000..58e6e222a3f2b
--- /dev/null
+++ b/src/core/server/elasticsearch/default_headers.test.ts
@@ -0,0 +1,23 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { getReservedHeaders, PRODUCT_ORIGIN_HEADER } from './default_headers';
+
+describe('getReservedHeaders', () => {
+ it('returns the list of reserved headers contained in a list', () => {
+ expect(getReservedHeaders(['foo', 'bar', PRODUCT_ORIGIN_HEADER])).toEqual([
+ PRODUCT_ORIGIN_HEADER,
+ ]);
+ });
+
+ it('ignores the case when identifying headers', () => {
+ expect(getReservedHeaders(['foo', 'bar', PRODUCT_ORIGIN_HEADER.toUpperCase()])).toEqual([
+ PRODUCT_ORIGIN_HEADER.toUpperCase(),
+ ]);
+ });
+});
diff --git a/src/core/server/elasticsearch/default_headers.ts b/src/core/server/elasticsearch/default_headers.ts
index 737d4772c5c0e..eef04754cd958 100644
--- a/src/core/server/elasticsearch/default_headers.ts
+++ b/src/core/server/elasticsearch/default_headers.ts
@@ -8,9 +8,22 @@
import { deepFreeze } from '@kbn/std';
+export const PRODUCT_ORIGIN_HEADER = 'x-elastic-product-origin';
+
+export const RESERVED_HEADERS = deepFreeze([PRODUCT_ORIGIN_HEADER]);
+
export const DEFAULT_HEADERS = deepFreeze({
// Elasticsearch uses this to identify when a request is coming from Kibana, to allow Kibana to
- // access system indices using the standard ES APIs without logging a warning. After migrating to
- // use the new system index APIs, this header can be removed.
- 'x-elastic-product-origin': 'kibana',
+ // access system indices using the standard ES APIs.
+ [PRODUCT_ORIGIN_HEADER]: 'kibana',
});
+
+export const getReservedHeaders = (headerNames: string[]): string[] => {
+ const reservedHeaders = [];
+ for (const headerName of headerNames) {
+ if (RESERVED_HEADERS.includes(headerName.toLowerCase())) {
+ reservedHeaders.push(headerName);
+ }
+ }
+ return reservedHeaders;
+};
diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts
index ae71932bdd3a9..d3f9693bab229 100644
--- a/src/core/server/elasticsearch/elasticsearch_config.test.ts
+++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts
@@ -108,6 +108,35 @@ test('#requestHeadersWhitelist accepts both string and array of strings', () =>
expect(configValue.requestHeadersWhitelist).toEqual(['token', 'X-Forwarded-Proto']);
});
+describe('reserved headers', () => {
+ test('throws if customHeaders contains reserved headers', () => {
+ expect(() => {
+ config.schema.validate({
+ customHeaders: { foo: 'bar', 'x-elastic-product-origin': 'beats' },
+ });
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"[customHeaders]: cannot use reserved headers: [x-elastic-product-origin]"`
+ );
+ });
+
+ test('throws if requestHeadersWhitelist contains reserved headers', () => {
+ expect(() => {
+ config.schema.validate({ requestHeadersWhitelist: ['foo', 'x-elastic-product-origin'] });
+ }).toThrowErrorMatchingInlineSnapshot(`
+ "[requestHeadersWhitelist]: types that failed validation:
+ - [requestHeadersWhitelist.0]: expected value of type [string] but got [Array]
+ - [requestHeadersWhitelist.1]: cannot use reserved headers: [x-elastic-product-origin]"
+ `);
+ expect(() => {
+ config.schema.validate({ requestHeadersWhitelist: 'x-elastic-product-origin' });
+ }).toThrowErrorMatchingInlineSnapshot(`
+ "[requestHeadersWhitelist]: types that failed validation:
+ - [requestHeadersWhitelist.0]: cannot use reserved headers: [x-elastic-product-origin]
+ - [requestHeadersWhitelist.1]: could not parse array value from json input"
+ `);
+ });
+});
+
describe('reads files', () => {
beforeEach(() => {
mockReadFileSync.mockReset();
diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts
index 300ff4a61a288..879002a6ece51 100644
--- a/src/core/server/elasticsearch/elasticsearch_config.ts
+++ b/src/core/server/elasticsearch/elasticsearch_config.ts
@@ -12,6 +12,7 @@ import { readFileSync } from 'fs';
import { ConfigDeprecationProvider } from 'src/core/server';
import { readPkcs12Keystore, readPkcs12Truststore } from '../utils';
import { ServiceConfigDescriptor } from '../internal_types';
+import { getReservedHeaders } from './default_headers';
const hostURISchema = schema.uri({ scheme: ['http', 'https'] });
@@ -52,10 +53,42 @@ export const configSchema = schema.object({
)
),
password: schema.maybe(schema.string()),
- requestHeadersWhitelist: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], {
- defaultValue: ['authorization'],
+ requestHeadersWhitelist: schema.oneOf(
+ [
+ schema.string({
+ // can't use `validate` option on union types, forced to validate each individual subtypes
+ // see https://github.com/elastic/kibana/issues/64906
+ validate: (headersWhitelist) => {
+ const reservedHeaders = getReservedHeaders([headersWhitelist]);
+ if (reservedHeaders.length) {
+ return `cannot use reserved headers: [${reservedHeaders.join(', ')}]`;
+ }
+ },
+ }),
+ schema.arrayOf(schema.string(), {
+ // can't use `validate` option on union types, forced to validate each individual subtypes
+ // see https://github.com/elastic/kibana/issues/64906
+ validate: (headersWhitelist) => {
+ const reservedHeaders = getReservedHeaders(headersWhitelist);
+ if (reservedHeaders.length) {
+ return `cannot use reserved headers: [${reservedHeaders.join(', ')}]`;
+ }
+ },
+ }),
+ ],
+ {
+ defaultValue: ['authorization'],
+ }
+ ),
+ customHeaders: schema.recordOf(schema.string(), schema.string(), {
+ defaultValue: {},
+ validate: (customHeaders) => {
+ const reservedHeaders = getReservedHeaders(Object.keys(customHeaders));
+ if (reservedHeaders.length) {
+ return `cannot use reserved headers: [${reservedHeaders.join(', ')}]`;
+ }
+ },
}),
- customHeaders: schema.recordOf(schema.string(), schema.string(), { defaultValue: {} }),
shardTimeout: schema.duration({ defaultValue: '30s' }),
requestTimeout: schema.duration({ defaultValue: '30s' }),
pingTimeout: schema.duration({ defaultValue: schema.siblingRef('requestTimeout') }),
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts
index fc26c837d5e52..267d671361184 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts
@@ -96,8 +96,7 @@ describe('getSearchDsl', () => {
mappings,
opts.type,
opts.sortField,
- opts.sortOrder,
- opts.pit
+ opts.sortOrder
);
});
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts
index cae5e43897bcf..9820544f02bd1 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts
@@ -78,7 +78,7 @@ export function getSearchDsl(
hasReferenceOperator,
kueryNode,
}),
- ...getSortingParams(mappings, type, sortField, sortOrder, pit),
+ ...getSortingParams(mappings, type, sortField, sortOrder),
...(pit ? getPitParams(pit) : {}),
...(searchAfter ? { search_after: searchAfter } : {}),
};
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts
index 73c7065705fc5..1376f0d50a9da 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts
@@ -79,11 +79,6 @@ describe('searchDsl/getSortParams', () => {
],
});
});
- it('appends tiebreaker when PIT is provided', () => {
- expect(getSortingParams(MAPPINGS, 'saved', 'title', undefined, { id: 'abc' }).sort).toEqual(
- expect.arrayContaining([{ _shard_doc: 'asc' }])
- );
- });
});
describe('sortField is simple root property with multiple types', () => {
it('returns correct params', () => {
@@ -98,11 +93,6 @@ describe('searchDsl/getSortParams', () => {
],
});
});
- it('appends tiebreaker when PIT is provided', () => {
- expect(
- getSortingParams(MAPPINGS, ['saved', 'pending'], 'type', undefined, { id: 'abc' }).sort
- ).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }]));
- });
});
describe('sortField is simple non-root property with multiple types', () => {
it('returns correct params', () => {
@@ -124,11 +114,6 @@ describe('searchDsl/getSortParams', () => {
],
});
});
- it('appends tiebreaker when PIT is provided', () => {
- expect(
- getSortingParams(MAPPINGS, 'saved', 'title.raw', undefined, { id: 'abc' }).sort
- ).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }]));
- });
});
describe('sortField is multi-field with single type as array', () => {
it('returns correct params', () => {
@@ -143,11 +128,6 @@ describe('searchDsl/getSortParams', () => {
],
});
});
- it('appends tiebreaker when PIT is provided', () => {
- expect(
- getSortingParams(MAPPINGS, ['saved'], 'title.raw', undefined, { id: 'abc' }).sort
- ).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }]));
- });
});
describe('sortField is root multi-field with multiple types', () => {
it('returns correct params', () => {
@@ -162,12 +142,6 @@ describe('searchDsl/getSortParams', () => {
],
});
});
- it('appends tiebreaker when PIT is provided', () => {
- expect(
- getSortingParams(MAPPINGS, ['saved', 'pending'], 'type.raw', undefined, { id: 'abc' })
- .sort
- ).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }]));
- });
});
describe('sortField is not-root multi-field with multiple types', () => {
it('returns correct params', () => {
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts
index abef9bfa0a300..e3bfba6a80f59 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts
@@ -8,12 +8,6 @@
import Boom from '@hapi/boom';
import { getProperty, IndexMapping } from '../../../mappings';
-import { SavedObjectsPitParams } from '../../../types';
-
-// TODO: The plan is for ES to automatically add this tiebreaker when
-// using PIT. We should remove this logic once that is resolved.
-// https://github.com/elastic/elasticsearch/issues/56828
-const ES_PROVIDED_TIEBREAKER = { _shard_doc: 'asc' };
const TOP_LEVEL_FIELDS = ['_id', '_score'];
@@ -21,8 +15,7 @@ export function getSortingParams(
mappings: IndexMapping,
type: string | string[],
sortField?: string,
- sortOrder?: string,
- pit?: SavedObjectsPitParams
+ sortOrder?: string
) {
if (!sortField) {
return {};
@@ -38,7 +31,6 @@ export function getSortingParams(
order: sortOrder,
},
},
- ...(pit ? [ES_PROVIDED_TIEBREAKER] : []),
],
};
}
@@ -59,7 +51,6 @@ export function getSortingParams(
unmapped_type: rootField.type,
},
},
- ...(pit ? [ES_PROVIDED_TIEBREAKER] : []),
],
};
}
@@ -84,7 +75,6 @@ export function getSortingParams(
unmapped_type: field.type,
},
},
- ...(pit ? [ES_PROVIDED_TIEBREAKER] : []),
],
};
}
diff --git a/src/dev/typescript/ref_output_cache/ref_output_cache.ts b/src/dev/typescript/ref_output_cache/ref_output_cache.ts
index 6f51243e47555..ca69236a706d2 100644
--- a/src/dev/typescript/ref_output_cache/ref_output_cache.ts
+++ b/src/dev/typescript/ref_output_cache/ref_output_cache.ts
@@ -76,28 +76,28 @@ export class RefOutputCache {
* written to the directory.
*/
async initCaches() {
- const archive =
- this.archives.get(this.mergeBase) ??
- (await this.archives.getFirstAvailable([
- this.mergeBase,
- ...(await this.repo.getRecentShasFrom(this.mergeBase, 5)),
- ]));
-
- if (!archive) {
- return;
- }
-
const outdatedOutDirs = (
await concurrentMap(100, this.outDirs, async (outDir) => ({
path: outDir,
- outdated: !(await matchMergeBase(outDir, archive.sha)),
+ outdated: !(await matchMergeBase(outDir, this.mergeBase)),
}))
)
.filter((o) => o.outdated)
.map((o) => o.path);
if (!outdatedOutDirs.length) {
- this.log.debug('all outDirs have the most recent cache');
+ this.log.debug('all outDirs have a recent cache');
+ return;
+ }
+
+ const archive =
+ this.archives.get(this.mergeBase) ??
+ (await this.archives.getFirstAvailable([
+ this.mergeBase,
+ ...(await this.repo.getRecentShasFrom(this.mergeBase, 5)),
+ ]));
+
+ if (!archive) {
return;
}
diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts b/src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts
index 5517011f05718..8c7e48f173031 100644
--- a/src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts
+++ b/src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts
@@ -59,6 +59,7 @@ export function isFieldFiltered(
const scriptedOrMissing =
!filterState.missing ||
field.type === '_source' ||
+ field.type === 'unknown_selected' ||
field.scripted ||
fieldCounts[field.name] > 0;
const needle = filterState.name ? filterState.name.toLowerCase() : '';
diff --git a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts
index 9792e98ba84c7..68099fb0c8e2a 100644
--- a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts
+++ b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts
@@ -217,6 +217,20 @@ describe('group_fields', function () {
]);
});
+ it('should filter fields by a given name', function () {
+ const fieldFilterState = { ...getDefaultFieldFilter(), ...{ name: 'curr' } };
+
+ const actual1 = groupFields(
+ fields as IndexPatternField[],
+ ['customer_birth_date', 'currency', 'unknown'],
+ 5,
+ fieldCounts,
+ fieldFilterState,
+ false
+ );
+ expect(actual1.selected.map((field) => field.name)).toEqual(['currency']);
+ });
+
it('excludes unmapped fields if showUnmappedFields set to false', function () {
const fieldFilterState = getDefaultFieldFilter();
const fieldsWithUnmappedField = [...fields];
diff --git a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx
index eefb96b78aac6..dc6cbcedc8086 100644
--- a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx
+++ b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx
@@ -71,10 +71,18 @@ export function groupFields(
}
}
}
- // add columns, that are not part of the index pattern, to be removeable
+ // add selected columns, that are not part of the index pattern, to be removeable
for (const column of columns) {
- if (!result.selected.find((field) => field.name === column)) {
- result.selected.push({ name: column, displayName: column } as IndexPatternField);
+ const tmpField = {
+ name: column,
+ displayName: column,
+ type: 'unknown_selected',
+ } as IndexPatternField;
+ if (
+ !result.selected.find((field) => field.name === column) &&
+ isFieldFiltered(tmpField, fieldFilterState, fieldCounts)
+ ) {
+ result.selected.push(tmpField);
}
}
result.selected.sort((a, b) => {
diff --git a/src/plugins/embeddable/README.asciidoc b/src/plugins/embeddable/README.asciidoc
index daa6040eab7eb..165dc37c56cb3 100644
--- a/src/plugins/embeddable/README.asciidoc
+++ b/src/plugins/embeddable/README.asciidoc
@@ -22,18 +22,17 @@ There is also an example of rendering dashboard container outside of dashboard a
=== Docs
-link:./docs/README.md[Embeddable docs, guides & caveats]
+link:https://github.com/elastic/kibana/blob/master/src/plugins/embeddable/docs/README.md[Embeddable docs, guides & caveats]
=== API docs
-==== Server API
-https://github.com/elastic/kibana/blob/master/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md[Server Setup contract]
-https://github.com/elastic/kibana/blob/master/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablestart.md[Server Start contract]
-
-===== Browser API
+==== Browser API
https://github.com/elastic/kibana/blob/master/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablesetup.md[Browser Setup contract]
https://github.com/elastic/kibana/blob/master/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestart.md[Browser Start contract]
+==== Server API
+https://github.com/elastic/kibana/blob/master/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md[Server Setup contract]
+
=== Testing
Run unit tests
diff --git a/src/plugins/expressions/README.asciidoc b/src/plugins/expressions/README.asciidoc
index e07f6e2909ab8..554e3bfcb6976 100644
--- a/src/plugins/expressions/README.asciidoc
+++ b/src/plugins/expressions/README.asciidoc
@@ -46,7 +46,7 @@ image::https://user-images.githubusercontent.com/9773803/74162514-3250a880-4c21-
https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionsserversetup.md[Server Setup contract]
https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionsserverstart.md[Server Start contract]
-===== Browser API
+==== Browser API
https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicesetup.md[Browser Setup contract]
https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsstart.md[Browser Start contract]
diff --git a/src/plugins/expressions/common/expression_functions/specs/index.ts b/src/plugins/expressions/common/expression_functions/specs/index.ts
index 938fce026027c..9408b3a433712 100644
--- a/src/plugins/expressions/common/expression_functions/specs/index.ts
+++ b/src/plugins/expressions/common/expression_functions/specs/index.ts
@@ -15,6 +15,8 @@ import { theme } from './theme';
import { cumulativeSum } from './cumulative_sum';
import { derivative } from './derivative';
import { movingAverage } from './moving_average';
+import { mapColumn } from './map_column';
+import { math } from './math';
export const functionSpecs: AnyExpressionFunctionDefinition[] = [
clog,
@@ -25,6 +27,8 @@ export const functionSpecs: AnyExpressionFunctionDefinition[] = [
cumulativeSum,
derivative,
movingAverage,
+ mapColumn,
+ math,
];
export * from './clog';
@@ -35,3 +39,5 @@ export * from './theme';
export * from './cumulative_sum';
export * from './derivative';
export * from './moving_average';
+export { mapColumn, MapColumnArguments } from './map_column';
+export { math, MathArguments, MathInput } from './math';
diff --git a/src/plugins/expressions/common/expression_functions/specs/map_column.ts b/src/plugins/expressions/common/expression_functions/specs/map_column.ts
new file mode 100644
index 0000000000000..e2605e5ddf38d
--- /dev/null
+++ b/src/plugins/expressions/common/expression_functions/specs/map_column.ts
@@ -0,0 +1,123 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { ExpressionFunctionDefinition } from '../types';
+import { Datatable, getType } from '../../expression_types';
+
+export interface MapColumnArguments {
+ id?: string | null;
+ name: string;
+ expression?: (datatable: Datatable) => Promise;
+ copyMetaFrom?: string | null;
+}
+
+export const mapColumn: ExpressionFunctionDefinition<
+ 'mapColumn',
+ Datatable,
+ MapColumnArguments,
+ Promise
+> = {
+ name: 'mapColumn',
+ aliases: ['mc'], // midnight commander. So many times I've launched midnight commander instead of moving a file.
+ type: 'datatable',
+ inputTypes: ['datatable'],
+ help: i18n.translate('expressions.functions.mapColumnHelpText', {
+ defaultMessage:
+ 'Adds a column calculated as the result of other columns. ' +
+ 'Changes are made only when you provide arguments.' +
+ 'See also {alterColumnFn} and {staticColumnFn}.',
+ values: {
+ alterColumnFn: '`alterColumn`',
+ staticColumnFn: '`staticColumn`',
+ },
+ }),
+ args: {
+ id: {
+ types: ['string', 'null'],
+ help: i18n.translate('expressions.functions.mapColumn.args.idHelpText', {
+ defaultMessage:
+ 'An optional id of the resulting column. When `null` the name/column argument is used as id.',
+ }),
+ required: false,
+ default: null,
+ },
+ name: {
+ types: ['string'],
+ aliases: ['_', 'column'],
+ help: i18n.translate('expressions.functions.mapColumn.args.nameHelpText', {
+ defaultMessage: 'The name of the resulting column.',
+ }),
+ required: true,
+ },
+ expression: {
+ types: ['boolean', 'number', 'string', 'null'],
+ resolve: false,
+ aliases: ['exp', 'fn', 'function'],
+ help: i18n.translate('expressions.functions.mapColumn.args.expressionHelpText', {
+ defaultMessage:
+ 'An expression that is executed on every row, provided with a single-row {DATATABLE} context and returning the cell value.',
+ values: {
+ DATATABLE: '`datatable`',
+ },
+ }),
+ required: true,
+ },
+ copyMetaFrom: {
+ types: ['string', 'null'],
+ help: i18n.translate('expressions.functions.mapColumn.args.copyMetaFromHelpText', {
+ defaultMessage:
+ "If set, the meta object from the specified column id is copied over to the specified target column. If the column doesn't exist it silently fails.",
+ }),
+ required: false,
+ default: null,
+ },
+ },
+ fn: (input, args) => {
+ const expression = args.expression || (() => Promise.resolve(null));
+ const columnId = args.id != null ? args.id : args.name;
+
+ const columns = [...input.columns];
+ const rowPromises = input.rows.map((row) => {
+ return expression({
+ type: 'datatable',
+ columns,
+ rows: [row],
+ }).then((val) => ({
+ ...row,
+ [columnId]: val,
+ }));
+ });
+
+ return Promise.all(rowPromises).then((rows) => {
+ const existingColumnIndex = columns.findIndex(({ name }) => name === args.name);
+ const type = rows.length ? getType(rows[0][columnId]) : 'null';
+ const newColumn = {
+ id: columnId,
+ name: args.name,
+ meta: { type },
+ };
+ if (args.copyMetaFrom) {
+ const metaSourceFrom = columns.find(({ id }) => id === args.copyMetaFrom);
+ newColumn.meta = { ...newColumn.meta, ...(metaSourceFrom?.meta || {}) };
+ }
+
+ if (existingColumnIndex === -1) {
+ columns.push(newColumn);
+ } else {
+ columns[existingColumnIndex] = newColumn;
+ }
+
+ return {
+ type: 'datatable',
+ columns,
+ rows,
+ } as Datatable;
+ });
+ },
+};
diff --git a/src/plugins/expressions/common/expression_functions/specs/math.ts b/src/plugins/expressions/common/expression_functions/specs/math.ts
new file mode 100644
index 0000000000000..a70c032769b57
--- /dev/null
+++ b/src/plugins/expressions/common/expression_functions/specs/math.ts
@@ -0,0 +1,167 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { map, zipObject } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import { evaluate } from '@kbn/tinymath';
+import { ExpressionFunctionDefinition } from '../types';
+import { Datatable, isDatatable } from '../../expression_types';
+
+export type MathArguments = {
+ expression: string;
+ onError?: 'null' | 'zero' | 'false' | 'throw';
+};
+
+export type MathInput = number | Datatable;
+
+const TINYMATH = '`TinyMath`';
+const TINYMATH_URL =
+ 'https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html';
+
+const isString = (val: any): boolean => typeof val === 'string';
+
+function pivotObjectArray<
+ RowType extends { [key: string]: any },
+ ReturnColumns extends string | number | symbol = keyof RowType
+>(rows: RowType[], columns?: string[]): Record {
+ const columnNames = columns || Object.keys(rows[0]);
+ if (!columnNames.every(isString)) {
+ throw new Error('Columns should be an array of strings');
+ }
+
+ const columnValues = map(columnNames, (name) => map(rows, name));
+ return zipObject(columnNames, columnValues);
+}
+
+export const errors = {
+ emptyExpression: () =>
+ new Error(
+ i18n.translate('expressions.functions.math.emptyExpressionErrorMessage', {
+ defaultMessage: 'Empty expression',
+ })
+ ),
+ tooManyResults: () =>
+ new Error(
+ i18n.translate('expressions.functions.math.tooManyResultsErrorMessage', {
+ defaultMessage:
+ 'Expressions must return a single number. Try wrapping your expression in {mean} or {sum}',
+ values: {
+ mean: 'mean()',
+ sum: 'sum()',
+ },
+ })
+ ),
+ executionFailed: () =>
+ new Error(
+ i18n.translate('expressions.functions.math.executionFailedErrorMessage', {
+ defaultMessage: 'Failed to execute math expression. Check your column names',
+ })
+ ),
+ emptyDatatable: () =>
+ new Error(
+ i18n.translate('expressions.functions.math.emptyDatatableErrorMessage', {
+ defaultMessage: 'Empty datatable',
+ })
+ ),
+};
+
+const fallbackValue = {
+ null: null,
+ zero: 0,
+ false: false,
+} as const;
+
+export const math: ExpressionFunctionDefinition<
+ 'math',
+ MathInput,
+ MathArguments,
+ boolean | number | null
+> = {
+ name: 'math',
+ type: undefined,
+ inputTypes: ['number', 'datatable'],
+ help: i18n.translate('expressions.functions.mathHelpText', {
+ defaultMessage:
+ 'Interprets a {TINYMATH} math expression using a {TYPE_NUMBER} or {DATATABLE} as {CONTEXT}. ' +
+ 'The {DATATABLE} columns are available by their column name. ' +
+ 'If the {CONTEXT} is a number it is available as {value}.',
+ values: {
+ TINYMATH,
+ CONTEXT: '_context_',
+ DATATABLE: '`datatable`',
+ value: '`value`',
+ TYPE_NUMBER: '`number`',
+ },
+ }),
+ args: {
+ expression: {
+ aliases: ['_'],
+ types: ['string'],
+ help: i18n.translate('expressions.functions.math.args.expressionHelpText', {
+ defaultMessage: 'An evaluated {TINYMATH} expression. See {TINYMATH_URL}.',
+ values: {
+ TINYMATH,
+ TINYMATH_URL,
+ },
+ }),
+ },
+ onError: {
+ types: ['string'],
+ options: ['throw', 'false', 'zero', 'null'],
+ help: i18n.translate('expressions.functions.math.args.onErrorHelpText', {
+ defaultMessage:
+ "In case the {TINYMATH} evaluation fails or returns NaN, the return value is specified by onError. When `'throw'`, it will throw an exception, terminating expression execution (default).",
+ values: {
+ TINYMATH,
+ },
+ }),
+ },
+ },
+ fn: (input, args) => {
+ const { expression, onError } = args;
+ const onErrorValue = onError ?? 'throw';
+
+ if (!expression || expression.trim() === '') {
+ throw errors.emptyExpression();
+ }
+
+ const mathContext = isDatatable(input)
+ ? pivotObjectArray(
+ input.rows,
+ input.columns.map((col) => col.name)
+ )
+ : { value: input };
+
+ try {
+ const result = evaluate(expression, mathContext);
+ if (Array.isArray(result)) {
+ if (result.length === 1) {
+ return result[0];
+ }
+ throw errors.tooManyResults();
+ }
+ if (isNaN(result)) {
+ // make TS happy
+ if (onErrorValue !== 'throw' && onErrorValue in fallbackValue) {
+ return fallbackValue[onErrorValue];
+ }
+ throw errors.executionFailed();
+ }
+ return result;
+ } catch (e) {
+ if (onErrorValue !== 'throw' && onErrorValue in fallbackValue) {
+ return fallbackValue[onErrorValue];
+ }
+ if (isDatatable(input) && input.rows.length === 0) {
+ throw errors.emptyDatatable();
+ } else {
+ throw e;
+ }
+ }
+ },
+};
diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts
new file mode 100644
index 0000000000000..6b0dce4ff9a2a
--- /dev/null
+++ b/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts
@@ -0,0 +1,150 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { Datatable } from '../../../expression_types';
+import { mapColumn, MapColumnArguments } from '../map_column';
+import { emptyTable, functionWrapper, testTable } from './utils';
+
+const pricePlusTwo = (datatable: Datatable) => Promise.resolve(datatable.rows[0].price + 2);
+
+describe('mapColumn', () => {
+ const fn = functionWrapper(mapColumn);
+ const runFn = (input: Datatable, args: MapColumnArguments) =>
+ fn(input, args) as Promise;
+
+ it('returns a datatable with a new column with the values from mapping a function over each row in a datatable', () => {
+ return runFn(testTable, {
+ id: 'pricePlusTwo',
+ name: 'pricePlusTwo',
+ expression: pricePlusTwo,
+ }).then((result) => {
+ const arbitraryRowIndex = 2;
+
+ expect(result.type).toBe('datatable');
+ expect(result.columns).toEqual([
+ ...testTable.columns,
+ { id: 'pricePlusTwo', name: 'pricePlusTwo', meta: { type: 'number' } },
+ ]);
+ expect(result.columns[result.columns.length - 1]).toHaveProperty('name', 'pricePlusTwo');
+ expect(result.rows[arbitraryRowIndex]).toHaveProperty('pricePlusTwo');
+ });
+ });
+
+ it('overwrites existing column with the new column if an existing column name is provided', () => {
+ return runFn(testTable, { name: 'name', expression: pricePlusTwo }).then((result) => {
+ const nameColumnIndex = result.columns.findIndex(({ name }) => name === 'name');
+ const arbitraryRowIndex = 4;
+
+ expect(result.type).toBe('datatable');
+ expect(result.columns).toHaveLength(testTable.columns.length);
+ expect(result.columns[nameColumnIndex]).toHaveProperty('name', 'name');
+ expect(result.columns[nameColumnIndex].meta).toHaveProperty('type', 'number');
+ expect(result.rows[arbitraryRowIndex]).toHaveProperty('name', 202);
+ });
+ });
+
+ it('adds a column to empty tables', () => {
+ return runFn(emptyTable, { name: 'name', expression: pricePlusTwo }).then((result) => {
+ expect(result.type).toBe('datatable');
+ expect(result.columns).toHaveLength(1);
+ expect(result.columns[0]).toHaveProperty('name', 'name');
+ expect(result.columns[0].meta).toHaveProperty('type', 'null');
+ });
+ });
+
+ it('should assign specific id, different from name, when id arg is passed for new columns', () => {
+ return runFn(emptyTable, { name: 'name', id: 'myid', expression: pricePlusTwo }).then(
+ (result) => {
+ expect(result.type).toBe('datatable');
+ expect(result.columns).toHaveLength(1);
+ expect(result.columns[0]).toHaveProperty('name', 'name');
+ expect(result.columns[0]).toHaveProperty('id', 'myid');
+ expect(result.columns[0].meta).toHaveProperty('type', 'null');
+ }
+ );
+ });
+
+ it('should assign specific id, different from name, when id arg is passed for copied column', () => {
+ return runFn(testTable, { name: 'name', id: 'myid', expression: pricePlusTwo }).then(
+ (result) => {
+ const nameColumnIndex = result.columns.findIndex(({ name }) => name === 'name');
+ expect(result.type).toBe('datatable');
+ expect(result.columns[nameColumnIndex]).toEqual({
+ id: 'myid',
+ name: 'name',
+ meta: { type: 'number' },
+ });
+ }
+ );
+ });
+
+ it('should copy over the meta information from the specified column', () => {
+ return runFn(
+ {
+ ...testTable,
+ columns: [
+ ...testTable.columns,
+ // add a new entry
+ {
+ id: 'myId',
+ name: 'myName',
+ meta: { type: 'date', params: { id: 'number', params: { digits: 2 } } },
+ },
+ ],
+ rows: testTable.rows.map((row) => ({ ...row, myId: Date.now() })),
+ },
+ { name: 'name', copyMetaFrom: 'myId', expression: pricePlusTwo }
+ ).then((result) => {
+ const nameColumnIndex = result.columns.findIndex(({ name }) => name === 'name');
+ expect(result.type).toBe('datatable');
+ expect(result.columns[nameColumnIndex]).toEqual({
+ id: 'name',
+ name: 'name',
+ meta: { type: 'date', params: { id: 'number', params: { digits: 2 } } },
+ });
+ });
+ });
+
+ it('should be resilient if the references column for meta information does not exists', () => {
+ return runFn(emptyTable, { name: 'name', copyMetaFrom: 'time', expression: pricePlusTwo }).then(
+ (result) => {
+ expect(result.type).toBe('datatable');
+ expect(result.columns).toHaveLength(1);
+ expect(result.columns[0]).toHaveProperty('name', 'name');
+ expect(result.columns[0]).toHaveProperty('id', 'name');
+ expect(result.columns[0].meta).toHaveProperty('type', 'null');
+ }
+ );
+ });
+
+ it('should correctly infer the type fromt he first row if the references column for meta information does not exists', () => {
+ return runFn(
+ { ...emptyTable, rows: [...emptyTable.rows, { value: 5 }] },
+ { name: 'value', copyMetaFrom: 'time', expression: pricePlusTwo }
+ ).then((result) => {
+ expect(result.type).toBe('datatable');
+ expect(result.columns).toHaveLength(1);
+ expect(result.columns[0]).toHaveProperty('name', 'value');
+ expect(result.columns[0]).toHaveProperty('id', 'value');
+ expect(result.columns[0].meta).toHaveProperty('type', 'number');
+ });
+ });
+
+ describe('expression', () => {
+ it('maps null values to the new column', () => {
+ return runFn(testTable, { name: 'empty' }).then((result) => {
+ const emptyColumnIndex = result.columns.findIndex(({ name }) => name === 'empty');
+ const arbitraryRowIndex = 8;
+
+ expect(result.columns[emptyColumnIndex]).toHaveProperty('name', 'empty');
+ expect(result.columns[emptyColumnIndex].meta).toHaveProperty('type', 'null');
+ expect(result.rows[arbitraryRowIndex]).toHaveProperty('empty', null);
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.test.js b/src/plugins/expressions/common/expression_functions/specs/tests/math.test.ts
similarity index 63%
rename from x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.test.js
rename to src/plugins/expressions/common/expression_functions/specs/tests/math.test.ts
index f5b8123ab8568..7541852cdbdaf 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.test.js
+++ b/src/plugins/expressions/common/expression_functions/specs/tests/math.test.ts
@@ -1,19 +1,16 @@
/*
* 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.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
-import { functionWrapper } from '../../../test_helpers/function_wrapper';
-import { getFunctionErrors } from '../../../i18n';
-import { emptyTable, testTable } from './__fixtures__/test_tables';
-import { math } from './math';
-
-const errors = getFunctionErrors().math;
+import { errors, math } from '../math';
+import { emptyTable, functionWrapper, testTable } from './utils';
describe('math', () => {
- const fn = functionWrapper(math);
+ const fn = functionWrapper(math);
it('evaluates math expressions without reference to context', () => {
expect(fn(null, { expression: '10.5345' })).toBe(10.5345);
@@ -48,6 +45,19 @@ describe('math', () => {
expect(fn(testTable, { expression: 'count(name)' })).toBe(9);
});
});
+
+ describe('onError', () => {
+ it('should return the desired fallback value, for invalid expressions', () => {
+ expect(fn(testTable, { expression: 'mean(name)', onError: 'zero' })).toBe(0);
+ expect(fn(testTable, { expression: 'mean(name)', onError: 'null' })).toBe(null);
+ expect(fn(testTable, { expression: 'mean(name)', onError: 'false' })).toBe(false);
+ });
+ it('should return the desired fallback value, for division by zero', () => {
+ expect(fn(testTable, { expression: '1/0', onError: 'zero' })).toBe(0);
+ expect(fn(testTable, { expression: '1/0', onError: 'null' })).toBe(null);
+ expect(fn(testTable, { expression: '1/0', onError: 'false' })).toBe(false);
+ });
+ });
});
describe('invalid expressions', () => {
@@ -88,5 +98,23 @@ describe('math', () => {
new RegExp(errors.emptyDatatable().message)
);
});
+
+ it('should not throw when requesting fallback values for invalid expression', () => {
+ expect(() => fn(testTable, { expression: 'mean(name)', onError: 'zero' })).not.toThrow();
+ expect(() => fn(testTable, { expression: 'mean(name)', onError: 'false' })).not.toThrow();
+ expect(() => fn(testTable, { expression: 'mean(name)', onError: 'null' })).not.toThrow();
+ });
+
+ it('should throw when declared in the onError argument', () => {
+ expect(() => fn(testTable, { expression: 'mean(name)', onError: 'throw' })).toThrow(
+ new RegExp(errors.executionFailed().message)
+ );
+ });
+
+ it('should throw when dividing by zero', () => {
+ expect(() => fn(testTable, { expression: '1/0', onError: 'throw' })).toThrow(
+ new RegExp('Cannot divide by 0')
+ );
+ });
});
});
diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/utils.ts b/src/plugins/expressions/common/expression_functions/specs/tests/utils.ts
index 9006de9067616..7369570cf2c4b 100644
--- a/src/plugins/expressions/common/expression_functions/specs/tests/utils.ts
+++ b/src/plugins/expressions/common/expression_functions/specs/tests/utils.ts
@@ -9,16 +9,219 @@
import { mapValues } from 'lodash';
import { AnyExpressionFunctionDefinition } from '../../types';
import { ExecutionContext } from '../../../execution/types';
+import { Datatable } from '../../../expression_types';
/**
* Takes a function spec and passes in default args,
* overriding with any provided args.
*/
-export const functionWrapper = (spec: AnyExpressionFunctionDefinition) => {
+export const functionWrapper = (
+ spec: AnyExpressionFunctionDefinition
+) => {
const defaultArgs = mapValues(spec.args, (argSpec) => argSpec.default);
return (
- context: object | null,
+ context: ContextType,
args: Record = {},
handlers: ExecutionContext = {} as ExecutionContext
) => spec.fn(context, { ...defaultArgs, ...args }, handlers);
};
+
+const emptyTable: Datatable = {
+ type: 'datatable',
+ columns: [],
+ rows: [],
+};
+
+const testTable: Datatable = {
+ type: 'datatable',
+ columns: [
+ {
+ id: 'name',
+ name: 'name',
+ meta: { type: 'string' },
+ },
+ {
+ id: 'time',
+ name: 'time',
+ meta: { type: 'date' },
+ },
+ {
+ id: 'price',
+ name: 'price',
+ meta: { type: 'number' },
+ },
+ {
+ id: 'quantity',
+ name: 'quantity',
+ meta: { type: 'number' },
+ },
+ {
+ id: 'in_stock',
+ name: 'in_stock',
+ meta: { type: 'boolean' },
+ },
+ ],
+ rows: [
+ {
+ name: 'product1',
+ time: 1517842800950, // 05 Feb 2018 15:00:00 GMT
+ price: 605,
+ quantity: 100,
+ in_stock: true,
+ },
+ {
+ name: 'product1',
+ time: 1517929200950, // 06 Feb 2018 15:00:00 GMT
+ price: 583,
+ quantity: 200,
+ in_stock: true,
+ },
+ {
+ name: 'product1',
+ time: 1518015600950, // 07 Feb 2018 15:00:00 GMT
+ price: 420,
+ quantity: 300,
+ in_stock: true,
+ },
+ {
+ name: 'product2',
+ time: 1517842800950, // 05 Feb 2018 15:00:00 GMT
+ price: 216,
+ quantity: 350,
+ in_stock: false,
+ },
+ {
+ name: 'product2',
+ time: 1517929200950, // 06 Feb 2018 15:00:00 GMT
+ price: 200,
+ quantity: 256,
+ in_stock: false,
+ },
+ {
+ name: 'product2',
+ time: 1518015600950, // 07 Feb 2018 15:00:00 GMT
+ price: 190,
+ quantity: 231,
+ in_stock: false,
+ },
+ {
+ name: 'product3',
+ time: 1517842800950, // 05 Feb 2018 15:00:00 GMT
+ price: 67,
+ quantity: 240,
+ in_stock: true,
+ },
+ {
+ name: 'product4',
+ time: 1517842800950, // 05 Feb 2018 15:00:00 GMT
+ price: 311,
+ quantity: 447,
+ in_stock: false,
+ },
+ {
+ name: 'product5',
+ time: 1517842800950, // 05 Feb 2018 15:00:00 GMT
+ price: 288,
+ quantity: 384,
+ in_stock: true,
+ },
+ ],
+};
+
+const stringTable: Datatable = {
+ type: 'datatable',
+ columns: [
+ {
+ id: 'name',
+ name: 'name',
+ meta: { type: 'string' },
+ },
+ {
+ id: 'time',
+ name: 'time',
+ meta: { type: 'string' },
+ },
+ {
+ id: 'price',
+ name: 'price',
+ meta: { type: 'string' },
+ },
+ {
+ id: 'quantity',
+ name: 'quantity',
+ meta: { type: 'string' },
+ },
+ {
+ id: 'in_stock',
+ name: 'in_stock',
+ meta: { type: 'string' },
+ },
+ ],
+ rows: [
+ {
+ name: 'product1',
+ time: '2018-02-05T15:00:00.950Z',
+ price: '605',
+ quantity: '100',
+ in_stock: 'true',
+ },
+ {
+ name: 'product1',
+ time: '2018-02-06T15:00:00.950Z',
+ price: '583',
+ quantity: '200',
+ in_stock: 'true',
+ },
+ {
+ name: 'product1',
+ time: '2018-02-07T15:00:00.950Z',
+ price: '420',
+ quantity: '300',
+ in_stock: 'true',
+ },
+ {
+ name: 'product2',
+ time: '2018-02-05T15:00:00.950Z',
+ price: '216',
+ quantity: '350',
+ in_stock: 'false',
+ },
+ {
+ name: 'product2',
+ time: '2018-02-06T15:00:00.950Z',
+ price: '200',
+ quantity: '256',
+ in_stock: 'false',
+ },
+ {
+ name: 'product2',
+ time: '2018-02-07T15:00:00.950Z',
+ price: '190',
+ quantity: '231',
+ in_stock: 'false',
+ },
+ {
+ name: 'product3',
+ time: '2018-02-05T15:00:00.950Z',
+ price: '67',
+ quantity: '240',
+ in_stock: 'true',
+ },
+ {
+ name: 'product4',
+ time: '2018-02-05T15:00:00.950Z',
+ price: '311',
+ quantity: '447',
+ in_stock: 'false',
+ },
+ {
+ name: 'product5',
+ time: '2018-02-05T15:00:00.950Z',
+ price: '288',
+ quantity: '384',
+ in_stock: 'true',
+ },
+ ],
+};
+
+export { emptyTable, testTable, stringTable };
diff --git a/src/plugins/vis_type_xy/server/plugin.ts b/src/plugins/vis_type_xy/server/plugin.ts
index fd670e288ff5b..a9e6020cf3ee8 100644
--- a/src/plugins/vis_type_xy/server/plugin.ts
+++ b/src/plugins/vis_type_xy/server/plugin.ts
@@ -20,6 +20,7 @@ export const uiSettingsConfig: Record> = {
name: i18n.translate('visTypeXy.advancedSettings.visualization.legacyChartsLibrary.name', {
defaultMessage: 'Legacy charts library',
}),
+ requiresPageReload: true,
value: false,
description: i18n.translate(
'visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description',
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/index.ts
index 048bc3468f149..5c4d1d55cff04 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/index.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/index.ts
@@ -34,8 +34,6 @@ import { joinRows } from './join_rows';
import { lt } from './lt';
import { lte } from './lte';
import { mapCenter } from './map_center';
-import { mapColumn } from './mapColumn';
-import { math } from './math';
import { metric } from './metric';
import { neq } from './neq';
import { ply } from './ply';
@@ -89,8 +87,6 @@ export const functions = [
lte,
joinRows,
mapCenter,
- mapColumn,
- math,
metric,
neq,
ply,
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.test.js
deleted file mode 100644
index d511c7774122d..0000000000000
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.test.js
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { functionWrapper } from '../../../test_helpers/function_wrapper';
-import { testTable, emptyTable } from './__fixtures__/test_tables';
-import { mapColumn } from './mapColumn';
-
-const pricePlusTwo = (datatable) => Promise.resolve(datatable.rows[0].price + 2);
-
-describe('mapColumn', () => {
- const fn = functionWrapper(mapColumn);
-
- it('returns a datatable with a new column with the values from mapping a function over each row in a datatable', () => {
- return fn(testTable, {
- id: 'pricePlusTwo',
- name: 'pricePlusTwo',
- expression: pricePlusTwo,
- }).then((result) => {
- const arbitraryRowIndex = 2;
-
- expect(result.type).toBe('datatable');
- expect(result.columns).toEqual([
- ...testTable.columns,
- { id: 'pricePlusTwo', name: 'pricePlusTwo', meta: { type: 'number' } },
- ]);
- expect(result.columns[result.columns.length - 1]).toHaveProperty('name', 'pricePlusTwo');
- expect(result.rows[arbitraryRowIndex]).toHaveProperty('pricePlusTwo');
- });
- });
-
- it('overwrites existing column with the new column if an existing column name is provided', () => {
- return fn(testTable, { name: 'name', expression: pricePlusTwo }).then((result) => {
- const nameColumnIndex = result.columns.findIndex(({ name }) => name === 'name');
- const arbitraryRowIndex = 4;
-
- expect(result.type).toBe('datatable');
- expect(result.columns).toHaveLength(testTable.columns.length);
- expect(result.columns[nameColumnIndex]).toHaveProperty('name', 'name');
- expect(result.columns[nameColumnIndex].meta).toHaveProperty('type', 'number');
- expect(result.rows[arbitraryRowIndex]).toHaveProperty('name', 202);
- });
- });
-
- it('adds a column to empty tables', () => {
- return fn(emptyTable, { name: 'name', expression: pricePlusTwo }).then((result) => {
- expect(result.type).toBe('datatable');
- expect(result.columns).toHaveLength(1);
- expect(result.columns[0]).toHaveProperty('name', 'name');
- expect(result.columns[0].meta).toHaveProperty('type', 'null');
- });
- });
-
- describe('expression', () => {
- it('maps null values to the new column', () => {
- return fn(testTable, { name: 'empty' }).then((result) => {
- const emptyColumnIndex = result.columns.findIndex(({ name }) => name === 'empty');
- const arbitraryRowIndex = 8;
-
- expect(result.columns[emptyColumnIndex]).toHaveProperty('name', 'empty');
- expect(result.columns[emptyColumnIndex].meta).toHaveProperty('type', 'null');
- expect(result.rows[arbitraryRowIndex]).toHaveProperty('empty', null);
- });
- });
- });
-});
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.ts
deleted file mode 100644
index 63cc0d6cbc687..0000000000000
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { Datatable, ExpressionFunctionDefinition, getType } from '../../../types';
-import { getFunctionHelp } from '../../../i18n';
-
-interface Arguments {
- name: string;
- expression: (datatable: Datatable) => Promise;
-}
-
-export function mapColumn(): ExpressionFunctionDefinition<
- 'mapColumn',
- Datatable,
- Arguments,
- Promise
-> {
- const { help, args: argHelp } = getFunctionHelp().mapColumn;
-
- return {
- name: 'mapColumn',
- aliases: ['mc'], // midnight commander. So many times I've launched midnight commander instead of moving a file.
- type: 'datatable',
- inputTypes: ['datatable'],
- help,
- args: {
- name: {
- types: ['string'],
- aliases: ['_', 'column'],
- help: argHelp.name,
- required: true,
- },
- expression: {
- types: ['boolean', 'number', 'string', 'null'],
- resolve: false,
- aliases: ['exp', 'fn', 'function'],
- help: argHelp.expression,
- required: true,
- },
- },
- fn: (input, args) => {
- const expression = args.expression || (() => Promise.resolve(null));
-
- const columns = [...input.columns];
- const rowPromises = input.rows.map((row) => {
- return expression({
- type: 'datatable',
- columns,
- rows: [row],
- }).then((val) => ({
- ...row,
- [args.name]: val,
- }));
- });
-
- return Promise.all(rowPromises).then((rows) => {
- const existingColumnIndex = columns.findIndex(({ name }) => name === args.name);
- const type = rows.length ? getType(rows[0][args.name]) : 'null';
- const newColumn = { id: args.name, name: args.name, meta: { type } };
-
- if (existingColumnIndex === -1) {
- columns.push(newColumn);
- } else {
- columns[existingColumnIndex] = newColumn;
- }
-
- return {
- type: 'datatable',
- columns,
- rows,
- } as Datatable;
- });
- },
- };
-}
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts
deleted file mode 100644
index af70fa729b7da..0000000000000
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { evaluate } from '@kbn/tinymath';
-import { pivotObjectArray } from '../../../common/lib/pivot_object_array';
-import { Datatable, isDatatable, ExpressionFunctionDefinition } from '../../../types';
-import { getFunctionHelp, getFunctionErrors } from '../../../i18n';
-
-interface Arguments {
- expression: string;
-}
-
-type Input = number | Datatable;
-
-export function math(): ExpressionFunctionDefinition<'math', Input, Arguments, number> {
- const { help, args: argHelp } = getFunctionHelp().math;
- const errors = getFunctionErrors().math;
-
- return {
- name: 'math',
- type: 'number',
- inputTypes: ['number', 'datatable'],
- help,
- args: {
- expression: {
- aliases: ['_'],
- types: ['string'],
- help: argHelp.expression,
- },
- },
- fn: (input, args) => {
- const { expression } = args;
-
- if (!expression || expression.trim() === '') {
- throw errors.emptyExpression();
- }
-
- const mathContext = isDatatable(input)
- ? pivotObjectArray(
- input.rows,
- input.columns.map((col) => col.name)
- )
- : { value: input };
-
- try {
- const result = evaluate(expression, mathContext);
- if (Array.isArray(result)) {
- if (result.length === 1) {
- return result[0];
- }
- throw errors.tooManyResults();
- }
- if (isNaN(result)) {
- throw errors.executionFailed();
- }
- return result;
- } catch (e) {
- if (isDatatable(input) && input.rows.length === 0) {
- throw errors.emptyDatatable();
- } else {
- throw e;
- }
- }
- },
- };
-}
diff --git a/x-pack/plugins/canvas/i18n/functions/dict/map_column.ts b/x-pack/plugins/canvas/i18n/functions/dict/map_column.ts
deleted file mode 100644
index f8d0311d08961..0000000000000
--- a/x-pack/plugins/canvas/i18n/functions/dict/map_column.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { i18n } from '@kbn/i18n';
-import { mapColumn } from '../../../canvas_plugin_src/functions/common/mapColumn';
-import { FunctionHelp } from '../function_help';
-import { FunctionFactory } from '../../../types';
-import { CANVAS, DATATABLE } from '../../constants';
-
-export const help: FunctionHelp> = {
- help: i18n.translate('xpack.canvas.functions.mapColumnHelpText', {
- defaultMessage:
- 'Adds a column calculated as the result of other columns. ' +
- 'Changes are made only when you provide arguments.' +
- 'See also {alterColumnFn} and {staticColumnFn}.',
- values: {
- alterColumnFn: '`alterColumn`',
- staticColumnFn: '`staticColumn`',
- },
- }),
- args: {
- name: i18n.translate('xpack.canvas.functions.mapColumn.args.nameHelpText', {
- defaultMessage: 'The name of the resulting column.',
- }),
- expression: i18n.translate('xpack.canvas.functions.mapColumn.args.expressionHelpText', {
- defaultMessage:
- 'A {CANVAS} expression that is passed to each row as a single row {DATATABLE}.',
- values: {
- CANVAS,
- DATATABLE,
- },
- }),
- },
-};
diff --git a/x-pack/plugins/canvas/i18n/functions/dict/math.ts b/x-pack/plugins/canvas/i18n/functions/dict/math.ts
deleted file mode 100644
index 110136872e1ff..0000000000000
--- a/x-pack/plugins/canvas/i18n/functions/dict/math.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { i18n } from '@kbn/i18n';
-import { math } from '../../../canvas_plugin_src/functions/common/math';
-import { FunctionHelp } from '../function_help';
-import { FunctionFactory } from '../../../types';
-import { DATATABLE, CONTEXT, TINYMATH, TINYMATH_URL, TYPE_NUMBER } from '../../constants';
-
-export const help: FunctionHelp> = {
- help: i18n.translate('xpack.canvas.functions.mathHelpText', {
- defaultMessage:
- 'Interprets a {TINYMATH} math expression using a {TYPE_NUMBER} or {DATATABLE} as {CONTEXT}. ' +
- 'The {DATATABLE} columns are available by their column name. ' +
- 'If the {CONTEXT} is a number it is available as {value}.',
- values: {
- TINYMATH,
- CONTEXT,
- DATATABLE,
- value: '`value`',
- TYPE_NUMBER,
- },
- }),
- args: {
- expression: i18n.translate('xpack.canvas.functions.math.args.expressionHelpText', {
- defaultMessage: 'An evaluated {TINYMATH} expression. See {TINYMATH_URL}.',
- values: {
- TINYMATH,
- TINYMATH_URL,
- },
- }),
- },
-};
-
-export const errors = {
- emptyExpression: () =>
- new Error(
- i18n.translate('xpack.canvas.functions.math.emptyExpressionErrorMessage', {
- defaultMessage: 'Empty expression',
- })
- ),
- tooManyResults: () =>
- new Error(
- i18n.translate('xpack.canvas.functions.math.tooManyResultsErrorMessage', {
- defaultMessage:
- 'Expressions must return a single number. Try wrapping your expression in {mean} or {sum}',
- values: {
- mean: 'mean()',
- sum: 'sum()',
- },
- })
- ),
- executionFailed: () =>
- new Error(
- i18n.translate('xpack.canvas.functions.math.executionFailedErrorMessage', {
- defaultMessage: 'Failed to execute math expression. Check your column names',
- })
- ),
- emptyDatatable: () =>
- new Error(
- i18n.translate('xpack.canvas.functions.math.emptyDatatableErrorMessage', {
- defaultMessage: 'Empty datatable',
- })
- ),
-};
diff --git a/x-pack/plugins/canvas/i18n/functions/function_errors.ts b/x-pack/plugins/canvas/i18n/functions/function_errors.ts
index ac86eb4c4d0e9..4a85018c1b4ac 100644
--- a/x-pack/plugins/canvas/i18n/functions/function_errors.ts
+++ b/x-pack/plugins/canvas/i18n/functions/function_errors.ts
@@ -16,7 +16,6 @@ import { errors as demodata } from './dict/demodata';
import { errors as getCell } from './dict/get_cell';
import { errors as image } from './dict/image';
import { errors as joinRows } from './dict/join_rows';
-import { errors as math } from './dict/math';
import { errors as ply } from './dict/ply';
import { errors as pointseries } from './dict/pointseries';
import { errors as progress } from './dict/progress';
@@ -36,7 +35,6 @@ export const getFunctionErrors = () => ({
getCell,
image,
joinRows,
- math,
ply,
pointseries,
progress,
diff --git a/x-pack/plugins/canvas/i18n/functions/function_help.ts b/x-pack/plugins/canvas/i18n/functions/function_help.ts
index 245732e53cc89..512ebc4ff8c93 100644
--- a/x-pack/plugins/canvas/i18n/functions/function_help.ts
+++ b/x-pack/plugins/canvas/i18n/functions/function_help.ts
@@ -46,9 +46,7 @@ import { help as location } from './dict/location';
import { help as lt } from './dict/lt';
import { help as lte } from './dict/lte';
import { help as mapCenter } from './dict/map_center';
-import { help as mapColumn } from './dict/map_column';
import { help as markdown } from './dict/markdown';
-import { help as math } from './dict/math';
import { help as metric } from './dict/metric';
import { help as neq } from './dict/neq';
import { help as pie } from './dict/pie';
@@ -209,9 +207,7 @@ export const getFunctionHelp = (): FunctionHelpDict => ({
lt,
lte,
mapCenter,
- mapColumn,
markdown,
- math,
metric,
neq,
pie,
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
index a9845c2315604..53871eae6dba1 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
@@ -183,6 +183,13 @@ export const setup = async (arg?: {
const enable = (phase: Phases) => createFormToggleAction(`enablePhaseSwitch-${phase}`);
+ const showDataAllocationOptions = (phase: Phases) => () => {
+ act(() => {
+ find(`${phase}-dataTierAllocationControls.dataTierSelect`).simulate('click');
+ });
+ component.update();
+ };
+
const createMinAgeActions = (phase: Phases) => {
return {
hasMinAgeInput: () => exists(`${phase}-selectedMinimumAge`),
@@ -379,6 +386,7 @@ export const setup = async (arg?: {
},
warm: {
enable: enable('warm'),
+ showDataAllocationOptions: showDataAllocationOptions('warm'),
...createMinAgeActions('warm'),
setReplicas: setReplicas('warm'),
hasErrorIndicator: () => exists('phaseErrorIndicator-warm'),
@@ -390,6 +398,7 @@ export const setup = async (arg?: {
},
cold: {
enable: enable('cold'),
+ showDataAllocationOptions: showDataAllocationOptions('cold'),
...createMinAgeActions('cold'),
setReplicas: setReplicas('cold'),
setFreeze,
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
index 7fe5c6f50d046..ffa9b1aa236a0 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
@@ -693,43 +693,52 @@ describe(' ', () => {
expect(find('cold-dataTierAllocationControls.dataTierSelect').text()).toContain('Off');
});
});
- });
-
- describe('searchable snapshot', () => {
describe('on cloud', () => {
- describe('new policy', () => {
+ describe('using legacy data role config', () => {
beforeEach(async () => {
- // simulate creating a new policy
- httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('')]);
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
httpRequestsMockHelpers.setListNodes({
- isUsingDeprecatedDataRoleConfig: false,
nodesByAttributes: { test: ['123'] },
- nodesByRoles: { data: ['123'] },
+ // On cloud, even if there are data_* roles set, the default, recommended allocation option should not
+ // be available.
+ nodesByRoles: { data_hot: ['123'] },
+ isUsingDeprecatedDataRoleConfig: true,
});
httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] });
await act(async () => {
- testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } });
+ testBed = await setup({
+ appServicesContext: {
+ cloud: {
+ isCloudEnabled: true,
+ },
+ license: licensingMock.createLicense({ license: { type: 'basic' } }),
+ },
+ });
});
const { component } = testBed;
component.update();
});
- test('defaults searchable snapshot to true on cloud', async () => {
- const { find, actions } = testBed;
- await actions.cold.enable(true);
- expect(
- find('searchableSnapshotField-cold.searchableSnapshotToggle').props()['aria-checked']
- ).toBe(true);
+ test('removes default, recommended option', async () => {
+ const { actions, find } = testBed;
+ await actions.warm.enable(true);
+ actions.warm.showDataAllocationOptions();
+
+ expect(find('defaultDataAllocationOption').exists()).toBeFalsy();
+ expect(find('customDataAllocationOption').exists()).toBeTruthy();
+ expect(find('noneDataAllocationOption').exists()).toBeTruthy();
+ // Show the call-to-action for users to migrate their cluster to use node roles
+ expect(find('cloudDataTierCallout').exists()).toBeTruthy();
});
});
- describe('existing policy', () => {
+ describe('using node roles', () => {
beforeEach(async () => {
httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
httpRequestsMockHelpers.setListNodes({
- isUsingDeprecatedDataRoleConfig: false,
nodesByAttributes: { test: ['123'] },
- nodesByRoles: { data: ['123'] },
+ nodesByRoles: { data_hot: ['123'] },
+ isUsingDeprecatedDataRoleConfig: false,
});
httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] });
@@ -740,19 +749,34 @@ describe(' ', () => {
const { component } = testBed;
component.update();
});
- test('correctly sets snapshot repository default to "found-snapshots"', async () => {
- const { actions } = testBed;
+
+ test('should show recommended, custom and "off" options on cloud with data roles', async () => {
+ const { actions, find } = testBed;
+
+ await actions.warm.enable(true);
+ actions.warm.showDataAllocationOptions();
+ expect(find('defaultDataAllocationOption').exists()).toBeTruthy();
+ expect(find('customDataAllocationOption').exists()).toBeTruthy();
+ expect(find('noneDataAllocationOption').exists()).toBeTruthy();
+ // We should not be showing the call-to-action for users to activate the cold tier on cloud
+ expect(find('cloudMissingColdTierCallout').exists()).toBeFalsy();
+ // Do not show the call-to-action for users to migrate their cluster to use node roles
+ expect(find('cloudDataTierCallout').exists()).toBeFalsy();
+ });
+
+ test('should show cloud notice when cold tier nodes do not exist', async () => {
+ const { actions, find } = testBed;
await actions.cold.enable(true);
- await actions.cold.toggleSearchableSnapshot(true);
- await actions.savePolicy();
- const latestRequest = server.requests[server.requests.length - 1];
- const request = JSON.parse(JSON.parse(latestRequest.requestBody).body);
- expect(request.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual(
- 'found-snapshots'
- );
+ expect(find('cloudMissingColdTierCallout').exists()).toBeTruthy();
+ // Assert that other notices are not showing
+ expect(find('defaultAllocationNotice').exists()).toBeFalsy();
+ expect(find('noNodeAttributesWarning').exists()).toBeFalsy();
});
});
});
+ });
+
+ describe('searchable snapshot', () => {
describe('on non-enterprise license', () => {
beforeEach(async () => {
httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
@@ -789,6 +813,64 @@ describe(' ', () => {
expect(actions.cold.searchableSnapshotDisabledDueToLicense()).toBeTruthy();
});
});
+
+ describe('on cloud', () => {
+ describe('new policy', () => {
+ beforeEach(async () => {
+ // simulate creating a new policy
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('')]);
+ httpRequestsMockHelpers.setListNodes({
+ nodesByAttributes: { test: ['123'] },
+ nodesByRoles: { data: ['123'] },
+ isUsingDeprecatedDataRoleConfig: false,
+ });
+ httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] });
+
+ await act(async () => {
+ testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } });
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+ test('defaults searchable snapshot to true on cloud', async () => {
+ const { find, actions } = testBed;
+ await actions.cold.enable(true);
+ expect(
+ find('searchableSnapshotField-cold.searchableSnapshotToggle').props()['aria-checked']
+ ).toBe(true);
+ });
+ });
+ describe('existing policy', () => {
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
+ httpRequestsMockHelpers.setListNodes({
+ isUsingDeprecatedDataRoleConfig: false,
+ nodesByAttributes: { test: ['123'] },
+ nodesByRoles: { data_hot: ['123'] },
+ });
+ httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] });
+
+ await act(async () => {
+ testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } });
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+ test('correctly sets snapshot repository default to "found-snapshots"', async () => {
+ const { actions } = testBed;
+ await actions.cold.enable(true);
+ await actions.cold.toggleSearchableSnapshot(true);
+ await actions.savePolicy();
+ const latestRequest = server.requests[server.requests.length - 1];
+ const request = JSON.parse(JSON.parse(latestRequest.requestBody).body);
+ expect(request.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual(
+ 'found-snapshots'
+ );
+ });
+ });
+ });
});
describe('with rollover', () => {
beforeEach(async () => {
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts
index 113698fdf6df2..b02d190d10899 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts
@@ -125,7 +125,7 @@ describe(' node allocation', () => {
expect(actions.warm.hasDefaultAllocationWarning()).toBeTruthy();
});
- test('shows default allocation notice when hot tier exists, but not warm tier', async () => {
+ test('when configuring warm phase shows default allocation notice when hot tier exists, but not warm tier', async () => {
httpRequestsMockHelpers.setListNodes({
nodesByAttributes: {},
nodesByRoles: { data_hot: ['test'], data_cold: ['test'] },
@@ -309,7 +309,7 @@ describe(' node allocation', () => {
describe('on cloud', () => {
describe('with deprecated data role config', () => {
- test('should hide data tier option on cloud using legacy node role configuration', async () => {
+ test('should hide data tier option on cloud', async () => {
httpRequestsMockHelpers.setListNodes({
nodesByAttributes: { test: ['123'] },
// On cloud, if using legacy config there will not be any "data_*" roles set.
@@ -331,10 +331,29 @@ describe(' node allocation', () => {
expect(exists('customDataAllocationOption')).toBeTruthy();
expect(exists('noneDataAllocationOption')).toBeTruthy();
});
+
+ test('should ask users to migrate to node roles when on cloud using legacy data role', async () => {
+ httpRequestsMockHelpers.setListNodes({
+ nodesByAttributes: { test: ['123'] },
+ // On cloud, if using legacy config there will not be any "data_*" roles set.
+ nodesByRoles: { data: ['test'] },
+ isUsingDeprecatedDataRoleConfig: true,
+ });
+ await act(async () => {
+ testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } });
+ });
+ const { actions, component, exists } = testBed;
+
+ component.update();
+ await actions.warm.enable(true);
+ expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy();
+
+ expect(exists('cloudDataTierCallout')).toBeTruthy();
+ });
});
describe('with node role config', () => {
- test('shows off, custom and data role options on cloud with data roles', async () => {
+ test('shows data role, custom and "off" options on cloud with data roles', async () => {
httpRequestsMockHelpers.setListNodes({
nodesByAttributes: { test: ['123'] },
nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] },
@@ -372,7 +391,7 @@ describe(' node allocation', () => {
await actions.cold.enable(true);
expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy();
- expect(exists('cloudDataTierCallout')).toBeTruthy();
+ expect(exists('cloudMissingColdTierCallout')).toBeTruthy();
// Assert that other notices are not showing
expect(actions.cold.hasDefaultAllocationNotice()).toBeFalsy();
expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy();
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx
index 4d3dbbba39037..351d6ac1c530b 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx
@@ -7,21 +7,38 @@
import { i18n } from '@kbn/i18n';
import React, { FunctionComponent } from 'react';
-import { EuiCallOut } from '@elastic/eui';
+import { EuiCallOut, EuiLink } from '@elastic/eui';
const i18nTexts = {
title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.title', {
- defaultMessage: 'Create a cold tier',
+ defaultMessage: 'Migrate to data tiers',
}),
body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.body', {
- defaultMessage: 'Edit your Elastic Cloud deployment to set up a cold tier.',
+ defaultMessage: 'Migrate your Elastic Cloud deployment to use data tiers.',
}),
+ linkText: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.linkToCloudDeploymentDescription',
+ { defaultMessage: 'View cloud deployment' }
+ ),
};
-export const CloudDataTierCallout: FunctionComponent = () => {
+interface Props {
+ linkToCloudDeployment?: string;
+}
+
+/**
+ * A call-to-action for users to migrate to data tiers if their cluster is still running
+ * the deprecated node.data:true config.
+ */
+export const CloudDataTierCallout: FunctionComponent = ({ linkToCloudDeployment }) => {
return (
- {i18nTexts.body}
+ {i18nTexts.body}{' '}
+ {Boolean(linkToCloudDeployment) && (
+
+ {i18nTexts.linkText}
+
+ )}
);
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx
index 562267089051a..e43b750849774 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx
@@ -11,8 +11,6 @@ import { EuiCallOut } from '@elastic/eui';
import { PhaseWithAllocation, DataTierRole } from '../../../../../../../../../common/types';
-import { AllocationNodeRole } from '../../../../../../../lib';
-
const i18nTextsNodeRoleToDataTier: Record = {
data_hot: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierHotLabel', {
defaultMessage: 'hot',
@@ -84,24 +82,13 @@ const i18nTexts = {
interface Props {
phase: PhaseWithAllocation;
- targetNodeRole: AllocationNodeRole;
+ targetNodeRole: DataTierRole;
}
export const DefaultAllocationNotice: FunctionComponent = ({ phase, targetNodeRole }) => {
- const content =
- targetNodeRole === 'none' ? (
-
- {i18nTexts.warning[phase].body}
-
- ) : (
-
- {i18nTexts.notice[phase].body(targetNodeRole)}
-
- );
-
- return content;
+ return (
+
+ {i18nTexts.notice[phase].body(targetNodeRole)}
+
+ );
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx
new file mode 100644
index 0000000000000..a194f3c07f900
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx
@@ -0,0 +1,59 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import React, { FunctionComponent } from 'react';
+import { EuiCallOut } from '@elastic/eui';
+
+import { PhaseWithAllocation } from '../../../../../../../../../common/types';
+
+const i18nTexts = {
+ warning: {
+ warm: {
+ title: i18n.translate(
+ 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableTitle',
+ { defaultMessage: 'No nodes assigned to the warm tier' }
+ ),
+ body: i18n.translate(
+ 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableBody',
+ {
+ defaultMessage:
+ 'Assign at least one node to the warm or hot tier to use role-based allocation. The policy will fail to complete allocation if there are no available nodes.',
+ }
+ ),
+ },
+ cold: {
+ title: i18n.translate(
+ 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle',
+ { defaultMessage: 'No nodes assigned to the cold tier' }
+ ),
+ body: i18n.translate(
+ 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableBody',
+ {
+ defaultMessage:
+ 'Assign at least one node to the cold, warm, or hot tier to use role-based allocation. The policy will fail to complete allocation if there are no available nodes.',
+ }
+ ),
+ },
+ },
+};
+
+interface Props {
+ phase: PhaseWithAllocation;
+}
+
+export const DefaultAllocationWarning: FunctionComponent = ({ phase }) => {
+ return (
+
+ {i18nTexts.warning[phase].body}
+
+ );
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts
index b3f57ac24e0d7..938e0a850f933 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts
@@ -13,8 +13,12 @@ export { DataTierAllocation } from './data_tier_allocation';
export { DefaultAllocationNotice } from './default_allocation_notice';
+export { DefaultAllocationWarning } from './default_allocation_warning';
+
export { NoNodeAttributesWarning } from './no_node_attributes_warning';
+export { MissingColdTierCallout } from './missing_cold_tier_callout';
+
export { CloudDataTierCallout } from './cloud_data_tier_callout';
export { LoadingError } from './loading_error';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cold_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cold_tier_callout.tsx
new file mode 100644
index 0000000000000..21b8850e0b088
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cold_tier_callout.tsx
@@ -0,0 +1,45 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import React, { FunctionComponent } from 'react';
+import { EuiCallOut, EuiLink } from '@elastic/eui';
+
+const i18nTexts = {
+ title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.title', {
+ defaultMessage: 'Create a cold tier',
+ }),
+ body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.body', {
+ defaultMessage: 'Edit your Elastic Cloud deployment to set up a cold tier.',
+ }),
+ linkText: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.linkToCloudDeploymentDescription',
+ { defaultMessage: 'View cloud deployment' }
+ ),
+};
+
+interface Props {
+ linkToCloudDeployment?: string;
+}
+
+/**
+ * A call-to-action for users to activate their cold tier slider to provision cold tier nodes.
+ * This may need to be change when we have autoscaling enabled on a cluster because nodes may not
+ * yet exist, but will automatically be provisioned.
+ */
+export const MissingColdTierCallout: FunctionComponent = ({ linkToCloudDeployment }) => {
+ return (
+
+ {i18nTexts.body}{' '}
+ {Boolean(linkToCloudDeployment) && (
+
+ {i18nTexts.linkText}
+
+ )}
+
+ );
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx
index ad36039728f5c..7a660e0379a8d 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx
@@ -14,9 +14,7 @@ import { useKibana, useFormData } from '../../../../../../../shared_imports';
import { PhaseWithAllocation } from '../../../../../../../../common/types';
-import { getAvailableNodeRoleForPhase } from '../../../../../../lib/data_tiers';
-
-import { isNodeRoleFirstPreference } from '../../../../../../lib';
+import { getAvailableNodeRoleForPhase, isNodeRoleFirstPreference } from '../../../../../../lib';
import { useLoadNodes } from '../../../../../../services/api';
@@ -25,7 +23,9 @@ import { DataTierAllocationType } from '../../../../types';
import {
DataTierAllocation,
DefaultAllocationNotice,
+ DefaultAllocationWarning,
NoNodeAttributesWarning,
+ MissingColdTierCallout,
CloudDataTierCallout,
LoadingError,
} from './components';
@@ -65,30 +65,48 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr
);
const hasNodeAttrs = Boolean(Object.keys(nodesByAttributes ?? {}).length);
const isCloudEnabled = cloud?.isCloudEnabled ?? false;
+ const cloudDeploymentUrl = cloud?.cloudDeploymentUrl;
const renderNotice = () => {
switch (allocationType) {
case 'node_roles':
- if (isCloudEnabled && phase === 'cold') {
- const isUsingNodeRolesAllocation = !isUsingDeprecatedDataRoleConfig && hasDataNodeRoles;
+ /**
+ * We'll drive Cloud users to add a cold tier to their deployment if there are no nodes with the cold node role.
+ */
+ if (isCloudEnabled && phase === 'cold' && !isUsingDeprecatedDataRoleConfig) {
const hasNoNodesWithNodeRole = !nodesByRoles.data_cold?.length;
- if (isUsingNodeRolesAllocation && hasNoNodesWithNodeRole) {
+ if (hasDataNodeRoles && hasNoNodesWithNodeRole) {
// Tell cloud users they can deploy nodes on cloud.
return (
<>
-
+
>
);
}
}
+ /**
+ * Node role allocation moves data in a phase to a corresponding tier of the same name. To prevent policy execution from getting
+ * stuck ILM allocation will fall back to a previous tier if possible. We show the WARNING below to inform a user when even
+ * this fallback will not succeed.
+ */
const allocationNodeRole = getAvailableNodeRoleForPhase(phase, nodesByRoles);
- if (
- allocationNodeRole === 'none' ||
- !isNodeRoleFirstPreference(phase, allocationNodeRole)
- ) {
+ if (allocationNodeRole === 'none') {
+ return (
+ <>
+
+
+ >
+ );
+ }
+
+ /**
+ * If we are able to fallback to a data tier that does not map to this phase, we show a notice informing the user that their
+ * data will not be assigned to a corresponding tier.
+ */
+ if (!isNodeRoleFirstPreference(phase, allocationNodeRole)) {
return (
<>
@@ -106,6 +124,19 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr
>
);
}
+ /**
+ * Special cloud case: when deprecated data role configuration is in use, it means that this deployment is not using
+ * the new node role based allocation. We drive users to the cloud console to migrate to node role based allocation
+ * in that case.
+ */
+ if (isCloudEnabled && isUsingDeprecatedDataRoleConfig) {
+ return (
+ <>
+
+
+ >
+ );
+ }
break;
default:
return null;
@@ -141,9 +172,7 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr
hasNodeAttributes={hasNodeAttrs}
phase={phase}
nodes={nodesByAttributes}
- disableDataTierOption={Boolean(
- isCloudEnabled && !hasDataNodeRoles && isUsingDeprecatedDataRoleConfig
- )}
+ disableDataTierOption={Boolean(isCloudEnabled && isUsingDeprecatedDataRoleConfig)}
isLoading={isLoading}
/>
diff --git a/x-pack/plugins/index_lifecycle_management/public/types.ts b/x-pack/plugins/index_lifecycle_management/public/types.ts
index 27b3795c6731f..adfca9ad41b26 100644
--- a/x-pack/plugins/index_lifecycle_management/public/types.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/types.ts
@@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
import { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
import { ManagementSetup } from '../../../../src/plugins/management/public';
diff --git a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
index e5594bb0bb769..7aa838021f2a8 100644
--- a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
+++ b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
@@ -1,16 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DragDrop defined dropType is reflected in the className 1`] = `
-
-
- Hello!
-
-
+ Hello!
+
`;
exports[`DragDrop items that has dropType=undefined get special styling when another item is dragged 1`] = `
diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss
index 9e3f1e1c3cf26..961f7ee0ec400 100644
--- a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss
+++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss
@@ -81,13 +81,6 @@
}
}
-.lnsDragDrop__container {
- position: relative;
- overflow: visible !important; // sass-lint:disable-line no-important
- width: 100%;
- height: 100%;
-}
-
.lnsDragDrop__reorderableDrop {
position: absolute;
width: 100%;
diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx
index 618a7accb9b2b..76e44c29eaed5 100644
--- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx
+++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx
@@ -456,7 +456,7 @@ const DropInner = memo(function DropInner(props: DropInnerProps) {
isActiveDropTarget && dropType !== 'reorder' && dragging?.ghost ? dragging.ghost : undefined;
return (
-
+ <>
{React.cloneElement(children, {
'data-test-subj': dataTestSubj || 'lnsDragDrop',
className: classNames(children.props.className, classes, className),
@@ -471,7 +471,7 @@ const DropInner = memo(function DropInner(props: DropInnerProps) {
style: ghost.style,
})
: null}
-
+ >
);
});
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
index d5c0b9ff64807..af5411dd4d3b0 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
@@ -11,9 +11,9 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { Ast } from '@kbn/interpreter/common';
import { i18n } from '@kbn/i18n';
import {
+ EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
- EuiIcon,
EuiText,
EuiButtonEmpty,
EuiLink,
@@ -389,72 +389,84 @@ export const InnerVisualizationWrapper = ({
if (localState.configurationValidationError?.length) {
let showExtraErrors = null;
+ let showExtraErrorsAction = null;
+
if (localState.configurationValidationError.length > 1) {
if (localState.expandError) {
showExtraErrors = localState.configurationValidationError
.slice(1)
.map(({ longMessage }) => (
-
{longMessage}
-
+
));
} else {
- showExtraErrors = (
-
- {
- setLocalState((prevState: WorkspaceState) => ({
- ...prevState,
- expandError: !prevState.expandError,
- }));
- }}
- data-test-subj="configuration-failure-more-errors"
- >
- {i18n.translate('xpack.lens.editorFrame.configurationFailureMoreErrors', {
- defaultMessage: ` +{errors} {errors, plural, one {error} other {errors}}`,
- values: { errors: localState.configurationValidationError.length - 1 },
- })}
-
-
+ showExtraErrorsAction = (
+ {
+ setLocalState((prevState: WorkspaceState) => ({
+ ...prevState,
+ expandError: !prevState.expandError,
+ }));
+ }}
+ data-test-subj="configuration-failure-more-errors"
+ >
+ {i18n.translate('xpack.lens.editorFrame.configurationFailureMoreErrors', {
+ defaultMessage: ` +{errors} {errors, plural, one {error} other {errors}}`,
+ values: { errors: localState.configurationValidationError.length - 1 },
+ })}
+
);
}
}
return (
-
+
-
-
-
- {localState.configurationValidationError[0].longMessage}
+
+
+ {localState.configurationValidationError[0].longMessage}
+
+
+ {showExtraErrors}
+ >
+ }
+ iconColor="danger"
+ iconType="alert"
+ />
- {showExtraErrors}
);
}
if (localState.expressionBuildError?.length) {
return (
-
+
-
-
-
-
+
+
+
+
+ {localState.expressionBuildError[0].longMessage}
+ >
+ }
+ iconColor="danger"
+ iconType="alert"
/>
- {localState.expressionBuildError[0].longMessage}
);
}
@@ -474,34 +486,43 @@ export const InnerVisualizationWrapper = ({
const visibleErrorMessage = getOriginalRequestErrorMessage(error) || errorMessage;
return (
-
+
-
-
-
- {
+ setLocalState((prevState: WorkspaceState) => ({
+ ...prevState,
+ expandError: !prevState.expandError,
+ }));
+ }}
+ >
+ {i18n.translate('xpack.lens.editorFrame.expandRenderingErrorButton', {
+ defaultMessage: 'Show details of error',
+ })}
+
+ ) : null
+ }
+ body={
+ <>
+
+
+
+
+ {localState.expandError ? (
+ visibleErrorMessage
+ ) : null}
+ >
+ }
+ iconColor="danger"
+ iconType="alert"
/>
- {visibleErrorMessage ? (
-
- {
- setLocalState((prevState: WorkspaceState) => ({
- ...prevState,
- expandError: !prevState.expandError,
- }));
- }}
- >
- {i18n.translate('xpack.lens.editorFrame.expandRenderingErrorButton', {
- defaultMessage: 'Show details of error',
- })}
-
-
- {localState.expandError ? visibleErrorMessage : null}
-
- ) : null}
);
}}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss
index 3949c7deb53b4..167c17ee6ae9c 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss
@@ -11,6 +11,7 @@
min-height: $euiSizeXXL * 10;
overflow: visible;
border: none;
+ height: 100%;
.lnsWorkspacePanelWrapper__pageContentBody {
@include euiScrollBar;
diff --git a/x-pack/plugins/lists/common/constants.mock.ts b/x-pack/plugins/lists/common/constants.mock.ts
index 6f8782c148d45..27e0fa29b1e55 100644
--- a/x-pack/plugins/lists/common/constants.mock.ts
+++ b/x-pack/plugins/lists/common/constants.mock.ts
@@ -8,7 +8,7 @@
import moment from 'moment';
import { OsTypeArray } from './schemas/common';
-import { EntriesArray } from './schemas/types';
+import { EntriesArray, Entry, EntryMatch, EntryNested } from './schemas/types';
import { EndpointEntriesArray } from './schemas/types/endpoint';
export const DATE_NOW = '2020-04-20T15:25:31.830Z';
export const OLD_DATE_RELATIVE_TO_DATE_NOW = '2020-04-19T15:25:31.830Z';
@@ -72,6 +72,34 @@ export const ENDPOINT_ENTRIES: EndpointEntriesArray = [
},
{ field: 'some.not.nested.field', operator: 'included', type: 'match', value: 'some value' },
];
+// ENTRIES_WITH_IDS should only be used to mock out functionality of a collection of transforms
+// that are UI specific and useful for UI concerns that are inserted between the
+// API and the actual user interface. In some ways these might be viewed as
+// technical debt or to compensate for the differences and preferences
+// of how ReactJS might prefer data vs. how we want to model data.
+export const ENTRIES_WITH_IDS: EntriesArray = [
+ {
+ entries: [
+ {
+ field: 'nested.field',
+ id: '123',
+ operator: 'included',
+ type: 'match',
+ value: 'some value',
+ } as EntryMatch & { id: string },
+ ],
+ field: 'some.parentField',
+ id: '123',
+ type: 'nested',
+ } as EntryNested & { id: string },
+ {
+ field: 'some.not.nested.field',
+ id: '123',
+ operator: 'included',
+ type: 'match',
+ value: 'some value',
+ } as Entry & { id: string },
+];
export const ITEM_TYPE = 'simple';
export const OS_TYPES: OsTypeArray = ['windows'];
export const TAGS = [];
diff --git a/x-pack/plugins/lists/common/shared_imports.ts b/x-pack/plugins/lists/common/shared_imports.ts
index 7e96b13c036ec..2483c1f7dd992 100644
--- a/x-pack/plugins/lists/common/shared_imports.ts
+++ b/x-pack/plugins/lists/common/shared_imports.ts
@@ -12,6 +12,8 @@ export {
DefaultStringArray,
DefaultVersionNumber,
DefaultVersionNumberDecoded,
+ addIdToItem,
+ removeIdFromItem,
exactCheck,
getPaths,
foldLeftRight,
diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.ts
index ed16a405a5107..7a39bd5651014 100644
--- a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.ts
+++ b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.ts
@@ -7,6 +7,7 @@
import { act, renderHook } from '@testing-library/react-hooks';
+import { ENTRIES_WITH_IDS } from '../../../common/constants.mock';
import { coreMock } from '../../../../../../src/core/public/mocks';
import * as api from '../api';
import { getCreateExceptionListItemSchemaMock } from '../../../common/schemas/request/create_exception_list_item_schema.mock';
@@ -69,8 +70,8 @@ describe('usePersistExceptionItem', () => {
});
test('it invokes "updateExceptionListItem" when payload has "id"', async () => {
- const addExceptionItem = jest.spyOn(api, 'addExceptionListItem');
- const updateExceptionItem = jest.spyOn(api, 'updateExceptionListItem');
+ const addExceptionListItem = jest.spyOn(api, 'addExceptionListItem');
+ const updateExceptionListItem = jest.spyOn(api, 'updateExceptionListItem');
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
PersistHookProps,
@@ -78,12 +79,45 @@ describe('usePersistExceptionItem', () => {
>(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError }));
await waitForNextUpdate();
- result.current[1](getUpdateExceptionListItemSchemaMock());
+ // NOTE: Take note here passing in an exception item where it's
+ // entries have been enriched with ids to ensure that they get stripped
+ // before the call goes through
+ result.current[1]({ ...getUpdateExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS });
await waitForNextUpdate();
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
- expect(addExceptionItem).not.toHaveBeenCalled();
- expect(updateExceptionItem).toHaveBeenCalled();
+ expect(addExceptionListItem).not.toHaveBeenCalled();
+ expect(updateExceptionListItem).toHaveBeenCalledWith({
+ http: mockKibanaHttpService,
+ listItem: getUpdateExceptionListItemSchemaMock(),
+ signal: new AbortController().signal,
+ });
+ });
+ });
+
+ test('it invokes "addExceptionListItem" when payload does not have "id"', async () => {
+ const updateExceptionListItem = jest.spyOn(api, 'updateExceptionListItem');
+ const addExceptionListItem = jest.spyOn(api, 'addExceptionListItem');
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook<
+ PersistHookProps,
+ ReturnPersistExceptionItem
+ >(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError }));
+
+ await waitForNextUpdate();
+ // NOTE: Take note here passing in an exception item where it's
+ // entries have been enriched with ids to ensure that they get stripped
+ // before the call goes through
+ result.current[1]({ ...getCreateExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS });
+ await waitForNextUpdate();
+
+ expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
+ expect(updateExceptionListItem).not.toHaveBeenCalled();
+ expect(addExceptionListItem).toHaveBeenCalledWith({
+ http: mockKibanaHttpService,
+ listItem: getCreateExceptionListItemSchemaMock(),
+ signal: new AbortController().signal,
+ });
});
});
diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.ts b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.ts
index 995c6b8703bf4..6135d14aef6a4 100644
--- a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.ts
+++ b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.ts
@@ -7,9 +7,13 @@
import { Dispatch, useEffect, useState } from 'react';
-import { UpdateExceptionListItemSchema } from '../../../common/schemas';
+import {
+ CreateExceptionListItemSchema,
+ UpdateExceptionListItemSchema,
+} from '../../../common/schemas';
import { addExceptionListItem, updateExceptionListItem } from '../api';
-import { AddExceptionListItem, PersistHookProps } from '../types';
+import { transformNewItemOutput, transformOutput } from '../transforms';
+import { PersistHookProps } from '../types';
interface PersistReturnExceptionItem {
isLoading: boolean;
@@ -18,7 +22,7 @@ interface PersistReturnExceptionItem {
export type ReturnPersistExceptionItem = [
PersistReturnExceptionItem,
- Dispatch
+ Dispatch
];
/**
@@ -32,7 +36,9 @@ export const usePersistExceptionItem = ({
http,
onError,
}: PersistHookProps): ReturnPersistExceptionItem => {
- const [exceptionListItem, setExceptionItem] = useState(null);
+ const [exceptionListItem, setExceptionItem] = useState<
+ CreateExceptionListItemSchema | UpdateExceptionListItemSchema | null
+ >(null);
const [isSaved, setIsSaved] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const isUpdateExceptionItem = (item: unknown): item is UpdateExceptionListItemSchema =>
@@ -47,16 +53,25 @@ export const usePersistExceptionItem = ({
if (exceptionListItem != null) {
try {
setIsLoading(true);
+
if (isUpdateExceptionItem(exceptionListItem)) {
+ // Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes
+ // for context around the temporary `id`
+ const transformedList = transformOutput(exceptionListItem);
+
await updateExceptionListItem({
http,
- listItem: exceptionListItem,
+ listItem: transformedList,
signal: abortCtrl.signal,
});
} else {
+ // Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes
+ // for context around the temporary `id`
+ const transformedList = transformNewItemOutput(exceptionListItem);
+
await addExceptionListItem({
http,
- listItem: exceptionListItem,
+ listItem: transformedList,
signal: abortCtrl.signal,
});
}
diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts
index e61e74ca33236..62f959cb386a0 100644
--- a/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts
+++ b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts
@@ -7,6 +7,7 @@
import { act, renderHook } from '@testing-library/react-hooks';
+import { ENTRIES_WITH_IDS } from '../../../common/constants.mock';
import { getUpdateExceptionListItemSchemaMock } from '../../../common/schemas/request/update_exception_list_item_schema.mock';
import { coreMock } from '../../../../../../src/core/public/mocks';
import * as api from '../api';
@@ -24,6 +25,10 @@ import {
import { ExceptionsApi, useApi } from './use_api';
+jest.mock('uuid', () => ({
+ v4: jest.fn().mockReturnValue('123'),
+}));
+
const mockKibanaHttpService = coreMock.createStart().http;
describe('useApi', () => {
@@ -34,397 +39,428 @@ describe('useApi', () => {
jest.clearAllMocks();
});
- test('it invokes "deleteExceptionListItemById" when "deleteExceptionItem" used', async () => {
- const payload = getExceptionListItemSchemaMock();
- const onSuccessMock = jest.fn();
- const spyOnDeleteExceptionListItemById = jest
- .spyOn(api, 'deleteExceptionListItemById')
- .mockResolvedValue(payload);
-
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useApi(mockKibanaHttpService)
- );
- await waitForNextUpdate();
-
- const { id, namespace_type: namespaceType } = payload;
-
- await result.current.deleteExceptionItem({
- id,
- namespaceType,
- onError: jest.fn(),
- onSuccess: onSuccessMock,
+ describe('deleteExceptionItem', () => {
+ test('it invokes "deleteExceptionListItemById" when "deleteExceptionItem" used', async () => {
+ const payload = getExceptionListItemSchemaMock();
+ const onSuccessMock = jest.fn();
+ const spyOnDeleteExceptionListItemById = jest
+ .spyOn(api, 'deleteExceptionListItemById')
+ .mockResolvedValue(payload);
+
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useApi(mockKibanaHttpService)
+ );
+ await waitForNextUpdate();
+
+ const { id, namespace_type: namespaceType } = payload;
+
+ await result.current.deleteExceptionItem({
+ id,
+ namespaceType,
+ onError: jest.fn(),
+ onSuccess: onSuccessMock,
+ });
+
+ const expected: ApiCallByIdProps = {
+ http: mockKibanaHttpService,
+ id,
+ namespaceType,
+ signal: new AbortController().signal,
+ };
+
+ expect(spyOnDeleteExceptionListItemById).toHaveBeenCalledWith(expected);
+ expect(onSuccessMock).toHaveBeenCalled();
});
-
- const expected: ApiCallByIdProps = {
- http: mockKibanaHttpService,
- id,
- namespaceType,
- signal: new AbortController().signal,
- };
-
- expect(spyOnDeleteExceptionListItemById).toHaveBeenCalledWith(expected);
- expect(onSuccessMock).toHaveBeenCalled();
});
- });
- test('invokes "onError" callback if "deleteExceptionListItemById" fails', async () => {
- const mockError = new Error('failed to delete item');
- jest.spyOn(api, 'deleteExceptionListItemById').mockRejectedValue(mockError);
+ test('invokes "onError" callback if "deleteExceptionListItemById" fails', async () => {
+ const mockError = new Error('failed to delete item');
+ jest.spyOn(api, 'deleteExceptionListItemById').mockRejectedValue(mockError);
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useApi(mockKibanaHttpService)
- );
- await waitForNextUpdate();
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useApi(mockKibanaHttpService)
+ );
+ await waitForNextUpdate();
- const { id, namespace_type: namespaceType } = getExceptionListItemSchemaMock();
+ const { id, namespace_type: namespaceType } = getExceptionListItemSchemaMock();
- await result.current.deleteExceptionItem({
- id,
- namespaceType,
- onError: onErrorMock,
- onSuccess: jest.fn(),
- });
+ await result.current.deleteExceptionItem({
+ id,
+ namespaceType,
+ onError: onErrorMock,
+ onSuccess: jest.fn(),
+ });
- expect(onErrorMock).toHaveBeenCalledWith(mockError);
+ expect(onErrorMock).toHaveBeenCalledWith(mockError);
+ });
});
});
- test('it invokes "deleteExceptionListById" when "deleteExceptionList" used', async () => {
- const payload = getExceptionListSchemaMock();
- const onSuccessMock = jest.fn();
- const spyOnDeleteExceptionListById = jest
- .spyOn(api, 'deleteExceptionListById')
- .mockResolvedValue(payload);
-
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useApi(mockKibanaHttpService)
- );
- await waitForNextUpdate();
-
- const { id, namespace_type: namespaceType } = payload;
-
- await result.current.deleteExceptionList({
- id,
- namespaceType,
- onError: jest.fn(),
- onSuccess: onSuccessMock,
+ describe('deleteExceptionList', () => {
+ test('it invokes "deleteExceptionListById" when "deleteExceptionList" used', async () => {
+ const payload = getExceptionListSchemaMock();
+ const onSuccessMock = jest.fn();
+ const spyOnDeleteExceptionListById = jest
+ .spyOn(api, 'deleteExceptionListById')
+ .mockResolvedValue(payload);
+
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useApi(mockKibanaHttpService)
+ );
+ await waitForNextUpdate();
+
+ const { id, namespace_type: namespaceType } = payload;
+
+ await result.current.deleteExceptionList({
+ id,
+ namespaceType,
+ onError: jest.fn(),
+ onSuccess: onSuccessMock,
+ });
+
+ const expected: ApiCallByIdProps = {
+ http: mockKibanaHttpService,
+ id,
+ namespaceType,
+ signal: new AbortController().signal,
+ };
+
+ expect(spyOnDeleteExceptionListById).toHaveBeenCalledWith(expected);
+ expect(onSuccessMock).toHaveBeenCalled();
});
-
- const expected: ApiCallByIdProps = {
- http: mockKibanaHttpService,
- id,
- namespaceType,
- signal: new AbortController().signal,
- };
-
- expect(spyOnDeleteExceptionListById).toHaveBeenCalledWith(expected);
- expect(onSuccessMock).toHaveBeenCalled();
});
- });
- test('invokes "onError" callback if "deleteExceptionListById" fails', async () => {
- const mockError = new Error('failed to delete item');
- jest.spyOn(api, 'deleteExceptionListById').mockRejectedValue(mockError);
+ test('invokes "onError" callback if "deleteExceptionListById" fails', async () => {
+ const mockError = new Error('failed to delete item');
+ jest.spyOn(api, 'deleteExceptionListById').mockRejectedValue(mockError);
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useApi(mockKibanaHttpService)
- );
- await waitForNextUpdate();
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useApi(mockKibanaHttpService)
+ );
+ await waitForNextUpdate();
- const { id, namespace_type: namespaceType } = getExceptionListSchemaMock();
+ const { id, namespace_type: namespaceType } = getExceptionListSchemaMock();
- await result.current.deleteExceptionList({
- id,
- namespaceType,
- onError: onErrorMock,
- onSuccess: jest.fn(),
- });
+ await result.current.deleteExceptionList({
+ id,
+ namespaceType,
+ onError: onErrorMock,
+ onSuccess: jest.fn(),
+ });
- expect(onErrorMock).toHaveBeenCalledWith(mockError);
+ expect(onErrorMock).toHaveBeenCalledWith(mockError);
+ });
});
});
- test('it invokes "fetchExceptionListItemById" when "getExceptionItem" used', async () => {
- const payload = getExceptionListItemSchemaMock();
- const onSuccessMock = jest.fn();
- const spyOnFetchExceptionListItemById = jest
- .spyOn(api, 'fetchExceptionListItemById')
- .mockResolvedValue(payload);
-
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useApi(mockKibanaHttpService)
- );
- await waitForNextUpdate();
-
- const { id, namespace_type: namespaceType } = payload;
-
- await result.current.getExceptionItem({
- id,
- namespaceType,
- onError: jest.fn(),
- onSuccess: onSuccessMock,
+ describe('getExceptionItem', () => {
+ test('it invokes "fetchExceptionListItemById" when "getExceptionItem" used', async () => {
+ const payload = getExceptionListItemSchemaMock();
+ const onSuccessMock = jest.fn();
+ const spyOnFetchExceptionListItemById = jest
+ .spyOn(api, 'fetchExceptionListItemById')
+ .mockResolvedValue(payload);
+
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useApi(mockKibanaHttpService)
+ );
+ await waitForNextUpdate();
+
+ const { id, namespace_type: namespaceType } = payload;
+
+ await result.current.getExceptionItem({
+ id,
+ namespaceType,
+ onError: jest.fn(),
+ onSuccess: onSuccessMock,
+ });
+
+ const expected: ApiCallByIdProps = {
+ http: mockKibanaHttpService,
+ id,
+ namespaceType,
+ signal: new AbortController().signal,
+ };
+ const expectedExceptionListItem = {
+ ...getExceptionListItemSchemaMock(),
+ entries: ENTRIES_WITH_IDS,
+ };
+
+ expect(spyOnFetchExceptionListItemById).toHaveBeenCalledWith(expected);
+ expect(onSuccessMock).toHaveBeenCalledWith(expectedExceptionListItem);
});
-
- const expected: ApiCallByIdProps = {
- http: mockKibanaHttpService,
- id,
- namespaceType,
- signal: new AbortController().signal,
- };
-
- expect(spyOnFetchExceptionListItemById).toHaveBeenCalledWith(expected);
- expect(onSuccessMock).toHaveBeenCalled();
});
- });
- test('invokes "onError" callback if "fetchExceptionListItemById" fails', async () => {
- const mockError = new Error('failed to delete item');
- jest.spyOn(api, 'fetchExceptionListItemById').mockRejectedValue(mockError);
+ test('invokes "onError" callback if "fetchExceptionListItemById" fails', async () => {
+ const mockError = new Error('failed to delete item');
+ jest.spyOn(api, 'fetchExceptionListItemById').mockRejectedValue(mockError);
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useApi(mockKibanaHttpService)
- );
- await waitForNextUpdate();
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useApi(mockKibanaHttpService)
+ );
+ await waitForNextUpdate();
- const { id, namespace_type: namespaceType } = getExceptionListSchemaMock();
+ const { id, namespace_type: namespaceType } = getExceptionListSchemaMock();
- await result.current.getExceptionItem({
- id,
- namespaceType,
- onError: onErrorMock,
- onSuccess: jest.fn(),
- });
+ await result.current.getExceptionItem({
+ id,
+ namespaceType,
+ onError: onErrorMock,
+ onSuccess: jest.fn(),
+ });
- expect(onErrorMock).toHaveBeenCalledWith(mockError);
+ expect(onErrorMock).toHaveBeenCalledWith(mockError);
+ });
});
});
- test('it invokes "fetchExceptionListById" when "getExceptionList" used', async () => {
- const payload = getExceptionListSchemaMock();
- const onSuccessMock = jest.fn();
- const spyOnFetchExceptionListById = jest
- .spyOn(api, 'fetchExceptionListById')
- .mockResolvedValue(payload);
-
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useApi(mockKibanaHttpService)
- );
- await waitForNextUpdate();
-
- const { id, namespace_type: namespaceType } = payload;
-
- await result.current.getExceptionList({
- id,
- namespaceType,
- onError: jest.fn(),
- onSuccess: onSuccessMock,
+ describe('getExceptionList', () => {
+ test('it invokes "fetchExceptionListById" when "getExceptionList" used', async () => {
+ const payload = getExceptionListSchemaMock();
+ const onSuccessMock = jest.fn();
+ const spyOnFetchExceptionListById = jest
+ .spyOn(api, 'fetchExceptionListById')
+ .mockResolvedValue(payload);
+
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useApi(mockKibanaHttpService)
+ );
+ await waitForNextUpdate();
+
+ const { id, namespace_type: namespaceType } = payload;
+
+ await result.current.getExceptionList({
+ id,
+ namespaceType,
+ onError: jest.fn(),
+ onSuccess: onSuccessMock,
+ });
+
+ const expected: ApiCallByIdProps = {
+ http: mockKibanaHttpService,
+ id,
+ namespaceType,
+ signal: new AbortController().signal,
+ };
+
+ expect(spyOnFetchExceptionListById).toHaveBeenCalledWith(expected);
+ expect(onSuccessMock).toHaveBeenCalled();
});
-
- const expected: ApiCallByIdProps = {
- http: mockKibanaHttpService,
- id,
- namespaceType,
- signal: new AbortController().signal,
- };
-
- expect(spyOnFetchExceptionListById).toHaveBeenCalledWith(expected);
- expect(onSuccessMock).toHaveBeenCalled();
});
- });
-
- test('invokes "onError" callback if "fetchExceptionListById" fails', async () => {
- const mockError = new Error('failed to delete item');
- jest.spyOn(api, 'fetchExceptionListById').mockRejectedValue(mockError);
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useApi(mockKibanaHttpService)
- );
- await waitForNextUpdate();
+ test('invokes "onError" callback if "fetchExceptionListById" fails', async () => {
+ const mockError = new Error('failed to delete item');
+ jest.spyOn(api, 'fetchExceptionListById').mockRejectedValue(mockError);
- const { id, namespace_type: namespaceType } = getExceptionListSchemaMock();
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useApi(mockKibanaHttpService)
+ );
+ await waitForNextUpdate();
- await result.current.getExceptionList({
- id,
- namespaceType,
- onError: onErrorMock,
- onSuccess: jest.fn(),
- });
+ const { id, namespace_type: namespaceType } = getExceptionListSchemaMock();
- expect(onErrorMock).toHaveBeenCalledWith(mockError);
- });
- });
+ await result.current.getExceptionList({
+ id,
+ namespaceType,
+ onError: onErrorMock,
+ onSuccess: jest.fn(),
+ });
- test('it invokes "fetchExceptionListsItemsByListIds" when "getExceptionItem" used', async () => {
- const output = getFoundExceptionListItemSchemaMock();
- const onSuccessMock = jest.fn();
- const spyOnFetchExceptionListsItemsByListIds = jest
- .spyOn(api, 'fetchExceptionListsItemsByListIds')
- .mockResolvedValue(output);
-
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useApi(mockKibanaHttpService)
- );
- await waitForNextUpdate();
-
- await result.current.getExceptionListsItems({
- filterOptions: [],
- lists: [{ id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }],
- onError: jest.fn(),
- onSuccess: onSuccessMock,
- pagination: {
- page: 1,
- perPage: 20,
- total: 0,
- },
- showDetectionsListsOnly: false,
- showEndpointListsOnly: false,
+ expect(onErrorMock).toHaveBeenCalledWith(mockError);
});
-
- const expected: ApiCallByListIdProps = {
- filterOptions: [],
- http: mockKibanaHttpService,
- listIds: ['list_id'],
- namespaceTypes: ['single'],
- pagination: {
- page: 1,
- perPage: 20,
- total: 0,
- },
- signal: new AbortController().signal,
- };
-
- expect(spyOnFetchExceptionListsItemsByListIds).toHaveBeenCalledWith(expected);
- expect(onSuccessMock).toHaveBeenCalled();
});
});
- test('it does not invoke "fetchExceptionListsItemsByListIds" if no listIds', async () => {
- const output = getFoundExceptionListItemSchemaMock();
- const onSuccessMock = jest.fn();
- const spyOnFetchExceptionListsItemsByListIds = jest
- .spyOn(api, 'fetchExceptionListsItemsByListIds')
- .mockResolvedValue(output);
-
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useApi(mockKibanaHttpService)
- );
- await waitForNextUpdate();
-
- await result.current.getExceptionListsItems({
- filterOptions: [],
- lists: [{ id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }],
- onError: jest.fn(),
- onSuccess: onSuccessMock,
- pagination: {
- page: 1,
- perPage: 20,
- total: 0,
- },
- showDetectionsListsOnly: false,
- showEndpointListsOnly: true,
+ describe('getExceptionListsItems', () => {
+ test('it invokes "fetchExceptionListsItemsByListIds" when "getExceptionListsItems" used', async () => {
+ const output = getFoundExceptionListItemSchemaMock();
+ const onSuccessMock = jest.fn();
+ const spyOnFetchExceptionListsItemsByListIds = jest
+ .spyOn(api, 'fetchExceptionListsItemsByListIds')
+ .mockResolvedValue(output);
+
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useApi(mockKibanaHttpService)
+ );
+ await waitForNextUpdate();
+
+ await result.current.getExceptionListsItems({
+ filterOptions: [],
+ lists: [
+ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' },
+ ],
+ onError: jest.fn(),
+ onSuccess: onSuccessMock,
+ pagination: {
+ page: 1,
+ perPage: 1,
+ total: 0,
+ },
+ showDetectionsListsOnly: false,
+ showEndpointListsOnly: false,
+ });
+
+ const expected: ApiCallByListIdProps = {
+ filterOptions: [],
+ http: mockKibanaHttpService,
+ listIds: ['list_id'],
+ namespaceTypes: ['single'],
+ pagination: {
+ page: 1,
+ perPage: 1,
+ total: 0,
+ },
+ signal: new AbortController().signal,
+ };
+
+ expect(spyOnFetchExceptionListsItemsByListIds).toHaveBeenCalledWith(expected);
+ expect(onSuccessMock).toHaveBeenCalledWith({
+ exceptions: [{ ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS }],
+ pagination: {
+ page: 1,
+ perPage: 1,
+ total: 1,
+ },
+ });
});
+ });
- expect(spyOnFetchExceptionListsItemsByListIds).not.toHaveBeenCalled();
- expect(onSuccessMock).toHaveBeenCalledWith({
- exceptions: [],
- pagination: {
- page: 0,
- perPage: 20,
- total: 0,
- },
+ test('it does not invoke "fetchExceptionListsItemsByListIds" if no listIds', async () => {
+ const output = getFoundExceptionListItemSchemaMock();
+ const onSuccessMock = jest.fn();
+ const spyOnFetchExceptionListsItemsByListIds = jest
+ .spyOn(api, 'fetchExceptionListsItemsByListIds')
+ .mockResolvedValue(output);
+
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useApi(mockKibanaHttpService)
+ );
+ await waitForNextUpdate();
+
+ await result.current.getExceptionListsItems({
+ filterOptions: [],
+ lists: [
+ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' },
+ ],
+ onError: jest.fn(),
+ onSuccess: onSuccessMock,
+ pagination: {
+ page: 1,
+ perPage: 20,
+ total: 0,
+ },
+ showDetectionsListsOnly: false,
+ showEndpointListsOnly: true,
+ });
+
+ expect(spyOnFetchExceptionListsItemsByListIds).not.toHaveBeenCalled();
+ expect(onSuccessMock).toHaveBeenCalledWith({
+ exceptions: [],
+ pagination: {
+ page: 0,
+ perPage: 20,
+ total: 0,
+ },
+ });
});
});
- });
- test('invokes "onError" callback if "fetchExceptionListsItemsByListIds" fails', async () => {
- const mockError = new Error('failed to delete item');
- jest.spyOn(api, 'fetchExceptionListsItemsByListIds').mockRejectedValue(mockError);
-
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useApi(mockKibanaHttpService)
- );
- await waitForNextUpdate();
-
- await result.current.getExceptionListsItems({
- filterOptions: [],
- lists: [{ id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }],
- onError: onErrorMock,
- onSuccess: jest.fn(),
- pagination: {
- page: 1,
- perPage: 20,
- total: 0,
- },
- showDetectionsListsOnly: false,
- showEndpointListsOnly: false,
+ test('invokes "onError" callback if "fetchExceptionListsItemsByListIds" fails', async () => {
+ const mockError = new Error('failed to delete item');
+ jest.spyOn(api, 'fetchExceptionListsItemsByListIds').mockRejectedValue(mockError);
+
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useApi(mockKibanaHttpService)
+ );
+ await waitForNextUpdate();
+
+ await result.current.getExceptionListsItems({
+ filterOptions: [],
+ lists: [
+ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' },
+ ],
+ onError: onErrorMock,
+ onSuccess: jest.fn(),
+ pagination: {
+ page: 1,
+ perPage: 20,
+ total: 0,
+ },
+ showDetectionsListsOnly: false,
+ showEndpointListsOnly: false,
+ });
+
+ expect(onErrorMock).toHaveBeenCalledWith(mockError);
});
-
- expect(onErrorMock).toHaveBeenCalledWith(mockError);
});
});
- test('it invokes "addExceptionListItem" when "addExceptionListItem" used', async () => {
- const payload = getExceptionListItemSchemaMock();
- const itemToCreate = getCreateExceptionListItemSchemaMock();
- const spyOnFetchExceptionListItemById = jest
- .spyOn(api, 'addExceptionListItem')
- .mockResolvedValue(payload);
-
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useApi(mockKibanaHttpService)
- );
- await waitForNextUpdate();
-
- await result.current.addExceptionListItem({
- listItem: itemToCreate,
+ describe('addExceptionListItem', () => {
+ test('it removes exception item entry ids', async () => {
+ const payload = getExceptionListItemSchemaMock();
+ const itemToCreate = { ...getCreateExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS };
+ const spyOnFetchExceptionListItemById = jest
+ .spyOn(api, 'addExceptionListItem')
+ .mockResolvedValue(payload);
+
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useApi(mockKibanaHttpService)
+ );
+ await waitForNextUpdate();
+
+ await result.current.addExceptionListItem({
+ listItem: itemToCreate,
+ });
+
+ const expected: AddExceptionListItemProps = {
+ http: mockKibanaHttpService,
+ listItem: getCreateExceptionListItemSchemaMock(),
+ signal: new AbortController().signal,
+ };
+
+ expect(spyOnFetchExceptionListItemById).toHaveBeenCalledWith(expected);
});
-
- const expected: AddExceptionListItemProps = {
- http: mockKibanaHttpService,
- listItem: itemToCreate,
- signal: new AbortController().signal,
- };
-
- expect(spyOnFetchExceptionListItemById).toHaveBeenCalledWith(expected);
});
});
- test('it invokes "updateExceptionListItem" when "getExceptionItem" used', async () => {
- const payload = getExceptionListItemSchemaMock();
- const itemToUpdate = getUpdateExceptionListItemSchemaMock();
- const spyOnUpdateExceptionListItem = jest
- .spyOn(api, 'updateExceptionListItem')
- .mockResolvedValue(payload);
-
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useApi(mockKibanaHttpService)
- );
- await waitForNextUpdate();
-
- await result.current.updateExceptionListItem({
- listItem: itemToUpdate,
+ describe('updateExceptionListItem', () => {
+ test('it removes exception item entry ids', async () => {
+ const payload = getExceptionListItemSchemaMock();
+ const itemToUpdate = { ...getUpdateExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS };
+ const spyOnUpdateExceptionListItem = jest
+ .spyOn(api, 'updateExceptionListItem')
+ .mockResolvedValue(payload);
+
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useApi(mockKibanaHttpService)
+ );
+ await waitForNextUpdate();
+
+ await result.current.updateExceptionListItem({
+ listItem: itemToUpdate,
+ });
+
+ const expected: UpdateExceptionListItemProps = {
+ http: mockKibanaHttpService,
+ listItem: getUpdateExceptionListItemSchemaMock(),
+ signal: new AbortController().signal,
+ };
+
+ expect(spyOnUpdateExceptionListItem).toHaveBeenCalledWith(expected);
});
-
- const expected: UpdateExceptionListItemProps = {
- http: mockKibanaHttpService,
- listItem: itemToUpdate,
- signal: new AbortController().signal,
- };
-
- expect(spyOnUpdateExceptionListItem).toHaveBeenCalledWith(expected);
});
});
});
diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts
index b0c831ef3b857..9e4e338b09dbf 100644
--- a/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts
+++ b/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts
@@ -17,6 +17,7 @@ import {
} from '../../../common/schemas';
import { ApiCallFindListsItemsMemoProps, ApiCallMemoProps, ApiListExportProps } from '../types';
import { getIdsAndNamespaces } from '../utils';
+import { transformInput, transformNewItemOutput, transformOutput } from '../transforms';
export interface ExceptionsApi {
addExceptionListItem: (arg: {
@@ -46,10 +47,11 @@ export const useApi = (http: HttpStart): ExceptionsApi => {
listItem: CreateExceptionListItemSchema;
}): Promise {
const abortCtrl = new AbortController();
+ const sanitizedItem: CreateExceptionListItemSchema = transformNewItemOutput(listItem);
return Api.addExceptionListItem({
http,
- listItem,
+ listItem: sanitizedItem,
signal: abortCtrl.signal,
});
},
@@ -124,12 +126,14 @@ export const useApi = (http: HttpStart): ExceptionsApi => {
const abortCtrl = new AbortController();
try {
- const item = await Api.fetchExceptionListItemById({
- http,
- id,
- namespaceType,
- signal: abortCtrl.signal,
- });
+ const item = transformInput(
+ await Api.fetchExceptionListItemById({
+ http,
+ id,
+ namespaceType,
+ signal: abortCtrl.signal,
+ })
+ );
onSuccess(item);
} catch (error) {
onError(error);
@@ -187,7 +191,10 @@ export const useApi = (http: HttpStart): ExceptionsApi => {
signal: abortCtrl.signal,
});
onSuccess({
- exceptions: data,
+ // This data transform is UI specific and useful for UI concerns
+ // to compensate for the differences and preferences of how ReactJS might prefer
+ // data vs. how we want to model data. View `transformInput` for more details
+ exceptions: data.map((item) => transformInput(item)),
pagination: {
page,
perPage,
@@ -214,10 +221,11 @@ export const useApi = (http: HttpStart): ExceptionsApi => {
listItem: UpdateExceptionListItemSchema;
}): Promise {
const abortCtrl = new AbortController();
+ const sanitizedItem: UpdateExceptionListItemSchema = transformOutput(listItem);
return Api.updateExceptionListItem({
http,
- listItem,
+ listItem: sanitizedItem,
signal: abortCtrl.signal,
});
},
diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.test.ts
index d5d2638781879..1191b240d27bb 100644
--- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.test.ts
+++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.test.ts
@@ -12,9 +12,14 @@ import * as api from '../api';
import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock';
import { ExceptionListItemSchema } from '../../../common/schemas';
import { UseExceptionListItemsSuccess, UseExceptionListProps } from '../types';
+import { transformInput } from '../transforms';
import { ReturnExceptionListAndItems, useExceptionListItems } from './use_exception_list_items';
+jest.mock('uuid', () => ({
+ v4: jest.fn().mockReturnValue('123'),
+}));
+
const mockKibanaHttpService = coreMock.createStart().http;
describe('useExceptionListItems', () => {
@@ -99,8 +104,9 @@ describe('useExceptionListItems', () => {
await waitForNextUpdate();
await waitForNextUpdate();
- const expectedListItemsResult: ExceptionListItemSchema[] = getFoundExceptionListItemSchemaMock()
- .data;
+ const expectedListItemsResult: ExceptionListItemSchema[] = getFoundExceptionListItemSchemaMock().data.map(
+ (item) => transformInput(item)
+ );
const expectedResult: UseExceptionListItemsSuccess = {
exceptions: expectedListItemsResult,
pagination: { page: 1, perPage: 1, total: 1 },
diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.ts
index 50271530b42e7..b9a8628d2ceac 100644
--- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.ts
+++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.ts
@@ -11,6 +11,7 @@ import { fetchExceptionListsItemsByListIds } from '../api';
import { FilterExceptionsOptions, Pagination, UseExceptionListProps } from '../types';
import { ExceptionListItemSchema } from '../../../common/schemas';
import { getIdsAndNamespaces } from '../utils';
+import { transformInput } from '../transforms';
type Func = () => void;
export type ReturnExceptionListAndItems = [
@@ -95,8 +96,12 @@ export const useExceptionListItems = ({
}
setLoading(false);
} else {
- // eslint-disable-next-line @typescript-eslint/naming-convention
- const { page, per_page, total, data } = await fetchExceptionListsItemsByListIds({
+ const {
+ page,
+ per_page: perPage,
+ total,
+ data,
+ } = await fetchExceptionListsItemsByListIds({
filterOptions: filters,
http,
listIds: ids,
@@ -108,20 +113,24 @@ export const useExceptionListItems = ({
signal: abortCtrl.signal,
});
+ // Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes
+ // for context around the temporary `id`
+ const transformedData = data.map((item) => transformInput(item));
+
if (isSubscribed) {
setPagination({
page,
- perPage: per_page,
+ perPage,
total,
});
- setExceptionListItems(data);
+ setExceptionListItems(transformedData);
if (onSuccess != null) {
onSuccess({
- exceptions: data,
+ exceptions: transformedData,
pagination: {
page,
- perPage: per_page,
+ perPage,
total,
},
});
diff --git a/x-pack/plugins/lists/public/exceptions/transforms.test.ts b/x-pack/plugins/lists/public/exceptions/transforms.test.ts
new file mode 100644
index 0000000000000..12b0f0bd8624a
--- /dev/null
+++ b/x-pack/plugins/lists/public/exceptions/transforms.test.ts
@@ -0,0 +1,282 @@
+/*
+ * 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 { ExceptionListItemSchema } from '../../common/schemas/response/exception_list_item_schema';
+import { UpdateExceptionListItemSchema } from '../../common/schemas/request/update_exception_list_item_schema';
+import { CreateExceptionListItemSchema } from '../../common/schemas/request/create_exception_list_item_schema';
+import { getCreateExceptionListItemSchemaMock } from '../../common/schemas/request/create_exception_list_item_schema.mock';
+import { getUpdateExceptionListItemSchemaMock } from '../../common/schemas/request/update_exception_list_item_schema.mock';
+import { getExceptionListItemSchemaMock } from '../../common/schemas/response/exception_list_item_schema.mock';
+import { ENTRIES_WITH_IDS } from '../../common/constants.mock';
+import { Entry, EntryMatch, EntryNested } from '../../common/schemas';
+
+import {
+ addIdToExceptionItemEntries,
+ removeIdFromExceptionItemsEntries,
+ transformInput,
+ transformOutput,
+} from './transforms';
+
+jest.mock('uuid', () => ({
+ v4: jest.fn().mockReturnValue('123'),
+}));
+
+describe('Exceptions transforms', () => {
+ describe('transformOutput', () => {
+ it('returns same output as input with stripped ids per entry - CreateExceptionListItemSchema', () => {
+ const mockCreateExceptionItem = {
+ ...getCreateExceptionListItemSchemaMock(),
+ entries: ENTRIES_WITH_IDS,
+ };
+ const output = transformOutput(mockCreateExceptionItem);
+ const expectedOutput: CreateExceptionListItemSchema = getCreateExceptionListItemSchemaMock();
+
+ expect(output).toEqual(expectedOutput);
+ });
+
+ it('returns same output as input with stripped ids per entry - UpdateExceptionListItemSchema', () => {
+ const mockUpdateExceptionItem = {
+ ...getUpdateExceptionListItemSchemaMock(),
+ entries: ENTRIES_WITH_IDS,
+ };
+ const output = transformOutput(mockUpdateExceptionItem);
+ const expectedOutput: UpdateExceptionListItemSchema = getUpdateExceptionListItemSchemaMock();
+
+ expect(output).toEqual(expectedOutput);
+ });
+ });
+
+ describe('transformInput', () => {
+ it('returns same output as input with added ids per entry', () => {
+ const mockExceptionItem = getExceptionListItemSchemaMock();
+ const output = transformInput(mockExceptionItem);
+ const expectedOutput: ExceptionListItemSchema = {
+ ...getExceptionListItemSchemaMock(),
+ entries: ENTRIES_WITH_IDS,
+ };
+
+ expect(output).toEqual(expectedOutput);
+ });
+ });
+
+ describe('addIdToExceptionItemEntries', () => {
+ it('returns same output as input with added ids per entry', () => {
+ const mockExceptionItem: ExceptionListItemSchema = {
+ ...getExceptionListItemSchemaMock(),
+ entries: [
+ {
+ field: 'some.not.nested.field',
+ operator: 'included',
+ type: 'match',
+ value: 'some value',
+ },
+ ],
+ };
+ const output = addIdToExceptionItemEntries(mockExceptionItem);
+ const expectedOutput: ExceptionListItemSchema = {
+ ...getExceptionListItemSchemaMock(),
+ entries: [
+ {
+ field: 'some.not.nested.field',
+ id: '123',
+ operator: 'included',
+ type: 'match',
+ value: 'some value',
+ } as Entry & { id: string },
+ ],
+ };
+
+ expect(output).toEqual(expectedOutput);
+ });
+
+ it('returns same output as input with added ids per nested entry', () => {
+ const mockExceptionItem: ExceptionListItemSchema = {
+ ...getExceptionListItemSchemaMock(),
+ entries: [
+ {
+ entries: [
+ {
+ field: 'nested.field',
+ operator: 'included',
+ type: 'match',
+ value: 'some value',
+ },
+ ],
+ field: 'some.parentField',
+ type: 'nested',
+ },
+ ],
+ };
+ const output = addIdToExceptionItemEntries(mockExceptionItem);
+ const expectedOutput: ExceptionListItemSchema = {
+ ...getExceptionListItemSchemaMock(),
+ entries: [
+ {
+ entries: [
+ {
+ field: 'nested.field',
+ id: '123',
+ operator: 'included',
+ type: 'match',
+ value: 'some value',
+ } as EntryMatch & { id: string },
+ ],
+ field: 'some.parentField',
+ id: '123',
+ type: 'nested',
+ } as EntryNested & { id: string },
+ ],
+ };
+
+ expect(output).toEqual(expectedOutput);
+ });
+ });
+
+ describe('removeIdFromExceptionItemsEntries', () => {
+ it('returns same output as input with stripped ids per entry - CreateExceptionListItemSchema', () => {
+ const mockCreateExceptionItem = {
+ ...getCreateExceptionListItemSchemaMock(),
+ entries: [
+ {
+ field: 'some.not.nested.field',
+ id: '123',
+ operator: 'included',
+ type: 'match',
+ value: 'some value',
+ } as Entry & { id: string },
+ ],
+ };
+ const output = removeIdFromExceptionItemsEntries(mockCreateExceptionItem);
+ const expectedOutput: CreateExceptionListItemSchema = {
+ ...getCreateExceptionListItemSchemaMock(),
+ entries: [
+ {
+ field: 'some.not.nested.field',
+ operator: 'included',
+ type: 'match',
+ value: 'some value',
+ },
+ ],
+ };
+
+ expect(output).toEqual(expectedOutput);
+ });
+
+ it('returns same output as input with stripped ids per nested entry - CreateExceptionListItemSchema', () => {
+ const mockCreateExceptionItem = {
+ ...getCreateExceptionListItemSchemaMock(),
+ entries: [
+ {
+ entries: [
+ {
+ field: 'nested.field',
+ id: '123',
+ operator: 'included',
+ type: 'match',
+ value: 'some value',
+ } as EntryMatch & { id: string },
+ ],
+ field: 'some.parentField',
+ id: '123',
+ type: 'nested',
+ } as EntryNested & { id: string },
+ ],
+ };
+ const output = removeIdFromExceptionItemsEntries(mockCreateExceptionItem);
+ const expectedOutput: CreateExceptionListItemSchema = {
+ ...getCreateExceptionListItemSchemaMock(),
+ entries: [
+ {
+ entries: [
+ {
+ field: 'nested.field',
+ operator: 'included',
+ type: 'match',
+ value: 'some value',
+ },
+ ],
+ field: 'some.parentField',
+ type: 'nested',
+ },
+ ],
+ };
+
+ expect(output).toEqual(expectedOutput);
+ });
+
+ it('returns same output as input with stripped ids per entry - UpdateExceptionListItemSchema', () => {
+ const mockUpdateExceptionItem = {
+ ...getUpdateExceptionListItemSchemaMock(),
+ entries: [
+ {
+ field: 'some.not.nested.field',
+ id: '123',
+ operator: 'included',
+ type: 'match',
+ value: 'some value',
+ } as Entry & { id: string },
+ ],
+ };
+ const output = removeIdFromExceptionItemsEntries(mockUpdateExceptionItem);
+ const expectedOutput: UpdateExceptionListItemSchema = {
+ ...getUpdateExceptionListItemSchemaMock(),
+ entries: [
+ {
+ field: 'some.not.nested.field',
+ operator: 'included',
+ type: 'match',
+ value: 'some value',
+ },
+ ],
+ };
+
+ expect(output).toEqual(expectedOutput);
+ });
+
+ it('returns same output as input with stripped ids per nested entry - UpdateExceptionListItemSchema', () => {
+ const mockUpdateExceptionItem = {
+ ...getUpdateExceptionListItemSchemaMock(),
+ entries: [
+ {
+ entries: [
+ {
+ field: 'nested.field',
+ id: '123',
+ operator: 'included',
+ type: 'match',
+ value: 'some value',
+ } as EntryMatch & { id: string },
+ ],
+ field: 'some.parentField',
+ id: '123',
+ type: 'nested',
+ } as EntryNested & { id: string },
+ ],
+ };
+ const output = removeIdFromExceptionItemsEntries(mockUpdateExceptionItem);
+ const expectedOutput: UpdateExceptionListItemSchema = {
+ ...getUpdateExceptionListItemSchemaMock(),
+ entries: [
+ {
+ entries: [
+ {
+ field: 'nested.field',
+ operator: 'included',
+ type: 'match',
+ value: 'some value',
+ },
+ ],
+ field: 'some.parentField',
+ type: 'nested',
+ },
+ ],
+ };
+
+ expect(output).toEqual(expectedOutput);
+ });
+ });
+});
diff --git a/x-pack/plugins/lists/public/exceptions/transforms.ts b/x-pack/plugins/lists/public/exceptions/transforms.ts
new file mode 100644
index 0000000000000..0791760611bf5
--- /dev/null
+++ b/x-pack/plugins/lists/public/exceptions/transforms.ts
@@ -0,0 +1,114 @@
+/*
+ * 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 { flow } from 'fp-ts/lib/function';
+
+import {
+ CreateExceptionListItemSchema,
+ EntriesArray,
+ Entry,
+ ExceptionListItemSchema,
+ UpdateExceptionListItemSchema,
+} from '../../common';
+import { addIdToItem, removeIdFromItem } from '../../common/shared_imports';
+
+// These are a collection of transforms that are UI specific and useful for UI concerns
+// that are inserted between the API and the actual user interface. In some ways these
+// might be viewed as technical debt or to compensate for the differences and preferences
+// of how ReactJS might prefer data vs. how we want to model data. Each function should have
+// a description giving context around the transform.
+
+/**
+ * Transforms the output of exception items to compensate for technical debt or UI concerns such as
+ * ReactJS preferences for having ids within arrays if the data is not modeled that way.
+ *
+ * If you add a new transform of the output called "myNewTransform" do it
+ * in the form of:
+ * flow(removeIdFromExceptionItemsEntries, myNewTransform)(exceptionItem)
+ *
+ * @param exceptionItem The exceptionItem to transform the output of
+ * @returns The exceptionItem transformed from the output
+ */
+export const transformOutput = (
+ exceptionItem: UpdateExceptionListItemSchema | ExceptionListItemSchema
+): UpdateExceptionListItemSchema | ExceptionListItemSchema =>
+ flow(removeIdFromExceptionItemsEntries)(exceptionItem);
+
+export const transformNewItemOutput = (
+ exceptionItem: CreateExceptionListItemSchema
+): CreateExceptionListItemSchema => flow(removeIdFromExceptionItemsEntries)(exceptionItem);
+
+/**
+ * Transforms the output of rules to compensate for technical debt or UI concerns such as
+ * ReactJS preferences for having ids within arrays if the data is not modeled that way.
+ *
+ * If you add a new transform of the input called "myNewTransform" do it
+ * in the form of:
+ * flow(addIdToExceptionItemEntries, myNewTransform)(exceptionItem)
+ *
+ * @param exceptionItem The exceptionItem to transform the output of
+ * @returns The exceptionItem transformed from the output
+ */
+export const transformInput = (exceptionItem: ExceptionListItemSchema): ExceptionListItemSchema =>
+ flow(addIdToExceptionItemEntries)(exceptionItem);
+
+/**
+ * This adds an id to the incoming exception item entries as ReactJS prefers to have
+ * an id added to them for use as a stable id. Later if we decide to change the data
+ * model to have id's within the array then this code should be removed. If not, then
+ * this code should stay as an adapter for ReactJS.
+ *
+ * This does break the type system slightly as we are lying a bit to the type system as we return
+ * the same exceptionItem as we have previously but are augmenting the arrays with an id which TypeScript
+ * doesn't mind us doing here. However, downstream you will notice that you have an id when the type
+ * does not indicate it. In that case use (ExceptionItem & { id: string }) temporarily if you're using the id. If you're not,
+ * you can ignore the id and just use the normal TypeScript with ReactJS.
+ *
+ * @param exceptionItem The exceptionItem to add an id to the threat matches.
+ * @returns exceptionItem The exceptionItem but with id added to the exception item entries
+ */
+export const addIdToExceptionItemEntries = (
+ exceptionItem: ExceptionListItemSchema
+): ExceptionListItemSchema => {
+ const entries = exceptionItem.entries.map((entry) => {
+ if (entry.type === 'nested') {
+ return addIdToItem({
+ ...entry,
+ entries: entry.entries.map((nestedEntry) => addIdToItem(nestedEntry)),
+ });
+ } else {
+ return addIdToItem(entry);
+ }
+ });
+ return { ...exceptionItem, entries };
+};
+
+/**
+ * This removes an id from the exceptionItem entries as ReactJS prefers to have
+ * an id added to them for use as a stable id. Later if we decide to change the data
+ * model to have id's within the array then this code should be removed. If not, then
+ * this code should stay as an adapter for ReactJS.
+ *
+ * @param exceptionItem The exceptionItem to remove an id from the entries.
+ * @returns exceptionItem The exceptionItem but with id removed from the entries
+ */
+export const removeIdFromExceptionItemsEntries = (
+ exceptionItem: T
+): T => {
+ const { entries } = exceptionItem;
+ const entriesNoId = entries.map((entry) => {
+ if (entry.type === 'nested') {
+ return removeIdFromItem({
+ ...entry,
+ entries: entry.entries.map((nestedEntry) => removeIdFromItem(nestedEntry)),
+ });
+ } else {
+ return removeIdFromItem(entry);
+ }
+ });
+ return { ...exceptionItem, entries: entriesNoId };
+};
diff --git a/x-pack/plugins/lists/public/exceptions/types.ts b/x-pack/plugins/lists/public/exceptions/types.ts
index e37c03978c9f6..03cae387711f8 100644
--- a/x-pack/plugins/lists/public/exceptions/types.ts
+++ b/x-pack/plugins/lists/public/exceptions/types.ts
@@ -33,8 +33,6 @@ export interface Pagination {
export type AddExceptionList = UpdateExceptionListSchema | CreateExceptionListSchema;
-export type AddExceptionListItem = CreateExceptionListItemSchema | UpdateExceptionListItemSchema;
-
export interface PersistHookProps {
http: HttpStart;
onError: (arg: Error) => void;
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_time_range_selector.scss b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_time_range_selector.scss
index d946a0cf94032..65a72a1d4a48d 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_time_range_selector.scss
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_time_range_selector.scss
@@ -46,6 +46,7 @@
}
.body {
display: block;
+ height: 315px;
}
}
& > li.has-body.active {
diff --git a/x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.test.ts b/x-pack/plugins/security_solution/common/add_remove_id_to_item.test.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.test.ts
rename to x-pack/plugins/security_solution/common/add_remove_id_to_item.test.ts
diff --git a/x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.ts b/x-pack/plugins/security_solution/common/add_remove_id_to_item.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/common/utils/add_remove_id_to_item.ts
rename to x-pack/plugins/security_solution/common/add_remove_id_to_item.ts
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index 31b4cef1a9d45..c277ce369dca0 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -45,10 +45,10 @@ export const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000; // ms
export const DEFAULT_RULE_REFRESH_IDLE_VALUE = 2700000; // ms
export const DEFAULT_RULE_NOTIFICATION_QUERY_SIZE = 100;
-// Document path where threat indicator fields are expected. Used as
-// both the source of enrichment fields and the destination for enrichment in
-// the generated detection alert
-export const DEFAULT_INDICATOR_PATH = 'threat.indicator';
+// Document path where threat indicator fields are expected. Fields are used
+// to enrich signals, and are copied to threat.indicator.
+export const DEFAULT_INDICATOR_SOURCE_PATH = 'threatintel.indicator';
+export const INDICATOR_DESTINATION_PATH = 'threat.indicator';
export enum SecurityPageName {
detections = 'detections',
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts
index 3bae9551f4df7..a51c1f77844d5 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { DEFAULT_INDICATOR_PATH } from '../../../constants';
+import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../constants';
import {
MachineLearningCreateSchema,
MachineLearningUpdateSchema,
@@ -57,7 +57,7 @@ export const getCreateThreatMatchRulesSchemaMock = (
rule_id: ruleId,
threat_query: '*:*',
threat_index: ['list-index'],
- threat_indicator_path: DEFAULT_INDICATOR_PATH,
+ threat_indicator_path: DEFAULT_INDICATOR_SOURCE_PATH,
threat_mapping: [
{
entries: [
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts
index 8dc7427ed0933..730e2949d7a11 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { DEFAULT_INDICATOR_PATH } from '../../../constants';
+import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../constants';
import { getListArrayMock } from '../types/lists.mock';
import { RulesSchema } from './rules_schema';
@@ -151,7 +151,7 @@ export const getThreatMatchingSchemaPartialMock = (enabled = false): Partial {
+ before(() => {
+ cleanKibana();
+ loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
+ waitForAlertsIndexToBeCreated();
+ createCustomRule(newRule);
+ goToManageAlertsDetectionRules();
+ goToRuleDetails();
+
+ cy.get(RULE_STATUS).should('have.text', '—');
+
+ // this is a made-up index that has just the necessary
+ // mappings to conduct tests, avoiding loading large
+ // amounts of data like in auditbeat_exceptions
+ esArchiverLoad('exceptions');
+
+ goToExceptionsTab();
+ });
+
+ after(() => {
+ esArchiverUnload('exceptions');
+ });
+
+ it('Does not overwrite values and-ed together', () => {
+ cy.get(ADD_EXCEPTIONS_BTN).click({ force: true });
+
+ // add multiple entries with invalid field values
+ addExceptionEntryFieldValue('agent.name', 0);
+ cy.get(ADD_AND_BTN).click();
+ addExceptionEntryFieldValue('@timestamp', 1);
+ cy.get(ADD_AND_BTN).click();
+ addExceptionEntryFieldValue('c', 2);
+
+ // delete second item, invalid values 'a' and 'c' should remain
+ cy.get(ENTRY_DELETE_BTN).eq(1).click();
+ cy.get(FIELD_INPUT).eq(0).should('have.text', 'agent.name');
+ cy.get(FIELD_INPUT).eq(1).should('have.text', 'c');
+
+ closeExceptionBuilderModal();
+ });
+
+ it('Does not overwrite values or-ed together', () => {
+ cy.get(ADD_EXCEPTIONS_BTN).click({ force: true });
+
+ // exception item 1
+ addExceptionEntryFieldValueOfItemX('agent.name', 0, 0);
+ cy.get(ADD_AND_BTN).click();
+ addExceptionEntryFieldValueOfItemX('user.id.keyword', 0, 1);
+
+ // exception item 2
+ cy.get(ADD_OR_BTN).click();
+ addExceptionEntryFieldValueOfItemX('user.first', 1, 0);
+ cy.get(ADD_AND_BTN).click();
+ addExceptionEntryFieldValueOfItemX('user.last', 1, 1);
+ cy.get(ADD_AND_BTN).click();
+ addExceptionEntryFieldValueOfItemX('e', 1, 2);
+
+ // delete single entry from exception item 2
+ cy.get(ENTRY_DELETE_BTN).eq(3).click();
+ cy.get(EXCEPTION_ITEM_CONTAINER)
+ .eq(0)
+ .find(FIELD_INPUT)
+ .eq(0)
+ .should('have.text', 'agent.name');
+ cy.get(EXCEPTION_ITEM_CONTAINER)
+ .eq(0)
+ .find(FIELD_INPUT)
+ .eq(1)
+ .should('have.text', 'user.id.keyword');
+ cy.get(EXCEPTION_ITEM_CONTAINER)
+ .eq(1)
+ .find(FIELD_INPUT)
+ .eq(0)
+ .should('have.text', 'user.first');
+ cy.get(EXCEPTION_ITEM_CONTAINER).eq(1).find(FIELD_INPUT).eq(1).should('have.text', 'e');
+
+ // delete remaining entries in exception item 2
+ cy.get(ENTRY_DELETE_BTN).eq(2).click();
+ cy.get(ENTRY_DELETE_BTN).eq(2).click();
+ cy.get(EXCEPTION_ITEM_CONTAINER)
+ .eq(0)
+ .find(FIELD_INPUT)
+ .eq(0)
+ .should('have.text', 'agent.name');
+ cy.get(EXCEPTION_ITEM_CONTAINER)
+ .eq(0)
+ .find(FIELD_INPUT)
+ .eq(1)
+ .should('have.text', 'user.id.keyword');
+ cy.get(EXCEPTION_ITEM_CONTAINER).eq(1).should('not.exist');
+
+ closeExceptionBuilderModal();
+ });
+
+ it('Does not overwrite values of nested entry items', () => {
+ openExceptionModalFromRuleSettings();
+ cy.get(LOADING_SPINNER).should('not.exist');
+
+ // exception item 1
+ addExceptionEntryFieldValueOfItemX('agent.name', 0, 0);
+ cy.get(ADD_AND_BTN).click();
+ addExceptionEntryFieldValueOfItemX('b', 0, 1);
+
+ // exception item 2 with nested field
+ cy.get(ADD_OR_BTN).click();
+ addExceptionEntryFieldValueOfItemX('c', 1, 0);
+ cy.get(ADD_NESTED_BTN).click();
+ addExceptionEntryFieldValueOfItemX('user.id{downarrow}{enter}', 1, 1);
+ cy.get(ADD_AND_BTN).click();
+ addExceptionEntryFieldValueOfItemX('last{downarrow}{enter}', 1, 3);
+ // This button will now read `Add non-nested button`
+ cy.get(ADD_NESTED_BTN).click();
+ addExceptionEntryFieldValueOfItemX('@timestamp', 1, 4);
+
+ // should have only deleted `user.id`
+ cy.get(ENTRY_DELETE_BTN).eq(4).click();
+ cy.get(EXCEPTION_ITEM_CONTAINER)
+ .eq(0)
+ .find(FIELD_INPUT)
+ .eq(0)
+ .should('have.text', 'agent.name');
+ cy.get(EXCEPTION_ITEM_CONTAINER).eq(0).find(FIELD_INPUT).eq(1).should('have.text', 'b');
+ cy.get(EXCEPTION_ITEM_CONTAINER).eq(1).find(FIELD_INPUT).eq(0).should('have.text', 'c');
+ cy.get(EXCEPTION_ITEM_CONTAINER).eq(1).find(FIELD_INPUT).eq(1).should('have.text', 'user');
+ cy.get(EXCEPTION_ITEM_CONTAINER).eq(1).find(FIELD_INPUT).eq(2).should('have.text', 'last');
+ cy.get(EXCEPTION_ITEM_CONTAINER)
+ .eq(1)
+ .find(FIELD_INPUT)
+ .eq(3)
+ .should('have.text', '@timestamp');
+
+ // deleting the last value of a nested entry, should delete the child and parent
+ cy.get(ENTRY_DELETE_BTN).eq(4).click();
+ cy.get(EXCEPTION_ITEM_CONTAINER)
+ .eq(0)
+ .find(FIELD_INPUT)
+ .eq(0)
+ .should('have.text', 'agent.name');
+ cy.get(EXCEPTION_ITEM_CONTAINER).eq(0).find(FIELD_INPUT).eq(1).should('have.text', 'b');
+ cy.get(EXCEPTION_ITEM_CONTAINER).eq(1).find(FIELD_INPUT).eq(0).should('have.text', 'c');
+ cy.get(EXCEPTION_ITEM_CONTAINER)
+ .eq(1)
+ .find(FIELD_INPUT)
+ .eq(1)
+ .should('have.text', '@timestamp');
+
+ closeExceptionBuilderModal();
+ });
+});
diff --git a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts
index 7cd273b1db746..2479b76cf1de4 100644
--- a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts
@@ -24,6 +24,20 @@ export const OPERATOR_INPUT = '[data-test-subj="operatorAutocompleteComboBox"]';
export const VALUES_INPUT =
'[data-test-subj="valuesAutocompleteMatch"] [data-test-subj="comboBoxInput"]';
+export const ADD_AND_BTN = '[data-test-subj="exceptionsAndButton"]';
+
+export const ADD_OR_BTN = '[data-test-subj="exceptionsOrButton"]';
+
+export const ADD_NESTED_BTN = '[data-test-subj="exceptionsNestedButton"]';
+
+export const ENTRY_DELETE_BTN = '[data-test-subj="builderItemEntryDeleteButton"]';
+
+export const FIELD_INPUT_LIST_BTN = '[data-test-subj="comboBoxToggleListButton"]';
+
+export const CANCEL_BTN = '[data-test-subj="cancelExceptionAddButton"]';
+
+export const BUILDER_MODAL_BODY = '[data-test-subj="exceptionsBuilderWrapper"]';
+
export const EXCEPTIONS_TABLE_TAB = '[data-test-subj="allRulesTableTab-exceptions"]';
export const EXCEPTIONS_TABLE = '[data-test-subj="exceptions-table"]';
@@ -43,3 +57,5 @@ export const EXCEPTIONS_TABLE_LIST_NAME = '[data-test-subj="exceptionsTableName"
export const EXCEPTIONS_TABLE_MODAL = '[data-test-subj="referenceErrorModal"]';
export const EXCEPTIONS_TABLE_MODAL_CONFIRM_BTN = '[data-test-subj="confirmModalConfirmButton"]';
+
+export const EXCEPTION_ITEM_CONTAINER = '[data-test-subj="exceptionEntriesContainer"]';
diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts
index 529ef4afdfa63..10644e046a68b 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts
@@ -116,9 +116,19 @@ export const reloadDeletedRules = () => {
cy.get(RELOAD_PREBUILT_RULES_BTN).click({ force: true });
};
+/**
+ * Selects the number of rules. Since there can be missing click handlers
+ * when the page loads at first, we use a pipe and a trigger of click
+ * on it and then check to ensure that it is checked before continuing
+ * with the tests.
+ * @param numberOfRules The number of rules to click/check
+ */
export const selectNumberOfRules = (numberOfRules: number) => {
for (let i = 0; i < numberOfRules; i++) {
- cy.get(RULE_CHECKBOX).eq(i).click({ force: true });
+ cy.get(RULE_CHECKBOX)
+ .eq(i)
+ .pipe(($el) => $el.trigger('click'))
+ .should('be.checked');
}
};
diff --git a/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts b/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts
new file mode 100644
index 0000000000000..97e93ef8194a4
--- /dev/null
+++ b/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts
@@ -0,0 +1,62 @@
+/*
+ * 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 { Exception } from '../objects/exception';
+import {
+ FIELD_INPUT,
+ OPERATOR_INPUT,
+ VALUES_INPUT,
+ CANCEL_BTN,
+ BUILDER_MODAL_BODY,
+ EXCEPTION_ITEM_CONTAINER,
+} from '../screens/exceptions';
+
+export const addExceptionEntryFieldValueOfItemX = (
+ field: string,
+ itemIndex = 0,
+ fieldIndex = 0
+) => {
+ cy.get(EXCEPTION_ITEM_CONTAINER)
+ .eq(itemIndex)
+ .find(FIELD_INPUT)
+ .eq(fieldIndex)
+ .type(`${field}{enter}`);
+ cy.get(BUILDER_MODAL_BODY).click();
+};
+
+export const addExceptionEntryFieldValue = (field: string, index = 0) => {
+ cy.get(FIELD_INPUT).eq(index).type(`${field}{enter}`);
+ cy.get(BUILDER_MODAL_BODY).click();
+};
+
+export const addExceptionEntryOperatorValue = (operator: string, index = 0) => {
+ cy.get(OPERATOR_INPUT).eq(index).type(`${operator}{enter}`);
+ cy.get(BUILDER_MODAL_BODY).click();
+};
+
+export const addExceptionEntryValue = (values: string[], index = 0) => {
+ values.forEach((value) => {
+ cy.get(VALUES_INPUT).eq(index).type(`${value}{enter}`);
+ });
+ cy.get(BUILDER_MODAL_BODY).click();
+};
+
+export const addExceptionEntry = (exception: Exception, index = 0) => {
+ addExceptionEntryFieldValue(exception.field, index);
+ addExceptionEntryOperatorValue(exception.operator, index);
+ addExceptionEntryValue(exception.values, index);
+};
+
+export const addNestedExceptionEntry = (exception: Exception, index = 0) => {
+ addExceptionEntryFieldValue(exception.field, index);
+ addExceptionEntryOperatorValue(exception.operator, index);
+ addExceptionEntryValue(exception.values, index);
+};
+
+export const closeExceptionBuilderModal = () => {
+ cy.get(CANCEL_BTN).click();
+};
diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts
index 57037e9f269b4..411f326a0ace6 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts
@@ -54,6 +54,12 @@ export const addsException = (exception: Exception) => {
cy.get(CONFIRM_BTN).should('not.exist');
};
+export const openExceptionModalFromRuleSettings = () => {
+ cy.get(ADD_EXCEPTIONS_BTN).click();
+ cy.get(LOADING_SPINNER).should('not.exist');
+ cy.get(FIELD_INPUT).should('be.visible');
+};
+
export const addsExceptionFromRuleSettings = (exception: Exception) => {
cy.get(ADD_EXCEPTIONS_BTN).click();
cy.get(LOADING_SPINNER).should('exist');
diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.tsx
index b7a8a45edce5e..03000e8916617 100644
--- a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.tsx
@@ -35,19 +35,20 @@ export const useGetFieldsByIssueType = ({
}: Props): UseGetFieldsByIssueType => {
const [isLoading, setIsLoading] = useState(true);
const [fields, setFields] = useState({});
+ const didCancel = useRef(false);
const abortCtrl = useRef(new AbortController());
useEffect(() => {
- let didCancel = false;
const fetchData = async () => {
if (!connector || !issueType) {
setIsLoading(false);
return;
}
- abortCtrl.current = new AbortController();
- setIsLoading(true);
try {
+ abortCtrl.current = new AbortController();
+ setIsLoading(true);
+
const res = await getFieldsByIssueType({
http,
signal: abortCtrl.current.signal,
@@ -55,7 +56,7 @@ export const useGetFieldsByIssueType = ({
id: issueType,
});
- if (!didCancel) {
+ if (!didCancel.current) {
setIsLoading(false);
setFields(res.data ?? {});
if (res.status && res.status === 'error') {
@@ -66,22 +67,24 @@ export const useGetFieldsByIssueType = ({
}
}
} catch (error) {
- if (!didCancel) {
+ if (!didCancel.current) {
setIsLoading(false);
- toastNotifications.addDanger({
- title: i18n.FIELDS_API_ERROR,
- text: error.message,
- });
+ if (error.name !== 'AbortError') {
+ toastNotifications.addDanger({
+ title: i18n.FIELDS_API_ERROR,
+ text: error.message,
+ });
+ }
}
}
};
+ didCancel.current = false;
abortCtrl.current.abort();
fetchData();
return () => {
- didCancel = true;
- setIsLoading(false);
+ didCancel.current = true;
abortCtrl.current.abort();
};
}, [http, connector, issueType, toastNotifications]);
diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.tsx
index 4b60a9840c82b..3c35d315a2bcd 100644
--- a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.tsx
@@ -35,27 +35,27 @@ export const useGetIssueTypes = ({
}: Props): UseGetIssueTypes => {
const [isLoading, setIsLoading] = useState(true);
const [issueTypes, setIssueTypes] = useState([]);
+ const didCancel = useRef(false);
const abortCtrl = useRef(new AbortController());
useEffect(() => {
- let didCancel = false;
const fetchData = async () => {
if (!connector) {
setIsLoading(false);
return;
}
- abortCtrl.current = new AbortController();
- setIsLoading(true);
-
try {
+ abortCtrl.current = new AbortController();
+ setIsLoading(true);
+
const res = await getIssueTypes({
http,
signal: abortCtrl.current.signal,
connectorId: connector.id,
});
- if (!didCancel) {
+ if (!didCancel.current) {
setIsLoading(false);
const asOptions = (res.data ?? []).map((type) => ({
text: type.name ?? '',
@@ -71,25 +71,29 @@ export const useGetIssueTypes = ({
}
}
} catch (error) {
- if (!didCancel) {
+ if (!didCancel.current) {
setIsLoading(false);
- toastNotifications.addDanger({
- title: i18n.ISSUE_TYPES_API_ERROR,
- text: error.message,
- });
+ if (error.name !== 'AbortError') {
+ toastNotifications.addDanger({
+ title: i18n.ISSUE_TYPES_API_ERROR,
+ text: error.message,
+ });
+ }
}
}
};
+ didCancel.current = false;
abortCtrl.current.abort();
fetchData();
return () => {
- didCancel = true;
- setIsLoading(false);
+ didCancel.current = true;
abortCtrl.current.abort();
};
- }, [http, connector, toastNotifications, handleIssueType]);
+ // handleIssueType unmounts the component at init causing the request to be aborted
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [http, connector, toastNotifications]);
return {
issueTypes,
diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.tsx
index 170cf2b53395e..b44b0558f1536 100644
--- a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.tsx
@@ -36,20 +36,20 @@ export const useGetIssues = ({
}: Props): UseGetIssues => {
const [isLoading, setIsLoading] = useState(false);
const [issues, setIssues] = useState([]);
+ const didCancel = useRef(false);
const abortCtrl = useRef(new AbortController());
useEffect(() => {
- let didCancel = false;
const fetchData = debounce(500, async () => {
if (!actionConnector || isEmpty(query)) {
setIsLoading(false);
return;
}
- abortCtrl.current = new AbortController();
- setIsLoading(true);
-
try {
+ abortCtrl.current = new AbortController();
+ setIsLoading(true);
+
const res = await getIssues({
http,
signal: abortCtrl.current.signal,
@@ -57,7 +57,7 @@ export const useGetIssues = ({
title: query ?? '',
});
- if (!didCancel) {
+ if (!didCancel.current) {
setIsLoading(false);
setIssues(res.data ?? []);
if (res.status && res.status === 'error') {
@@ -68,22 +68,24 @@ export const useGetIssues = ({
}
}
} catch (error) {
- if (!didCancel) {
+ if (!didCancel.current) {
setIsLoading(false);
- toastNotifications.addDanger({
- title: i18n.ISSUES_API_ERROR,
- text: error.message,
- });
+ if (error.name !== 'AbortError') {
+ toastNotifications.addDanger({
+ title: i18n.ISSUES_API_ERROR,
+ text: error.message,
+ });
+ }
}
}
});
+ didCancel.current = false;
abortCtrl.current.abort();
fetchData();
return () => {
- didCancel = true;
- setIsLoading(false);
+ didCancel.current = true;
abortCtrl.current.abort();
};
}, [http, actionConnector, toastNotifications, query]);
diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.tsx
index 89b42b1a88c1e..6c70286426168 100644
--- a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.tsx
@@ -35,10 +35,10 @@ export const useGetSingleIssue = ({
}: Props): UseGetSingleIssue => {
const [isLoading, setIsLoading] = useState(false);
const [issue, setIssue] = useState(null);
+ const didCancel = useRef(false);
const abortCtrl = useRef(new AbortController());
useEffect(() => {
- let didCancel = false;
const fetchData = async () => {
if (!actionConnector || !id) {
setIsLoading(false);
@@ -55,7 +55,7 @@ export const useGetSingleIssue = ({
id,
});
- if (!didCancel) {
+ if (!didCancel.current) {
setIsLoading(false);
setIssue(res.data ?? null);
if (res.status && res.status === 'error') {
@@ -66,22 +66,24 @@ export const useGetSingleIssue = ({
}
}
} catch (error) {
- if (!didCancel) {
+ if (!didCancel.current) {
setIsLoading(false);
- toastNotifications.addDanger({
- title: i18n.GET_ISSUE_API_ERROR(id),
- text: error.message,
- });
+ if (error.name !== 'AbortError') {
+ toastNotifications.addDanger({
+ title: i18n.GET_ISSUE_API_ERROR(id),
+ text: error.message,
+ });
+ }
}
}
};
+ didCancel.current = false;
abortCtrl.current.abort();
fetchData();
return () => {
- didCancel = true;
- setIsLoading(false);
+ didCancel.current = true;
abortCtrl.current.abort();
};
}, [http, actionConnector, id, toastNotifications]);
diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.tsx
index 99964f466058f..34cbb0a69b0f4 100644
--- a/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.tsx
@@ -34,27 +34,27 @@ export const useGetIncidentTypes = ({
}: Props): UseGetIncidentTypes => {
const [isLoading, setIsLoading] = useState(true);
const [incidentTypes, setIncidentTypes] = useState([]);
+ const didCancel = useRef(false);
const abortCtrl = useRef(new AbortController());
useEffect(() => {
- let didCancel = false;
const fetchData = async () => {
if (!connector) {
setIsLoading(false);
return;
}
- abortCtrl.current = new AbortController();
- setIsLoading(true);
-
try {
+ abortCtrl.current = new AbortController();
+ setIsLoading(true);
+
const res = await getIncidentTypes({
http,
signal: abortCtrl.current.signal,
connectorId: connector.id,
});
- if (!didCancel) {
+ if (!didCancel.current) {
setIsLoading(false);
setIncidentTypes(res.data ?? []);
if (res.status && res.status === 'error') {
@@ -65,22 +65,24 @@ export const useGetIncidentTypes = ({
}
}
} catch (error) {
- if (!didCancel) {
+ if (!didCancel.current) {
setIsLoading(false);
- toastNotifications.addDanger({
- title: i18n.INCIDENT_TYPES_API_ERROR,
- text: error.message,
- });
+ if (error.name !== 'AbortError') {
+ toastNotifications.addDanger({
+ title: i18n.INCIDENT_TYPES_API_ERROR,
+ text: error.message,
+ });
+ }
}
}
};
+ didCancel.current = false;
abortCtrl.current.abort();
fetchData();
return () => {
- didCancel = true;
- setIsLoading(false);
+ didCancel.current = true;
abortCtrl.current.abort();
};
}, [http, connector, toastNotifications]);
diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.tsx
index 0a71891ae41b2..5b44c6b4a32b2 100644
--- a/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.tsx
@@ -7,9 +7,9 @@
import { useState, useEffect, useRef } from 'react';
import { HttpSetup, ToastsApi } from 'kibana/public';
+import { ActionConnector } from '../../../containers/types';
import { getSeverity } from './api';
import * as i18n from './translations';
-import { ActionConnector } from '../../../containers/types';
type Severity = Array<{ id: number; name: string }>;
@@ -31,26 +31,26 @@ export const useGetSeverity = ({ http, toastNotifications, connector }: Props):
const [isLoading, setIsLoading] = useState(true);
const [severity, setSeverity] = useState([]);
const abortCtrl = useRef(new AbortController());
+ const didCancel = useRef(false);
useEffect(() => {
- let didCancel = false;
const fetchData = async () => {
if (!connector) {
setIsLoading(false);
return;
}
- abortCtrl.current = new AbortController();
- setIsLoading(true);
-
try {
+ abortCtrl.current = new AbortController();
+ setIsLoading(true);
+
const res = await getSeverity({
http,
signal: abortCtrl.current.signal,
connectorId: connector.id,
});
- if (!didCancel) {
+ if (!didCancel.current) {
setIsLoading(false);
setSeverity(res.data ?? []);
@@ -62,22 +62,24 @@ export const useGetSeverity = ({ http, toastNotifications, connector }: Props):
}
}
} catch (error) {
- if (!didCancel) {
+ if (!didCancel.current) {
setIsLoading(false);
- toastNotifications.addDanger({
- title: i18n.SEVERITY_API_ERROR,
- text: error.message,
- });
+ if (error.name !== 'AbortError') {
+ toastNotifications.addDanger({
+ title: i18n.SEVERITY_API_ERROR,
+ text: error.message,
+ });
+ }
}
}
};
+ didCancel.current = false;
abortCtrl.current.abort();
fetchData();
return () => {
- didCancel = true;
- setIsLoading(false);
+ didCancel.current = true;
abortCtrl.current.abort();
};
}, [http, connector, toastNotifications]);
diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx
index 16e905bdabfee..a979f96d84ab2 100644
--- a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx
@@ -37,20 +37,20 @@ export const useGetChoices = ({
}: UseGetChoicesProps): UseGetChoices => {
const [isLoading, setIsLoading] = useState(false);
const [choices, setChoices] = useState([]);
+ const didCancel = useRef(false);
const abortCtrl = useRef(new AbortController());
useEffect(() => {
- let didCancel = false;
const fetchData = async () => {
if (!connector) {
setIsLoading(false);
return;
}
- abortCtrl.current = new AbortController();
- setIsLoading(true);
-
try {
+ abortCtrl.current = new AbortController();
+ setIsLoading(true);
+
const res = await getChoices({
http,
signal: abortCtrl.current.signal,
@@ -58,7 +58,7 @@ export const useGetChoices = ({
fields,
});
- if (!didCancel) {
+ if (!didCancel.current) {
setIsLoading(false);
setChoices(res.data ?? []);
if (res.status && res.status === 'error') {
@@ -71,22 +71,24 @@ export const useGetChoices = ({
}
}
} catch (error) {
- if (!didCancel) {
+ if (!didCancel.current) {
setIsLoading(false);
- toastNotifications.addDanger({
- title: i18n.CHOICES_API_ERROR,
- text: error.message,
- });
+ if (error.name !== 'AbortError') {
+ toastNotifications.addDanger({
+ title: i18n.CHOICES_API_ERROR,
+ text: error.message,
+ });
+ }
}
}
};
+ didCancel.current = false;
abortCtrl.current.abort();
fetchData();
return () => {
- didCancel = true;
- setIsLoading(false);
+ didCancel.current = true;
abortCtrl.current.abort();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.tsx
index ff5762b8476de..3590fffdef5b2 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.tsx
@@ -22,25 +22,25 @@ export const useActionTypes = (): UseActionTypesResponse => {
const [, dispatchToaster] = useStateToaster();
const [loading, setLoading] = useState(true);
const [actionTypes, setActionTypes] = useState([]);
- const didCancel = useRef(false);
- const abortCtrl = useRef(new AbortController());
+ const isCancelledRef = useRef(false);
+ const abortCtrlRef = useRef(new AbortController());
const queryFirstTime = useRef(true);
const refetchActionTypes = useCallback(async () => {
try {
setLoading(true);
- didCancel.current = false;
- abortCtrl.current.abort();
- abortCtrl.current = new AbortController();
+ isCancelledRef.current = false;
+ abortCtrlRef.current.abort();
+ abortCtrlRef.current = new AbortController();
- const res = await fetchActionTypes({ signal: abortCtrl.current.signal });
+ const res = await fetchActionTypes({ signal: abortCtrlRef.current.signal });
- if (!didCancel.current) {
+ if (!isCancelledRef.current) {
setLoading(false);
setActionTypes(res);
}
} catch (error) {
- if (!didCancel.current) {
+ if (!isCancelledRef.current) {
setLoading(false);
setActionTypes([]);
errorToToaster({
@@ -59,8 +59,8 @@ export const useActionTypes = (): UseActionTypesResponse => {
}
return () => {
- didCancel.current = true;
- abortCtrl.current.abort();
+ isCancelledRef.current = true;
+ abortCtrlRef.current.abort();
queryFirstTime.current = true;
};
}, [refetchActionTypes]);
diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx
index cc8c93fc990eb..21d1832796ba8 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { useEffect, useCallback, useReducer } from 'react';
+import { useEffect, useCallback, useReducer, useRef } from 'react';
import { getCaseConfigure, patchCaseConfigure, postCaseConfigure } from './api';
import {
@@ -207,129 +207,128 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => {
}, []);
const [, dispatchToaster] = useStateToaster();
+ const isCancelledRefetchRef = useRef(false);
+ const abortCtrlRefetchRef = useRef(new AbortController());
- const refetchCaseConfigure = useCallback(() => {
- let didCancel = false;
- const abortCtrl = new AbortController();
+ const isCancelledPersistRef = useRef(false);
+ const abortCtrlPersistRef = useRef(new AbortController());
- const fetchCaseConfiguration = async () => {
- try {
- setLoading(true);
- const res = await getCaseConfigure({ signal: abortCtrl.signal });
- if (!didCancel) {
- if (res != null) {
- setConnector(res.connector);
- if (setClosureType != null) {
- setClosureType(res.closureType);
- }
- setVersion(res.version);
- setMappings(res.mappings);
+ const refetchCaseConfigure = useCallback(async () => {
+ try {
+ isCancelledRefetchRef.current = false;
+ abortCtrlRefetchRef.current.abort();
+ abortCtrlRefetchRef.current = new AbortController();
- if (!state.firstLoad) {
- setFirstLoad(true);
- if (setCurrentConfiguration != null) {
- setCurrentConfiguration({
- closureType: res.closureType,
- connector: {
- ...res.connector,
- },
- });
- }
- }
- if (res.error != null) {
- errorToToaster({
- dispatchToaster,
- error: new Error(res.error),
- title: i18n.ERROR_TITLE,
+ setLoading(true);
+ const res = await getCaseConfigure({ signal: abortCtrlRefetchRef.current.signal });
+
+ if (!isCancelledRefetchRef.current) {
+ if (res != null) {
+ setConnector(res.connector);
+ if (setClosureType != null) {
+ setClosureType(res.closureType);
+ }
+ setVersion(res.version);
+ setMappings(res.mappings);
+
+ if (!state.firstLoad) {
+ setFirstLoad(true);
+ if (setCurrentConfiguration != null) {
+ setCurrentConfiguration({
+ closureType: res.closureType,
+ connector: {
+ ...res.connector,
+ },
});
}
}
- setLoading(false);
+ if (res.error != null) {
+ errorToToaster({
+ dispatchToaster,
+ error: new Error(res.error),
+ title: i18n.ERROR_TITLE,
+ });
+ }
}
- } catch (error) {
- if (!didCancel) {
- setLoading(false);
+ setLoading(false);
+ }
+ } catch (error) {
+ if (!isCancelledRefetchRef.current) {
+ if (error.name !== 'AbortError') {
errorToToaster({
dispatchToaster,
error: error.body && error.body.message ? new Error(error.body.message) : error,
title: i18n.ERROR_TITLE,
});
}
+ setLoading(false);
}
- };
-
- fetchCaseConfiguration();
-
- return () => {
- didCancel = true;
- abortCtrl.abort();
- };
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state.firstLoad]);
const persistCaseConfigure = useCallback(
async ({ connector, closureType }: ConnectorConfiguration) => {
- let didCancel = false;
- const abortCtrl = new AbortController();
- const saveCaseConfiguration = async () => {
- try {
- setPersistLoading(true);
- const connectorObj = {
- connector,
- closure_type: closureType,
- };
- const res =
- state.version.length === 0
- ? await postCaseConfigure(connectorObj, abortCtrl.signal)
- : await patchCaseConfigure(
- {
- ...connectorObj,
- version: state.version,
- },
- abortCtrl.signal
- );
- if (!didCancel) {
- setConnector(res.connector);
- if (setClosureType) {
- setClosureType(res.closureType);
- }
- setVersion(res.version);
- setMappings(res.mappings);
- if (setCurrentConfiguration != null) {
- setCurrentConfiguration({
- closureType: res.closureType,
- connector: {
- ...res.connector,
+ try {
+ isCancelledPersistRef.current = false;
+ abortCtrlPersistRef.current.abort();
+ abortCtrlPersistRef.current = new AbortController();
+ setPersistLoading(true);
+
+ const connectorObj = {
+ connector,
+ closure_type: closureType,
+ };
+
+ const res =
+ state.version.length === 0
+ ? await postCaseConfigure(connectorObj, abortCtrlPersistRef.current.signal)
+ : await patchCaseConfigure(
+ {
+ ...connectorObj,
+ version: state.version,
},
- });
- }
- if (res.error != null) {
- errorToToaster({
- dispatchToaster,
- error: new Error(res.error),
- title: i18n.ERROR_TITLE,
- });
- }
- displaySuccessToast(i18n.SUCCESS_CONFIGURE, dispatchToaster);
- setPersistLoading(false);
+ abortCtrlPersistRef.current.signal
+ );
+
+ if (!isCancelledPersistRef.current) {
+ setConnector(res.connector);
+ if (setClosureType) {
+ setClosureType(res.closureType);
+ }
+ setVersion(res.version);
+ setMappings(res.mappings);
+ if (setCurrentConfiguration != null) {
+ setCurrentConfiguration({
+ closureType: res.closureType,
+ connector: {
+ ...res.connector,
+ },
+ });
}
- } catch (error) {
- if (!didCancel) {
- setConnector(state.currentConfiguration.connector);
- setPersistLoading(false);
+ if (res.error != null) {
+ errorToToaster({
+ dispatchToaster,
+ error: new Error(res.error),
+ title: i18n.ERROR_TITLE,
+ });
+ }
+ displaySuccessToast(i18n.SUCCESS_CONFIGURE, dispatchToaster);
+ setPersistLoading(false);
+ }
+ } catch (error) {
+ if (!isCancelledPersistRef.current) {
+ if (error.name !== 'AbortError') {
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
}
+ setConnector(state.currentConfiguration.connector);
+ setPersistLoading(false);
}
- };
- saveCaseConfiguration();
- return () => {
- didCancel = true;
- abortCtrl.abort();
- };
+ }
},
[
dispatchToaster,
@@ -345,6 +344,12 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => {
useEffect(() => {
refetchCaseConfigure();
+ return () => {
+ isCancelledRefetchRef.current = true;
+ abortCtrlRefetchRef.current.abort();
+ isCancelledPersistRef.current = true;
+ abortCtrlPersistRef.current.abort();
+ };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_connectors.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_connectors.tsx
index d21e50902ca83..338d04f702c63 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/configure/use_connectors.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_connectors.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { useState, useEffect, useCallback } from 'react';
+import { useState, useEffect, useCallback, useRef } from 'react';
import { useStateToaster, errorToToaster } from '../../../common/components/toasters';
import * as i18n from '../translations';
@@ -22,40 +22,45 @@ export const useConnectors = (): UseConnectorsResponse => {
const [, dispatchToaster] = useStateToaster();
const [loading, setLoading] = useState(true);
const [connectors, setConnectors] = useState([]);
+ const isCancelledRef = useRef(false);
+ const abortCtrlRef = useRef(new AbortController());
- const refetchConnectors = useCallback(() => {
- let didCancel = false;
- const abortCtrl = new AbortController();
- const getConnectors = async () => {
- try {
- setLoading(true);
- const res = await fetchConnectors({ signal: abortCtrl.signal });
- if (!didCancel) {
- setLoading(false);
- setConnectors(res);
- }
- } catch (error) {
- if (!didCancel) {
- setLoading(false);
- setConnectors([]);
+ const refetchConnectors = useCallback(async () => {
+ try {
+ isCancelledRef.current = false;
+ abortCtrlRef.current.abort();
+ abortCtrlRef.current = new AbortController();
+
+ setLoading(true);
+ const res = await fetchConnectors({ signal: abortCtrlRef.current.signal });
+
+ if (!isCancelledRef.current) {
+ setLoading(false);
+ setConnectors(res);
+ }
+ } catch (error) {
+ if (!isCancelledRef.current) {
+ if (error.name !== 'AbortError') {
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
}
+
+ setLoading(false);
+ setConnectors([]);
}
- };
- getConnectors();
- return () => {
- didCancel = true;
- abortCtrl.abort();
- };
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
refetchConnectors();
+ return () => {
+ isCancelledRef.current = true;
+ abortCtrlRef.current.abort();
+ };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx
index 0fe45aaab799b..da069ee6f1075 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { useCallback, useReducer } from 'react';
+import { useCallback, useReducer, useRef, useEffect } from 'react';
import { CaseStatuses } from '../../../../case/common/api';
import {
displaySuccessToast,
@@ -87,49 +87,45 @@ export const useUpdateCases = (): UseUpdateCases => {
isUpdated: false,
});
const [, dispatchToaster] = useStateToaster();
+ const isCancelledRef = useRef(false);
+ const abortCtrlRef = useRef(new AbortController());
- const dispatchUpdateCases = useCallback((cases: BulkUpdateStatus[], action: string) => {
- let cancel = false;
- const abortCtrl = new AbortController();
+ const dispatchUpdateCases = useCallback(async (cases: BulkUpdateStatus[], action: string) => {
+ try {
+ isCancelledRef.current = false;
+ abortCtrlRef.current.abort();
+ abortCtrlRef.current = new AbortController();
- const patchData = async () => {
- try {
- dispatch({ type: 'FETCH_INIT' });
- const patchResponse = await patchCasesStatus(cases, abortCtrl.signal);
- if (!cancel) {
- const resultCount = Object.keys(patchResponse).length;
- const firstTitle = patchResponse[0].title;
+ dispatch({ type: 'FETCH_INIT' });
+ const patchResponse = await patchCasesStatus(cases, abortCtrlRef.current.signal);
- dispatch({ type: 'FETCH_SUCCESS', payload: true });
+ if (!isCancelledRef.current) {
+ const resultCount = Object.keys(patchResponse).length;
+ const firstTitle = patchResponse[0].title;
- const messageArgs = {
- totalCases: resultCount,
- caseTitle: resultCount === 1 ? firstTitle : '',
- };
+ dispatch({ type: 'FETCH_SUCCESS', payload: true });
+ const messageArgs = {
+ totalCases: resultCount,
+ caseTitle: resultCount === 1 ? firstTitle : '',
+ };
- const message =
- action === 'status'
- ? getStatusToasterMessage(patchResponse[0].status, messageArgs)
- : '';
+ const message =
+ action === 'status' ? getStatusToasterMessage(patchResponse[0].status, messageArgs) : '';
- displaySuccessToast(message, dispatchToaster);
- }
- } catch (error) {
- if (!cancel) {
+ displaySuccessToast(message, dispatchToaster);
+ }
+ } catch (error) {
+ if (!isCancelledRef.current) {
+ if (error.name !== 'AbortError') {
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
- dispatch({ type: 'FETCH_FAILURE' });
}
+ dispatch({ type: 'FETCH_FAILURE' });
}
- };
- patchData();
- return () => {
- cancel = true;
- abortCtrl.abort();
- };
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -137,14 +133,25 @@ export const useUpdateCases = (): UseUpdateCases => {
dispatch({ type: 'RESET_IS_UPDATED' });
}, []);
- const updateBulkStatus = useCallback((cases: Case[], status: string) => {
- const updateCasesStatus: BulkUpdateStatus[] = cases.map((theCase) => ({
- status,
- id: theCase.id,
- version: theCase.version,
- }));
- dispatchUpdateCases(updateCasesStatus, 'status');
+ const updateBulkStatus = useCallback(
+ (cases: Case[], status: string) => {
+ const updateCasesStatus: BulkUpdateStatus[] = cases.map((theCase) => ({
+ status,
+ id: theCase.id,
+ version: theCase.version,
+ }));
+ dispatchUpdateCases(updateCasesStatus, 'status');
+ },
// eslint-disable-next-line react-hooks/exhaustive-deps
+ []
+ );
+
+ useEffect(() => {
+ return () => {
+ isCancelledRef.current = true;
+ abortCtrlRef.current.abort();
+ };
}, []);
+
return { ...state, updateBulkStatus, dispatchResetIsUpdated };
};
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx
index 923c20dcf8ebd..f3d59a2883f2a 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { useCallback, useReducer } from 'react';
+import { useCallback, useReducer, useRef, useEffect } from 'react';
import {
displaySuccessToast,
errorToToaster,
@@ -78,45 +78,43 @@ export const useDeleteCases = (): UseDeleteCase => {
isDeleted: false,
});
const [, dispatchToaster] = useStateToaster();
+ const isCancelledRef = useRef(false);
+ const abortCtrlRef = useRef(new AbortController());
- const dispatchDeleteCases = useCallback((cases: DeleteCase[]) => {
- let cancel = false;
- const abortCtrl = new AbortController();
+ const dispatchDeleteCases = useCallback(async (cases: DeleteCase[]) => {
+ try {
+ isCancelledRef.current = false;
+ abortCtrlRef.current.abort();
+ abortCtrlRef.current = new AbortController();
+ dispatch({ type: 'FETCH_INIT' });
- const deleteData = async () => {
- try {
- dispatch({ type: 'FETCH_INIT' });
- const caseIds = cases.map((theCase) => theCase.id);
- // We don't allow user batch delete sub cases on UI at the moment.
- if (cases[0].type != null || cases.length > 1) {
- await deleteCases(caseIds, abortCtrl.signal);
- } else {
- await deleteSubCases(caseIds, abortCtrl.signal);
- }
+ const caseIds = cases.map((theCase) => theCase.id);
+ // We don't allow user batch delete sub cases on UI at the moment.
+ if (cases[0].type != null || cases.length > 1) {
+ await deleteCases(caseIds, abortCtrlRef.current.signal);
+ } else {
+ await deleteSubCases(caseIds, abortCtrlRef.current.signal);
+ }
- if (!cancel) {
- dispatch({ type: 'FETCH_SUCCESS', payload: true });
- displaySuccessToast(
- i18n.DELETED_CASES(cases.length, cases.length === 1 ? cases[0].title : ''),
- dispatchToaster
- );
- }
- } catch (error) {
- if (!cancel) {
+ if (!isCancelledRef.current) {
+ dispatch({ type: 'FETCH_SUCCESS', payload: true });
+ displaySuccessToast(
+ i18n.DELETED_CASES(cases.length, cases.length === 1 ? cases[0].title : ''),
+ dispatchToaster
+ );
+ }
+ } catch (error) {
+ if (!isCancelledRef.current) {
+ if (error.name !== 'AbortError') {
errorToToaster({
title: i18n.ERROR_DELETING,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
- dispatch({ type: 'FETCH_FAILURE' });
}
+ dispatch({ type: 'FETCH_FAILURE' });
}
- };
- deleteData();
- return () => {
- abortCtrl.abort();
- cancel = true;
- };
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -142,5 +140,12 @@ export const useDeleteCases = (): UseDeleteCase => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state.isDisplayConfirmDeleteModal]);
+ useEffect(() => {
+ return () => {
+ isCancelledRef.current = true;
+ abortCtrlRef.current.abort();
+ };
+ }, []);
+
return { ...state, dispatchResetIsDeleted, handleOnDeleteConfirm, handleToggleModal };
};
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.tsx
index 9b536f32e7eb8..9b10247794c8d 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useState, useRef } from 'react';
import { errorToToaster, useStateToaster } from '../../common/components/toasters';
import { getActionLicense } from './api';
@@ -28,53 +28,58 @@ const MINIMUM_LICENSE_REQUIRED_CONNECTOR = '.jira';
export const useGetActionLicense = (): ActionLicenseState => {
const [actionLicenseState, setActionLicensesState] = useState(initialData);
-
const [, dispatchToaster] = useStateToaster();
+ const isCancelledRef = useRef(false);
+ const abortCtrlRef = useRef(new AbortController());
- const fetchActionLicense = useCallback(() => {
- let didCancel = false;
- const abortCtrl = new AbortController();
- const fetchData = async () => {
+ const fetchActionLicense = useCallback(async () => {
+ try {
+ isCancelledRef.current = false;
+ abortCtrlRef.current.abort();
+ abortCtrlRef.current = new AbortController();
setActionLicensesState({
- ...actionLicenseState,
+ ...initialData,
isLoading: true,
});
- try {
- const response = await getActionLicense(abortCtrl.signal);
- if (!didCancel) {
- setActionLicensesState({
- actionLicense:
- response.find((l) => l.id === MINIMUM_LICENSE_REQUIRED_CONNECTOR) ?? null,
- isLoading: false,
- isError: false,
- });
- }
- } catch (error) {
- if (!didCancel) {
+
+ const response = await getActionLicense(abortCtrlRef.current.signal);
+
+ if (!isCancelledRef.current) {
+ setActionLicensesState({
+ actionLicense: response.find((l) => l.id === MINIMUM_LICENSE_REQUIRED_CONNECTOR) ?? null,
+ isLoading: false,
+ isError: false,
+ });
+ }
+ } catch (error) {
+ if (!isCancelledRef.current) {
+ if (error.name !== 'AbortError') {
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
- setActionLicensesState({
- actionLicense: null,
- isLoading: false,
- isError: true,
- });
}
+
+ setActionLicensesState({
+ actionLicense: null,
+ isLoading: false,
+ isError: true,
+ });
}
- };
- fetchData();
- return () => {
- didCancel = true;
- abortCtrl.abort();
- };
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [actionLicenseState]);
useEffect(() => {
fetchActionLicense();
+
+ return () => {
+ isCancelledRef.current = true;
+ abortCtrlRef.current.abort();
+ };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+
return { ...actionLicenseState };
};
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx
index 1c4476e3cb2b7..fb8da8d0663ee 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx
@@ -5,7 +5,6 @@
* 2.0.
*/
-import { isEmpty } from 'lodash';
import { useEffect, useReducer, useCallback, useRef } from 'react';
import { CaseStatuses, CaseType } from '../../../../case/common/api';
@@ -96,48 +95,48 @@ export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => {
data: initialData,
});
const [, dispatchToaster] = useStateToaster();
- const abortCtrl = useRef(new AbortController());
- const didCancel = useRef(false);
+ const isCancelledRef = useRef(false);
+ const abortCtrlRef = useRef(new AbortController());
const updateCase = useCallback((newCase: Case) => {
dispatch({ type: 'UPDATE_CASE', payload: newCase });
}, []);
const callFetch = useCallback(async () => {
- const fetchData = async () => {
+ try {
+ isCancelledRef.current = false;
+ abortCtrlRef.current.abort();
+ abortCtrlRef.current = new AbortController();
dispatch({ type: 'FETCH_INIT' });
- try {
- const response = await (subCaseId
- ? getSubCase(caseId, subCaseId, true, abortCtrl.current.signal)
- : getCase(caseId, true, abortCtrl.current.signal));
- if (!didCancel.current) {
- dispatch({ type: 'FETCH_SUCCESS', payload: response });
- }
- } catch (error) {
- if (!didCancel.current) {
+
+ const response = await (subCaseId
+ ? getSubCase(caseId, subCaseId, true, abortCtrlRef.current.signal)
+ : getCase(caseId, true, abortCtrlRef.current.signal));
+
+ if (!isCancelledRef.current) {
+ dispatch({ type: 'FETCH_SUCCESS', payload: response });
+ }
+ } catch (error) {
+ if (!isCancelledRef.current) {
+ if (error.name !== 'AbortError') {
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
- dispatch({ type: 'FETCH_FAILURE' });
}
+ dispatch({ type: 'FETCH_FAILURE' });
}
- };
- didCancel.current = false;
- abortCtrl.current.abort();
- abortCtrl.current = new AbortController();
- fetchData();
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [caseId, subCaseId]);
useEffect(() => {
- if (!isEmpty(caseId)) {
- callFetch();
- }
+ callFetch();
+
return () => {
- didCancel.current = true;
- abortCtrl.current.abort();
+ isCancelledRef.current = true;
+ abortCtrlRef.current.abort();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [caseId, subCaseId]);
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx
index 12e5f6643351f..cc8deaf72eef6 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx
@@ -6,7 +6,7 @@
*/
import { isEmpty, uniqBy } from 'lodash/fp';
-import { useCallback, useEffect, useRef, useState } from 'react';
+import { useCallback, useEffect, useState, useRef } from 'react';
import deepEqual from 'fast-deep-equal';
import { errorToToaster, useStateToaster } from '../../common/components/toasters';
@@ -244,64 +244,67 @@ export const useGetCaseUserActions = (
const [caseUserActionsState, setCaseUserActionsState] = useState(
initialData
);
- const abortCtrl = useRef(new AbortController());
- const didCancel = useRef(false);
+ const abortCtrlRef = useRef(new AbortController());
+ const isCancelledRef = useRef(false);
const [, dispatchToaster] = useStateToaster();
const fetchCaseUserActions = useCallback(
- (thisCaseId: string, thisSubCaseId?: string) => {
- const fetchData = async () => {
- try {
+ async (thisCaseId: string, thisSubCaseId?: string) => {
+ try {
+ isCancelledRef.current = false;
+ abortCtrlRef.current.abort();
+ abortCtrlRef.current = new AbortController();
+ setCaseUserActionsState({
+ ...caseUserActionsState,
+ isLoading: true,
+ });
+
+ const response = await (thisSubCaseId
+ ? getSubCaseUserActions(thisCaseId, thisSubCaseId, abortCtrlRef.current.signal)
+ : getCaseUserActions(thisCaseId, abortCtrlRef.current.signal));
+
+ if (!isCancelledRef.current) {
+ // Attention Future developer
+ // We are removing the first item because it will always be the creation of the case
+ // and we do not want it to simplify our life
+ const participants = !isEmpty(response)
+ ? uniqBy('actionBy.username', response).map((cau) => cau.actionBy)
+ : [];
+
+ const caseUserActions = !isEmpty(response)
+ ? thisSubCaseId
+ ? response
+ : response.slice(1)
+ : [];
+
setCaseUserActionsState({
- ...caseUserActionsState,
- isLoading: true,
+ caseUserActions,
+ ...getPushedInfo(caseUserActions, caseConnectorId),
+ isLoading: false,
+ isError: false,
+ participants,
});
-
- const response = await (thisSubCaseId
- ? getSubCaseUserActions(thisCaseId, thisSubCaseId, abortCtrl.current.signal)
- : getCaseUserActions(thisCaseId, abortCtrl.current.signal));
- if (!didCancel.current) {
- // Attention Future developer
- // We are removing the first item because it will always be the creation of the case
- // and we do not want it to simplify our life
- const participants = !isEmpty(response)
- ? uniqBy('actionBy.username', response).map((cau) => cau.actionBy)
- : [];
-
- const caseUserActions = !isEmpty(response)
- ? thisSubCaseId
- ? response
- : response.slice(1)
- : [];
- setCaseUserActionsState({
- caseUserActions,
- ...getPushedInfo(caseUserActions, caseConnectorId),
- isLoading: false,
- isError: false,
- participants,
- });
- }
- } catch (error) {
- if (!didCancel.current) {
+ }
+ } catch (error) {
+ if (!isCancelledRef.current) {
+ if (error.name !== 'AbortError') {
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
- setCaseUserActionsState({
- caseServices: {},
- caseUserActions: [],
- hasDataToPush: false,
- isError: true,
- isLoading: false,
- participants: [],
- });
}
+
+ setCaseUserActionsState({
+ caseServices: {},
+ caseUserActions: [],
+ hasDataToPush: false,
+ isError: true,
+ isLoading: false,
+ participants: [],
+ });
}
- };
- abortCtrl.current.abort();
- abortCtrl.current = new AbortController();
- fetchData();
+ }
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[caseConnectorId]
@@ -313,8 +316,8 @@ export const useGetCaseUserActions = (
}
return () => {
- didCancel.current = true;
- abortCtrl.current.abort();
+ isCancelledRef.current = true;
+ abortCtrlRef.current.abort();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [caseId, subCaseId]);
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx
index 298d817fffa88..c83cc02dedb97 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { useCallback, useEffect, useReducer } from 'react';
+import { useCallback, useEffect, useReducer, useRef } from 'react';
import { CaseStatuses } from '../../../../case/common/api';
import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from './constants';
import { AllCases, SortFieldCase, FilterOptions, QueryParams, Case, UpdateByKey } from './types';
@@ -139,6 +139,10 @@ export const useGetCases = (initialQueryParams?: QueryParams): UseGetCases => {
selectedCases: [],
});
const [, dispatchToaster] = useStateToaster();
+ const didCancelFetchCases = useRef(false);
+ const didCancelUpdateCases = useRef(false);
+ const abortCtrlFetchCases = useRef(new AbortController());
+ const abortCtrlUpdateCases = useRef(new AbortController());
const setSelectedCases = useCallback((mySelectedCases: Case[]) => {
dispatch({ type: 'UPDATE_TABLE_SELECTIONS', payload: mySelectedCases });
@@ -152,81 +156,69 @@ export const useGetCases = (initialQueryParams?: QueryParams): UseGetCases => {
dispatch({ type: 'UPDATE_FILTER_OPTIONS', payload: newFilters });
}, []);
- const fetchCases = useCallback((filterOptions: FilterOptions, queryParams: QueryParams) => {
- let didCancel = false;
- const abortCtrl = new AbortController();
-
- const fetchData = async () => {
+ const fetchCases = useCallback(async (filterOptions: FilterOptions, queryParams: QueryParams) => {
+ try {
+ didCancelFetchCases.current = false;
+ abortCtrlFetchCases.current.abort();
+ abortCtrlFetchCases.current = new AbortController();
dispatch({ type: 'FETCH_INIT', payload: 'cases' });
- try {
- const response = await getCases({
- filterOptions,
- queryParams,
- signal: abortCtrl.signal,
+
+ const response = await getCases({
+ filterOptions,
+ queryParams,
+ signal: abortCtrlFetchCases.current.signal,
+ });
+
+ if (!didCancelFetchCases.current) {
+ dispatch({
+ type: 'FETCH_CASES_SUCCESS',
+ payload: response,
});
- if (!didCancel) {
- dispatch({
- type: 'FETCH_CASES_SUCCESS',
- payload: response,
- });
- }
- } catch (error) {
- if (!didCancel) {
+ }
+ } catch (error) {
+ if (!didCancelFetchCases.current) {
+ if (error.name !== 'AbortError') {
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
- dispatch({ type: 'FETCH_FAILURE', payload: 'cases' });
}
+ dispatch({ type: 'FETCH_FAILURE', payload: 'cases' });
}
- };
- fetchData();
- return () => {
- abortCtrl.abort();
- didCancel = true;
- };
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- useEffect(() => fetchCases(state.filterOptions, state.queryParams), [
- state.queryParams,
- state.filterOptions,
- ]);
-
const dispatchUpdateCaseProperty = useCallback(
- ({ updateKey, updateValue, caseId, refetchCasesStatus, version }: UpdateCase) => {
- let didCancel = false;
- const abortCtrl = new AbortController();
-
- const fetchData = async () => {
+ async ({ updateKey, updateValue, caseId, refetchCasesStatus, version }: UpdateCase) => {
+ try {
+ didCancelUpdateCases.current = false;
+ abortCtrlUpdateCases.current.abort();
+ abortCtrlUpdateCases.current = new AbortController();
dispatch({ type: 'FETCH_INIT', payload: 'caseUpdate' });
- try {
- await patchCase(
- caseId,
- { [updateKey]: updateValue },
- // saved object versions are typed as string | undefined, hope that's not true
- version ?? '',
- abortCtrl.signal
- );
- if (!didCancel) {
- dispatch({ type: 'FETCH_UPDATE_CASE_SUCCESS' });
- fetchCases(state.filterOptions, state.queryParams);
- refetchCasesStatus();
- }
- } catch (error) {
- if (!didCancel) {
+
+ await patchCase(
+ caseId,
+ { [updateKey]: updateValue },
+ // saved object versions are typed as string | undefined, hope that's not true
+ version ?? '',
+ abortCtrlUpdateCases.current.signal
+ );
+
+ if (!didCancelUpdateCases.current) {
+ dispatch({ type: 'FETCH_UPDATE_CASE_SUCCESS' });
+ fetchCases(state.filterOptions, state.queryParams);
+ refetchCasesStatus();
+ }
+ } catch (error) {
+ if (!didCancelUpdateCases.current) {
+ if (error.name !== 'AbortError') {
errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster });
- dispatch({ type: 'FETCH_FAILURE', payload: 'caseUpdate' });
}
+ dispatch({ type: 'FETCH_FAILURE', payload: 'caseUpdate' });
}
- };
- fetchData();
- return () => {
- abortCtrl.abort();
- didCancel = true;
- };
+ }
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[state.filterOptions, state.queryParams]
@@ -237,6 +229,17 @@ export const useGetCases = (initialQueryParams?: QueryParams): UseGetCases => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state.filterOptions, state.queryParams]);
+ useEffect(() => {
+ fetchCases(state.filterOptions, state.queryParams);
+ return () => {
+ didCancelFetchCases.current = true;
+ didCancelUpdateCases.current = true;
+ abortCtrlFetchCases.current.abort();
+ abortCtrlUpdateCases.current.abort();
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [state.queryParams, state.filterOptions]);
+
return {
...state,
dispatchUpdateCaseProperty,
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases_status.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases_status.tsx
index 057fc05008bb0..087f7ef455cba 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases_status.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases_status.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useState, useRef } from 'react';
import { errorToToaster, useStateToaster } from '../../common/components/toasters';
import { getCasesStatus } from './api';
@@ -32,51 +32,56 @@ export interface UseGetCasesStatus extends CasesStatusState {
export const useGetCasesStatus = (): UseGetCasesStatus => {
const [casesStatusState, setCasesStatusState] = useState(initialData);
const [, dispatchToaster] = useStateToaster();
+ const isCancelledRef = useRef(false);
+ const abortCtrlRef = useRef(new AbortController());
- const fetchCasesStatus = useCallback(() => {
- let didCancel = false;
- const abortCtrl = new AbortController();
- const fetchData = async () => {
+ const fetchCasesStatus = useCallback(async () => {
+ try {
+ isCancelledRef.current = false;
+ abortCtrlRef.current.abort();
+ abortCtrlRef.current = new AbortController();
setCasesStatusState({
- ...casesStatusState,
+ ...initialData,
isLoading: true,
});
- try {
- const response = await getCasesStatus(abortCtrl.signal);
- if (!didCancel) {
- setCasesStatusState({
- ...response,
- isLoading: false,
- isError: false,
- });
- }
- } catch (error) {
- if (!didCancel) {
+
+ const response = await getCasesStatus(abortCtrlRef.current.signal);
+
+ if (!isCancelledRef.current) {
+ setCasesStatusState({
+ ...response,
+ isLoading: false,
+ isError: false,
+ });
+ }
+ } catch (error) {
+ if (!isCancelledRef.current) {
+ if (error.name !== 'AbortError') {
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
- setCasesStatusState({
- countClosedCases: 0,
- countInProgressCases: 0,
- countOpenCases: 0,
- isLoading: false,
- isError: true,
- });
}
+ setCasesStatusState({
+ countClosedCases: 0,
+ countInProgressCases: 0,
+ countOpenCases: 0,
+ isLoading: false,
+ isError: true,
+ });
}
- };
- fetchData();
- return () => {
- didCancel = true;
- abortCtrl.abort();
- };
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [casesStatusState]);
+ }, []);
useEffect(() => {
fetchCasesStatus();
+
+ return () => {
+ isCancelledRef.current = true;
+ abortCtrlRef.current.abort();
+ };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_reporters.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_reporters.tsx
index 25c483045b84f..f2c33ec4730fe 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_get_reporters.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_reporters.tsx
@@ -5,9 +5,9 @@
* 2.0.
*/
-import { useCallback, useEffect, useState } from 'react';
-
+import { useCallback, useEffect, useState, useRef } from 'react';
import { isEmpty } from 'lodash/fp';
+
import { User } from '../../../../case/common/api';
import { errorToToaster, useStateToaster } from '../../common/components/toasters';
import { getReporters } from './api';
@@ -35,57 +35,61 @@ export const useGetReporters = (): UseGetReporters => {
const [reportersState, setReporterState] = useState(initialData);
const [, dispatchToaster] = useStateToaster();
+ const isCancelledRef = useRef(false);
+ const abortCtrlRef = useRef(new AbortController());
- const fetchReporters = useCallback(() => {
- let didCancel = false;
- const abortCtrl = new AbortController();
- const fetchData = async () => {
+ const fetchReporters = useCallback(async () => {
+ try {
+ isCancelledRef.current = false;
+ abortCtrlRef.current.abort();
+ abortCtrlRef.current = new AbortController();
setReporterState({
...reportersState,
isLoading: true,
});
- try {
- const response = await getReporters(abortCtrl.signal);
- const myReporters = response
- .map((r) =>
- r.full_name == null || isEmpty(r.full_name) ? r.username ?? '' : r.full_name
- )
- .filter((u) => !isEmpty(u));
- if (!didCancel) {
- setReporterState({
- reporters: myReporters,
- respReporters: response,
- isLoading: false,
- isError: false,
- });
- }
- } catch (error) {
- if (!didCancel) {
+
+ const response = await getReporters(abortCtrlRef.current.signal);
+ const myReporters = response
+ .map((r) => (r.full_name == null || isEmpty(r.full_name) ? r.username ?? '' : r.full_name))
+ .filter((u) => !isEmpty(u));
+
+ if (!isCancelledRef.current) {
+ setReporterState({
+ reporters: myReporters,
+ respReporters: response,
+ isLoading: false,
+ isError: false,
+ });
+ }
+ } catch (error) {
+ if (!isCancelledRef.current) {
+ if (error.name !== 'AbortError') {
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
- setReporterState({
- reporters: [],
- respReporters: [],
- isLoading: false,
- isError: true,
- });
}
+
+ setReporterState({
+ reporters: [],
+ respReporters: [],
+ isLoading: false,
+ isError: true,
+ });
}
- };
- fetchData();
- return () => {
- didCancel = true;
- abortCtrl.abort();
- };
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [reportersState]);
useEffect(() => {
fetchReporters();
+ return () => {
+ isCancelledRef.current = true;
+ abortCtrlRef.current.abort();
+ };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+
return { ...reportersState, fetchReporters };
};
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_tags.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_tags.tsx
index 208516d302eb4..4a7a298e2cd86 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_get_tags.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_tags.tsx
@@ -5,8 +5,7 @@
* 2.0.
*/
-import { useEffect, useReducer } from 'react';
-
+import { useEffect, useReducer, useRef, useCallback } from 'react';
import { errorToToaster, useStateToaster } from '../../common/components/toasters';
import { getTags } from './api';
import * as i18n from './translations';
@@ -59,37 +58,42 @@ export const useGetTags = (): UseGetTags => {
tags: initialData,
});
const [, dispatchToaster] = useStateToaster();
+ const isCancelledRef = useRef(false);
+ const abortCtrlRef = useRef(new AbortController());
- const callFetch = () => {
- let didCancel = false;
- const abortCtrl = new AbortController();
-
- const fetchData = async () => {
+ const callFetch = useCallback(async () => {
+ try {
+ isCancelledRef.current = false;
+ abortCtrlRef.current.abort();
+ abortCtrlRef.current = new AbortController();
dispatch({ type: 'FETCH_INIT' });
- try {
- const response = await getTags(abortCtrl.signal);
- if (!didCancel) {
- dispatch({ type: 'FETCH_SUCCESS', payload: response });
- }
- } catch (error) {
- if (!didCancel) {
+
+ const response = await getTags(abortCtrlRef.current.signal);
+
+ if (!isCancelledRef.current) {
+ dispatch({ type: 'FETCH_SUCCESS', payload: response });
+ }
+ } catch (error) {
+ if (!isCancelledRef.current) {
+ if (error.name !== 'AbortError') {
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
- dispatch({ type: 'FETCH_FAILURE' });
}
+ dispatch({ type: 'FETCH_FAILURE' });
}
- };
- fetchData();
- return () => {
- abortCtrl.abort();
- didCancel = true;
- };
- };
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
useEffect(() => {
callFetch();
+ return () => {
+ isCancelledRef.current = true;
+ abortCtrlRef.current.abort();
+ };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return { ...state, fetchTags: callFetch };
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_case.tsx
index c4fa030473534..d890c050f5034 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_post_case.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_case.tsx
@@ -6,7 +6,6 @@
*/
import { useReducer, useCallback, useRef, useEffect } from 'react';
-
import { CasePostRequest } from '../../../../case/common/api';
import { errorToToaster, useStateToaster } from '../../common/components/toasters';
import { postCase } from './api';
@@ -51,38 +50,41 @@ export const usePostCase = (): UsePostCase => {
isError: false,
});
const [, dispatchToaster] = useStateToaster();
- const cancel = useRef(false);
- const abortCtrl = useRef(new AbortController());
- const postMyCase = useCallback(
- async (data: CasePostRequest) => {
- try {
- dispatch({ type: 'FETCH_INIT' });
- abortCtrl.current.abort();
- cancel.current = false;
- abortCtrl.current = new AbortController();
- const response = await postCase(data, abortCtrl.current.signal);
- if (!cancel.current) {
- dispatch({ type: 'FETCH_SUCCESS' });
- }
- return response;
- } catch (error) {
- if (!cancel.current) {
+ const isCancelledRef = useRef(false);
+ const abortCtrlRef = useRef(new AbortController());
+
+ const postMyCase = useCallback(async (data: CasePostRequest) => {
+ try {
+ isCancelledRef.current = false;
+ abortCtrlRef.current.abort();
+ abortCtrlRef.current = new AbortController();
+
+ dispatch({ type: 'FETCH_INIT' });
+ const response = await postCase(data, abortCtrlRef.current.signal);
+
+ if (!isCancelledRef.current) {
+ dispatch({ type: 'FETCH_SUCCESS' });
+ }
+ return response;
+ } catch (error) {
+ if (!isCancelledRef.current) {
+ if (error.name !== 'AbortError') {
errorToToaster({
title: i18n.ERROR_TITLE,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
- dispatch({ type: 'FETCH_FAILURE' });
}
+ dispatch({ type: 'FETCH_FAILURE' });
}
- },
- [dispatchToaster]
- );
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
useEffect(() => {
return () => {
- abortCtrl.current.abort();
- cancel.current = true;
+ isCancelledRef.current = true;
+ abortCtrlRef.current.abort();
};
}, []);
return { ...state, postCase: postMyCase };
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx
index 8fc8053c14f70..5eb875287ba88 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx
@@ -5,8 +5,7 @@
* 2.0.
*/
-import { useReducer, useCallback } from 'react';
-
+import { useReducer, useCallback, useRef, useEffect } from 'react';
import { CommentRequest } from '../../../../case/common/api';
import { errorToToaster, useStateToaster } from '../../common/components/toasters';
@@ -58,38 +57,47 @@ export const usePostComment = (): UsePostComment => {
isError: false,
});
const [, dispatchToaster] = useStateToaster();
+ const isCancelledRef = useRef(false);
+ const abortCtrlRef = useRef(new AbortController());
const postMyComment = useCallback(
async ({ caseId, data, updateCase, subCaseId }: PostComment) => {
- let cancel = false;
- const abortCtrl = new AbortController();
-
try {
+ isCancelledRef.current = false;
+ abortCtrlRef.current.abort();
+ abortCtrlRef.current = new AbortController();
dispatch({ type: 'FETCH_INIT' });
- const response = await postComment(data, caseId, abortCtrl.signal, subCaseId);
- if (!cancel) {
+
+ const response = await postComment(data, caseId, abortCtrlRef.current.signal, subCaseId);
+
+ if (!isCancelledRef.current) {
dispatch({ type: 'FETCH_SUCCESS' });
if (updateCase) {
updateCase(response);
}
}
} catch (error) {
- if (!cancel) {
- errorToToaster({
- title: i18n.ERROR_TITLE,
- error: error.body && error.body.message ? new Error(error.body.message) : error,
- dispatchToaster,
- });
+ if (!isCancelledRef.current) {
+ if (error.name !== 'AbortError') {
+ errorToToaster({
+ title: i18n.ERROR_TITLE,
+ error: error.body && error.body.message ? new Error(error.body.message) : error,
+ dispatchToaster,
+ });
+ }
dispatch({ type: 'FETCH_FAILURE' });
}
}
- return () => {
- abortCtrl.abort();
- cancel = true;
- };
},
[dispatchToaster]
);
+ useEffect(() => {
+ return () => {
+ isCancelledRef.current = true;
+ abortCtrlRef.current.abort();
+ };
+ }, []);
+
return { ...state, postComment: postMyComment };
};
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx
index 03d881d7934e9..27a02d9300cc0 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx
@@ -67,17 +67,17 @@ export const usePostPushToService = (): UsePostPushToService => {
});
const [, dispatchToaster] = useStateToaster();
const cancel = useRef(false);
- const abortCtrl = useRef(new AbortController());
+ const abortCtrlRef = useRef(new AbortController());
const pushCaseToExternalService = useCallback(
async ({ caseId, connector }: PushToServiceRequest) => {
try {
- dispatch({ type: 'FETCH_INIT' });
- abortCtrl.current.abort();
+ abortCtrlRef.current.abort();
cancel.current = false;
- abortCtrl.current = new AbortController();
+ abortCtrlRef.current = new AbortController();
+ dispatch({ type: 'FETCH_INIT' });
- const response = await pushCase(caseId, connector.id, abortCtrl.current.signal);
+ const response = await pushCase(caseId, connector.id, abortCtrlRef.current.signal);
if (!cancel.current) {
dispatch({ type: 'FETCH_SUCCESS' });
@@ -90,11 +90,13 @@ export const usePostPushToService = (): UsePostPushToService => {
return response;
} catch (error) {
if (!cancel.current) {
- errorToToaster({
- title: i18n.ERROR_TITLE,
- error: error.body && error.body.message ? new Error(error.body.message) : error,
- dispatchToaster,
- });
+ if (error.name !== 'AbortError') {
+ errorToToaster({
+ title: i18n.ERROR_TITLE,
+ error: error.body && error.body.message ? new Error(error.body.message) : error,
+ dispatchToaster,
+ });
+ }
dispatch({ type: 'FETCH_FAILURE' });
}
}
@@ -105,7 +107,7 @@ export const usePostPushToService = (): UsePostPushToService => {
useEffect(() => {
return () => {
- abortCtrl.current.abort();
+ abortCtrlRef.current.abort();
cancel.current = true;
};
}, []);
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx
index 23a23caeb71bd..e8de2257009e6 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx
@@ -5,10 +5,9 @@
* 2.0.
*/
-import { useReducer, useCallback, useEffect, useRef } from 'react';
+import { useReducer, useCallback, useRef, useEffect } from 'react';
import { errorToToaster, useStateToaster } from '../../common/components/toasters';
-
import { patchCase, patchSubCase } from './api';
import { UpdateKey, UpdateByKey, CaseStatuses } from './types';
import * as i18n from './translations';
@@ -70,8 +69,8 @@ export const useUpdateCase = ({
updateKey: null,
});
const [, dispatchToaster] = useStateToaster();
- const abortCtrl = useRef(new AbortController());
- const didCancel = useRef(false);
+ const isCancelledRef = useRef(false);
+ const abortCtrlRef = useRef(new AbortController());
const dispatchUpdateCaseProperty = useCallback(
async ({
@@ -84,24 +83,27 @@ export const useUpdateCase = ({
onError,
}: UpdateByKey) => {
try {
- didCancel.current = false;
- abortCtrl.current = new AbortController();
+ isCancelledRef.current = false;
+ abortCtrlRef.current.abort();
+ abortCtrlRef.current = new AbortController();
dispatch({ type: 'FETCH_INIT', payload: updateKey });
+
const response = await (updateKey === 'status' && subCaseId
? patchSubCase(
caseId,
subCaseId,
{ status: updateValue as CaseStatuses },
caseData.version,
- abortCtrl.current.signal
+ abortCtrlRef.current.signal
)
: patchCase(
caseId,
{ [updateKey]: updateValue },
caseData.version,
- abortCtrl.current.signal
+ abortCtrlRef.current.signal
));
- if (!didCancel.current) {
+
+ if (!isCancelledRef.current) {
if (fetchCaseUserActions != null) {
fetchCaseUserActions(caseId, subCaseId);
}
@@ -119,7 +121,7 @@ export const useUpdateCase = ({
}
}
} catch (error) {
- if (!didCancel.current) {
+ if (!isCancelledRef.current) {
if (error.name !== 'AbortError') {
errorToToaster({
title: i18n.ERROR_TITLE,
@@ -140,8 +142,8 @@ export const useUpdateCase = ({
useEffect(() => {
return () => {
- didCancel.current = true;
- abortCtrl.current.abort();
+ isCancelledRef.current = true;
+ abortCtrlRef.current.abort();
};
}, []);
diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx
index e36b21823310e..81bce248852fe 100644
--- a/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx
+++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx
@@ -5,10 +5,8 @@
* 2.0.
*/
-import { useReducer, useCallback } from 'react';
-
+import { useReducer, useCallback, useRef, useEffect } from 'react';
import { errorToToaster, useStateToaster } from '../../common/components/toasters';
-
import { patchComment } from './api';
import * as i18n from './translations';
import { Case } from './types';
@@ -72,6 +70,8 @@ export const useUpdateComment = (): UseUpdateComment => {
isError: false,
});
const [, dispatchToaster] = useStateToaster();
+ const isCancelledRef = useRef(false);
+ const abortCtrlRef = useRef(new AbortController());
const dispatchUpdateComment = useCallback(
async ({
@@ -83,41 +83,49 @@ export const useUpdateComment = (): UseUpdateComment => {
updateCase,
version,
}: UpdateComment) => {
- let cancel = false;
- const abortCtrl = new AbortController();
try {
+ isCancelledRef.current = false;
+ abortCtrlRef.current.abort();
+ abortCtrlRef.current = new AbortController();
dispatch({ type: 'FETCH_INIT', payload: commentId });
+
const response = await patchComment(
caseId,
commentId,
commentUpdate,
version,
- abortCtrl.signal,
+ abortCtrlRef.current.signal,
subCaseId
);
- if (!cancel) {
+
+ if (!isCancelledRef.current) {
updateCase(response);
fetchUserActions();
dispatch({ type: 'FETCH_SUCCESS', payload: { commentId } });
}
} catch (error) {
- if (!cancel) {
- errorToToaster({
- title: i18n.ERROR_TITLE,
- error: error.body && error.body.message ? new Error(error.body.message) : error,
- dispatchToaster,
- });
+ if (!isCancelledRef.current) {
+ if (error.name !== 'AbortError') {
+ errorToToaster({
+ title: i18n.ERROR_TITLE,
+ error: error.body && error.body.message ? new Error(error.body.message) : error,
+ dispatchToaster,
+ });
+ }
dispatch({ type: 'FETCH_FAILURE', payload: commentId });
}
}
- return () => {
- cancel = true;
- abortCtrl.abort();
- };
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
+ useEffect(() => {
+ return () => {
+ isCancelledRef.current = true;
+ abortCtrlRef.current.abort();
+ };
+ }, []);
+
return { ...state, patchComment: dispatchUpdateComment };
};
diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx
index 11f424d83c530..a6dd64737f5ee 100644
--- a/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx
@@ -8,14 +8,15 @@
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { mount } from 'enzyme';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { AndOrBadge } from './';
+const mockTheme = { eui: { euiColorLightShade: '#ece' } };
+
describe('AndOrBadge', () => {
test('it renders top and bottom antenna bars when "includeAntennas" is true', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
);
@@ -27,7 +28,7 @@ describe('AndOrBadge', () => {
test('it does not render top and bottom antenna bars when "includeAntennas" is false', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
);
@@ -39,7 +40,7 @@ describe('AndOrBadge', () => {
test('it renders "and" when "type" is "and"', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
);
@@ -49,7 +50,7 @@ describe('AndOrBadge', () => {
test('it renders "or" when "type" is "or"', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
);
diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.test.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.test.tsx
index 5bc89baa3d415..489d02990b1f4 100644
--- a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.test.tsx
@@ -6,29 +6,19 @@
*/
import React from 'react';
-import { ThemeProvider } from 'styled-components';
import { mount } from 'enzyme';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { RoundedBadge } from './rounded_badge';
describe('RoundedBadge', () => {
test('it renders "and" when "type" is "and"', () => {
- const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
- );
+ const wrapper = mount( );
expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('AND');
});
test('it renders "or" when "type" is "or"', () => {
- const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
- );
+ const wrapper = mount( );
expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('OR');
});
diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.test.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.test.tsx
index 61cf44293005f..c6536a05be45d 100644
--- a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.test.tsx
@@ -8,14 +8,15 @@
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { mount } from 'enzyme';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { RoundedBadgeAntenna } from './rounded_badge_antenna';
+const mockTheme = { eui: { euiColorLightShade: '#ece' } };
+
describe('RoundedBadgeAntenna', () => {
test('it renders top and bottom antenna bars', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
);
@@ -27,7 +28,7 @@ describe('RoundedBadgeAntenna', () => {
test('it renders "and" when "type" is "and"', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
);
@@ -37,7 +38,7 @@ describe('RoundedBadgeAntenna', () => {
test('it renders "or" when "type" is "or"', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
);
diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx
index 6a3d82d47045a..79e6fe5506b84 100644
--- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx
@@ -6,9 +6,7 @@
*/
import React from 'react';
-import { ThemeProvider } from 'styled-components';
import { mount } from 'enzyme';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import {
@@ -20,21 +18,19 @@ import { FieldComponent } from './field';
describe('FieldComponent', () => {
test('it renders disabled if "isDisabled" is true', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -44,21 +40,19 @@ describe('FieldComponent', () => {
test('it renders loading if "isLoading" is true', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper.find(`[data-test-subj="fieldAutocompleteComboBox"] button`).at(0).simulate('click');
expect(
@@ -70,21 +64,19 @@ describe('FieldComponent', () => {
test('it allows user to clear values if "isClearable" is true', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -96,21 +88,19 @@ describe('FieldComponent', () => {
test('it correctly displays selected field', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -121,21 +111,19 @@ describe('FieldComponent', () => {
test('it invokes "onChange" when option selected', () => {
const mockOnChange = jest.fn();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
((wrapper.find(EuiComboBox).props() as unknown) as {
diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.test.tsx
index f577799827b89..b6300581f12dd 100644
--- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.test.tsx
@@ -6,19 +6,13 @@
*/
import React from 'react';
-import { ThemeProvider } from 'styled-components';
import { mount } from 'enzyme';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { AutocompleteFieldExistsComponent } from './field_value_exists';
describe('AutocompleteFieldExistsComponent', () => {
test('it renders field disabled', () => {
- const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
- );
+ const wrapper = mount( );
expect(
wrapper
diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx
index e01cc5ff1e042..c605a71c50e33 100644
--- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx
@@ -6,9 +6,7 @@
*/
import React from 'react';
-import { ThemeProvider } from 'styled-components';
import { mount } from 'enzyme';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { waitFor } from '@testing-library/react';
@@ -47,17 +45,15 @@ jest.mock('../../../lists_plugin_deps', () => {
describe('AutocompleteFieldListsComponent', () => {
test('it renders disabled if "isDisabled" is true', async () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -69,17 +65,15 @@ describe('AutocompleteFieldListsComponent', () => {
test('it renders loading if "isLoading" is true', async () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper
@@ -97,17 +91,15 @@ describe('AutocompleteFieldListsComponent', () => {
test('it allows user to clear values if "isClearable" is true', async () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
wrapper
@@ -118,17 +110,15 @@ describe('AutocompleteFieldListsComponent', () => {
test('it correctly displays lists that match the selected "keyword" field esType', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper.find('[data-test-subj="comboBoxToggleListButton"] button').simulate('click');
@@ -142,17 +132,15 @@ describe('AutocompleteFieldListsComponent', () => {
test('it correctly displays lists that match the selected "ip" field esType', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper.find('[data-test-subj="comboBoxToggleListButton"] button').simulate('click');
@@ -166,17 +154,15 @@ describe('AutocompleteFieldListsComponent', () => {
test('it correctly displays selected list', async () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -190,17 +176,15 @@ describe('AutocompleteFieldListsComponent', () => {
test('it invokes "onChange" when option selected', async () => {
const mockOnChange = jest.fn();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
((wrapper.find(EuiComboBox).props() as unknown) as {
diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx
index d4712092867e9..38d103fe65130 100644
--- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx
@@ -6,9 +6,7 @@
*/
import React from 'react';
-import { ThemeProvider } from 'styled-components';
import { mount, ReactWrapper } from 'enzyme';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { EuiSuperSelect, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { act } from '@testing-library/react';
@@ -44,23 +42,21 @@ describe('AutocompleteFieldMatchComponent', () => {
test('it renders row label if one passed in', () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -70,23 +66,21 @@ describe('AutocompleteFieldMatchComponent', () => {
test('it renders disabled if "isDisabled" is true', () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -96,23 +90,21 @@ describe('AutocompleteFieldMatchComponent', () => {
test('it renders loading if "isLoading" is true', () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper.find('[data-test-subj="valuesAutocompleteMatch"] button').at(0).simulate('click');
expect(
@@ -124,23 +116,21 @@ describe('AutocompleteFieldMatchComponent', () => {
test('it allows user to clear values if "isClearable" is true', () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -152,23 +142,21 @@ describe('AutocompleteFieldMatchComponent', () => {
test('it correctly displays selected value', () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -179,23 +167,21 @@ describe('AutocompleteFieldMatchComponent', () => {
test('it invokes "onChange" when new value created', async () => {
const mockOnChange = jest.fn();
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
((wrapper.find(EuiComboBox).props() as unknown) as {
@@ -208,23 +194,21 @@ describe('AutocompleteFieldMatchComponent', () => {
test('it invokes "onChange" when new value selected', async () => {
const mockOnChange = jest.fn();
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
((wrapper.find(EuiComboBox).props() as unknown) as {
@@ -236,23 +220,21 @@ describe('AutocompleteFieldMatchComponent', () => {
test('it refreshes autocomplete with search query when new value searched', () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
act(() => {
((wrapper.find(EuiComboBox).props() as unknown) as {
@@ -287,23 +269,21 @@ describe('AutocompleteFieldMatchComponent', () => {
test('it displays only two options - "true" or "false"', () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -326,23 +306,21 @@ describe('AutocompleteFieldMatchComponent', () => {
test('it invokes "onChange" with "true" when selected', () => {
const mockOnChange = jest.fn();
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
((wrapper.find(EuiSuperSelect).props() as unknown) as {
@@ -355,23 +333,21 @@ describe('AutocompleteFieldMatchComponent', () => {
test('it invokes "onChange" with "false" when selected', () => {
const mockOnChange = jest.fn();
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
((wrapper.find(EuiSuperSelect).props() as unknown) as {
@@ -396,23 +372,21 @@ describe('AutocompleteFieldMatchComponent', () => {
test('it number input when field type is number', () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -423,23 +397,21 @@ describe('AutocompleteFieldMatchComponent', () => {
test('it invokes "onChange" with numeric value when inputted', () => {
const mockOnChange = jest.fn();
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper
diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx
index aa2038262f40c..6b479c5ab8c4c 100644
--- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx
@@ -6,9 +6,7 @@
*/
import React from 'react';
-import { ThemeProvider } from 'styled-components';
import { mount, ReactWrapper } from 'enzyme';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { act } from '@testing-library/react';
@@ -43,24 +41,22 @@ describe('AutocompleteFieldMatchAnyComponent', () => {
test('it renders disabled if "isDisabled" is true', () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -70,24 +66,22 @@ describe('AutocompleteFieldMatchAnyComponent', () => {
test('it renders loading if "isLoading" is true', () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper.find(`[data-test-subj="valuesAutocompleteMatchAny"] button`).at(0).simulate('click');
expect(
@@ -99,24 +93,22 @@ describe('AutocompleteFieldMatchAnyComponent', () => {
test('it allows user to clear values if "isClearable" is true', () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -128,24 +120,22 @@ describe('AutocompleteFieldMatchAnyComponent', () => {
test('it correctly displays selected value', () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -156,24 +146,22 @@ describe('AutocompleteFieldMatchAnyComponent', () => {
test('it invokes "onChange" when new value created', async () => {
const mockOnChange = jest.fn();
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
((wrapper.find(EuiComboBox).props() as unknown) as {
@@ -186,24 +174,22 @@ describe('AutocompleteFieldMatchAnyComponent', () => {
test('it invokes "onChange" when new value selected', async () => {
const mockOnChange = jest.fn();
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
((wrapper.find(EuiComboBox).props() as unknown) as {
@@ -215,23 +201,21 @@ describe('AutocompleteFieldMatchAnyComponent', () => {
test('it refreshes autocomplete with search query when new value searched', () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
act(() => {
((wrapper.find(EuiComboBox).props() as unknown) as {
diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.test.tsx
index 56ae6d762e7ee..db16cbde2acb4 100644
--- a/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.test.tsx
@@ -6,9 +6,7 @@
*/
import React from 'react';
-import { ThemeProvider } from 'styled-components';
import { mount } from 'enzyme';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
@@ -18,17 +16,15 @@ import { isOperator, isNotOperator } from './operators';
describe('OperatorComponent', () => {
test('it renders disabled if "isDisabled" is true', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -38,17 +34,15 @@ describe('OperatorComponent', () => {
test('it renders loading if "isLoading" is true', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper.find(`[data-test-subj="operatorAutocompleteComboBox"] button`).at(0).simulate('click');
expect(
@@ -60,17 +54,15 @@ describe('OperatorComponent', () => {
test('it allows user to clear values if "isClearable" is true', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(wrapper.find(`button[data-test-subj="comboBoxClearButton"]`).exists()).toBeTruthy();
@@ -78,18 +70,16 @@ describe('OperatorComponent', () => {
test('it displays "operatorOptions" if param is passed in with items', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -99,18 +89,16 @@ describe('OperatorComponent', () => {
test('it does not display "operatorOptions" if param is passed in with no items', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -145,17 +133,15 @@ describe('OperatorComponent', () => {
test('it correctly displays selected operator', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -165,27 +151,25 @@ describe('OperatorComponent', () => {
test('it only displays subset of operators if field type is nested', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -195,17 +179,15 @@ describe('OperatorComponent', () => {
test('it only displays subset of operators if field type is boolean', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -221,17 +203,15 @@ describe('OperatorComponent', () => {
test('it invokes "onChange" when option selected', () => {
const mockOnChange = jest.fn();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
((wrapper.find(EuiComboBox).props() as unknown) as {
diff --git a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx
index 096bea37566b3..6d87b5d3a68b9 100644
--- a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx
@@ -6,10 +6,8 @@
*/
import { Chart, BarSeries, Axis, ScaleType } from '@elastic/charts';
-import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import { mount, ReactWrapper, shallow, ShallowWrapper } from 'enzyme';
import React from 'react';
-import { ThemeProvider } from 'styled-components';
import { escapeDataProviderId } from '../drag_and_drop/helpers';
import { TestProviders } from '../../mock';
@@ -37,8 +35,6 @@ jest.mock('uuid', () => {
};
});
-const theme = () => ({ eui: euiDarkVars, darkMode: true });
-
const customHeight = '100px';
const customWidth = '120px';
const chartDataSets = [
@@ -323,11 +319,9 @@ describe.each(chartDataSets)('BarChart with stackByField', () => {
beforeAll(() => {
wrapper = mount(
-
-
-
-
-
+
+
+
);
});
@@ -407,11 +401,9 @@ describe.each(chartDataSets)('BarChart with custom color', () => {
beforeAll(() => {
wrapper = mount(
-
-
-
-
-
+
+
+
);
});
diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx
index fb91a4a3ce92b..544f9b1abf8f2 100644
--- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx
@@ -5,10 +5,8 @@
* 2.0.
*/
-import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import { mount, ReactWrapper } from 'enzyme';
import React from 'react';
-import { ThemeProvider } from 'styled-components';
import '../../mock/match_media';
import '../../mock/react_beautiful_dnd';
@@ -26,8 +24,6 @@ jest.mock('@elastic/eui', () => {
};
});
-const theme = () => ({ eui: euiDarkVars, darkMode: true });
-
const allOthersDataProviderId =
'draggable-legend-item-527adabe-8e1c-4a1f-965c-2f3d65dda9e1-event_dataset-All others';
@@ -74,11 +70,9 @@ describe('DraggableLegend', () => {
beforeEach(() => {
wrapper = mount(
-
-
-
-
-
+
+
+
);
});
@@ -120,11 +114,9 @@ describe('DraggableLegend', () => {
it('does NOT render the legend when an empty collection of legendItems is provided', () => {
const wrapper = mount(
-
-
-
-
-
+
+
+
);
expect(wrapper.find('[data-test-subj="draggable-legend"]').exists()).toBe(false);
@@ -132,11 +124,9 @@ describe('DraggableLegend', () => {
it(`renders a legend with the minimum height when 'height' is zero`, () => {
const wrapper = mount(
-
-
-
-
-
+
+
+
);
expect(wrapper.find('[data-test-subj="draggable-legend"]').first()).toHaveStyleRule(
diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx
index 15c164e59557d..4958f6bae4a30 100644
--- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx
@@ -5,10 +5,8 @@
* 2.0.
*/
-import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import { mount, ReactWrapper } from 'enzyme';
import React from 'react';
-import { ThemeProvider } from 'styled-components';
import '../../mock/match_media';
import '../../mock/react_beautiful_dnd';
@@ -25,8 +23,6 @@ jest.mock('@elastic/eui', () => {
};
});
-const theme = () => ({ eui: euiDarkVars, darkMode: true });
-
describe('DraggableLegendItem', () => {
describe('rendering a regular (non "All others") legend item', () => {
const legendItem: LegendItem = {
@@ -41,11 +37,9 @@ describe('DraggableLegendItem', () => {
beforeEach(() => {
wrapper = mount(
-
-
-
-
-
+
+
+
);
});
@@ -79,11 +73,9 @@ describe('DraggableLegendItem', () => {
beforeEach(() => {
wrapper = mount(
-
-
-
-
-
+
+
+
);
});
@@ -118,11 +110,9 @@ describe('DraggableLegendItem', () => {
};
const wrapper = mount(
-
-
-
-
-
+
+
+
);
expect(wrapper.find('[data-test-subj="legend-color"]').exists()).toBe(false);
diff --git a/x-pack/plugins/security_solution/public/common/components/empty_value/empty_value.test.tsx b/x-pack/plugins/security_solution/public/common/components/empty_value/empty_value.test.tsx
index 764d9109816b5..e3c74bf425628 100644
--- a/x-pack/plugins/security_solution/public/common/components/empty_value/empty_value.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/empty_value/empty_value.test.tsx
@@ -5,7 +5,6 @@
* 2.0.
*/
-import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import { mount, shallow } from 'enzyme';
import React from 'react';
import { ThemeProvider } from 'styled-components';
@@ -21,7 +20,7 @@ import {
} from '.';
describe('EmptyValue', () => {
- const theme = () => ({ eui: euiDarkVars, darkMode: true });
+ const mockTheme = { eui: { euiColorMediumShade: '#ece' } };
test('it renders against snapshot', () => {
const wrapper = shallow({getEmptyString()}
);
@@ -35,7 +34,7 @@ describe('EmptyValue', () => {
describe('#getEmptyString', () => {
test('should turn into an empty string place holder', () => {
const wrapper = mountWithIntl(
-
+
{getEmptyString()}
);
@@ -45,7 +44,7 @@ describe('EmptyValue', () => {
describe('#getEmptyTagValue', () => {
const wrapper = mount(
-
+
{getEmptyTagValue()}
);
@@ -55,7 +54,7 @@ describe('EmptyValue', () => {
describe('#getEmptyStringTag', () => {
test('should turn into an span that has length of 1', () => {
const wrapper = mountWithIntl(
-
+
{getEmptyStringTag()}
);
@@ -64,7 +63,7 @@ describe('EmptyValue', () => {
test('should turn into an empty string tag place holder', () => {
const wrapper = mountWithIntl(
-
+
{getEmptyStringTag()}
);
@@ -75,7 +74,7 @@ describe('EmptyValue', () => {
describe('#defaultToEmptyTag', () => {
test('should default to an empty value when a value is null', () => {
const wrapper = mount(
-
+
{defaultToEmptyTag(null)}
);
@@ -84,7 +83,7 @@ describe('EmptyValue', () => {
test('should default to an empty value when a value is undefined', () => {
const wrapper = mount(
-
+
{defaultToEmptyTag(undefined)}
);
@@ -114,7 +113,7 @@ describe('EmptyValue', () => {
},
};
const wrapper = mount(
-
+
{getOrEmptyTag('a.b.c', test)}
);
@@ -130,7 +129,7 @@ describe('EmptyValue', () => {
},
};
const wrapper = mount(
-
+
{getOrEmptyTag('a.b.c', test)}
);
@@ -144,7 +143,7 @@ describe('EmptyValue', () => {
},
};
const wrapper = mount(
-
+
{getOrEmptyTag('a.b.c', test)}
);
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx
index af76a79f0e330..9ba6fe104be45 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx
@@ -8,7 +8,6 @@
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { mount, ReactWrapper } from 'enzyme';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { waitFor } from '@testing-library/react';
import { AddExceptionModal } from './';
@@ -32,6 +31,17 @@ import {
import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async';
import { AlertData } from '../types';
+const mockTheme = {
+ eui: {
+ euiBreakpoints: {
+ l: '1200px',
+ },
+ paddingSizes: {
+ m: '10px',
+ },
+ },
+};
+
jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index');
jest.mock('../../../../common/lib/kibana');
jest.mock('../../../containers/source');
@@ -101,7 +111,7 @@ describe('When the add exception modal is opened', () => {
},
]);
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
let wrapper: ReactWrapper;
beforeEach(async () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
file: { path: 'test/path' },
};
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
file: { path: 'test/path' },
};
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
file: { path: 'test/path' },
};
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
file: { path: 'test/path' },
};
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('when there are exception builder errors submit button is disabled', async () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
- {i18n.CANCEL}
+
+ {i18n.CANCEL}
+
{
test('it renders exceptionItemEntryFirstRowAndBadge for very first exception item in builder', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
);
@@ -27,7 +28,7 @@ describe('BuilderAndBadgeComponent', () => {
test('it renders exceptionItemEntryInvisibleAndBadge if "entriesLength" is 1 or less', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
);
@@ -39,7 +40,7 @@ describe('BuilderAndBadgeComponent', () => {
test('it renders regular "and" badge if exception item is not the first one and includes more than one entry', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
);
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx
index da236d130e930..9c9035d7e66e9 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx
@@ -58,6 +58,7 @@ describe('BuilderEntryItem', () => {
wrapper = mount(
{
wrapper = mount(
{
wrapper = mount(
{
wrapper = mount(
{
wrapper = mount(
{
wrapper = mount(
{
wrapper = mount(
{
wrapper = mount(
{
wrapper = mount(
{
wrapper = mount(
{
wrapper = mount(
{
}).onChange([{ label: 'machine.os' }]);
expect(mockOnChange).toHaveBeenCalledWith(
- { field: 'machine.os', operator: 'included', type: 'match', value: '' },
+ { id: '123', field: 'machine.os', operator: 'included', type: 'match', value: '' },
0
);
});
@@ -445,6 +456,7 @@ describe('BuilderEntryItem', () => {
wrapper = mount(
{
}).onChange([{ label: 'is not' }]);
expect(mockOnChange).toHaveBeenCalledWith(
- { field: 'ip', operator: 'excluded', type: 'match', value: '1234' },
+ { id: '123', field: 'ip', operator: 'excluded', type: 'match', value: '1234' },
0
);
});
@@ -480,6 +492,7 @@ describe('BuilderEntryItem', () => {
wrapper = mount(
{
}).onCreateOption('126.45.211.34');
expect(mockOnChange).toHaveBeenCalledWith(
- { field: 'ip', operator: 'excluded', type: 'match', value: '126.45.211.34' },
+ { id: '123', field: 'ip', operator: 'excluded', type: 'match', value: '126.45.211.34' },
0
);
});
@@ -515,6 +528,7 @@ describe('BuilderEntryItem', () => {
wrapper = mount(
{
}).onCreateOption('126.45.211.34');
expect(mockOnChange).toHaveBeenCalledWith(
- { field: 'ip', operator: 'included', type: 'match_any', value: ['126.45.211.34'] },
+ { id: '123', field: 'ip', operator: 'included', type: 'match_any', value: ['126.45.211.34'] },
0
);
});
@@ -550,6 +564,7 @@ describe('BuilderEntryItem', () => {
wrapper = mount(
{
expect(mockOnChange).toHaveBeenCalledWith(
{
+ id: '123',
field: 'ip',
operator: 'excluded',
type: 'list',
@@ -590,6 +606,7 @@ describe('BuilderEntryItem', () => {
wrapper = mount(
{
wrapper = mount(
= ({
);
} else {
- return comboBox;
+ return (
+
+ {comboBox}
+
+ );
}
},
[handleFieldChange, indexPattern, entry, listType]
@@ -176,7 +180,11 @@ export const BuilderEntryItem: React.FC = ({
);
} else {
- return comboBox;
+ return (
+
+ {comboBox}
+
+ );
}
};
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx
index 6505c5eb2b310..1ea54473032cc 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx
@@ -8,7 +8,6 @@
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { mount } from 'enzyme';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { useKibana } from '../../../../common/lib/kibana';
import { fields } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
@@ -18,6 +17,12 @@ import { getEntryMatchAnyMock } from '../../../../../../lists/common/schemas/typ
import { BuilderExceptionListItemComponent } from './exception_item';
+const mockTheme = {
+ eui: {
+ euiColorLightShade: '#ece',
+ },
+};
+
jest.mock('../../../../common/lib/kibana');
describe('BuilderExceptionListItemComponent', () => {
@@ -46,7 +51,7 @@ describe('BuilderExceptionListItemComponent', () => {
entries: [getEntryMatchMock(), getEntryMatchMock()],
};
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.entries = [getEntryMatchMock(), getEntryMatchMock()];
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.entries = [getEntryMatchMock()];
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.entries = [getEntryMatchMock()];
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
- {entries.map((item, index) => (
-
-
- {item.nested === 'child' && }
-
- {
+ const key = (item as typeof item & { id?: string }).id ?? `${index}`;
+ return (
+
+
+ {item.nested === 'child' && }
+
+
+
+
-
-
-
-
- ))}
+
+
+ );
+ })}
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx
index ea948ab9b3b5c..8d0f042e7a498 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx
@@ -28,7 +28,15 @@ import {
} from '../../autocomplete/operators';
import { BuilderEntry, ExceptionsBuilderExceptionItem, FormattedBuilderEntry } from '../types';
import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common';
-import { Entry, EntryNested } from '../../../../lists_plugin_deps';
+import {
+ EntryMatch,
+ EntryMatchAny,
+ EntryNested,
+ EntryList,
+ EntryExists,
+ OperatorTypeEnum,
+ OperatorEnum,
+} from '../../../../shared_imports';
import {
getEntryFromOperator,
@@ -46,6 +54,31 @@ import {
getCorrespondingKeywordField,
} from './helpers';
import { OperatorOption } from '../../autocomplete/types';
+import { ENTRIES_WITH_IDS } from '../../../../../../lists/common/constants.mock';
+
+jest.mock('uuid', () => ({
+ v4: jest.fn().mockReturnValue('123'),
+}));
+
+const getEntryNestedWithIdMock = () => ({
+ id: '123',
+ ...getEntryNestedMock(),
+});
+
+const getEntryExistsWithIdMock = () => ({
+ id: '123',
+ ...getEntryExistsMock(),
+});
+
+const getEntryMatchWithIdMock = () => ({
+ id: '123',
+ ...getEntryMatchMock(),
+});
+
+const getEntryMatchAnyWithIdMock = () => ({
+ id: '123',
+ ...getEntryMatchAnyMock(),
+});
const getMockIndexPattern = (): IIndexPattern => ({
id: '1234',
@@ -54,6 +87,7 @@ const getMockIndexPattern = (): IIndexPattern => ({
});
const getMockBuilderEntry = (): FormattedBuilderEntry => ({
+ id: '123',
field: getField('ip'),
operator: isOperator,
value: 'some value',
@@ -64,15 +98,16 @@ const getMockBuilderEntry = (): FormattedBuilderEntry => ({
});
const getMockNestedBuilderEntry = (): FormattedBuilderEntry => ({
+ id: '123',
field: getField('nestedField.child'),
operator: isOperator,
value: 'some value',
nested: 'child',
parent: {
parent: {
- ...getEntryNestedMock(),
+ ...getEntryNestedWithIdMock(),
field: 'nestedField',
- entries: [{ ...getEntryMatchMock(), field: 'child' }],
+ entries: [{ ...getEntryMatchWithIdMock(), field: 'child' }],
},
parentIndex: 0,
},
@@ -81,6 +116,7 @@ const getMockNestedBuilderEntry = (): FormattedBuilderEntry => ({
});
const getMockNestedParentBuilderEntry = (): FormattedBuilderEntry => ({
+ id: '123',
field: { ...getField('nestedField.child'), name: 'nestedField', esTypes: ['nested'] },
operator: isOperator,
value: undefined,
@@ -225,15 +261,16 @@ describe('Exception builder helpers', () => {
test('it returns nested fields that match parent value when "item.nested" is "child"', () => {
const payloadItem: FormattedBuilderEntry = {
+ id: '123',
field: getEndpointField('file.Ext.code_signature.status'),
operator: isOperator,
value: 'some value',
nested: 'child',
parent: {
parent: {
- ...getEntryNestedMock(),
+ ...getEntryNestedWithIdMock(),
field: 'file.Ext.code_signature',
- entries: [{ ...getEntryMatchMock(), field: 'child' }],
+ entries: [{ ...getEntryMatchWithIdMock(), field: 'child' }],
},
parentIndex: 0,
},
@@ -351,7 +388,7 @@ describe('Exception builder helpers', () => {
],
};
const payloadItem: BuilderEntry = {
- ...getEntryMatchMock(),
+ ...getEntryMatchWithIdMock(),
field: 'machine.os.raw.text',
value: 'some os',
};
@@ -363,6 +400,7 @@ describe('Exception builder helpers', () => {
undefined
);
const expected: FormattedBuilderEntry = {
+ id: '123',
entryIndex: 0,
field: {
name: 'machine.os.raw.text',
@@ -385,11 +423,11 @@ describe('Exception builder helpers', () => {
test('it returns "FormattedBuilderEntry" with value "nested" of "child" when "parent" and "parentIndex" are defined', () => {
const payloadIndexPattern: IIndexPattern = getMockIndexPattern();
- const payloadItem: BuilderEntry = { ...getEntryMatchMock(), field: 'child' };
+ const payloadItem: BuilderEntry = { ...getEntryMatchWithIdMock(), field: 'child' };
const payloadParent: EntryNested = {
- ...getEntryNestedMock(),
+ ...getEntryNestedWithIdMock(),
field: 'nestedField',
- entries: [{ ...getEntryMatchMock(), field: 'child' }],
+ entries: [{ ...getEntryMatchWithIdMock(), field: 'child' }],
};
const output = getFormattedBuilderEntry(
payloadIndexPattern,
@@ -399,6 +437,7 @@ describe('Exception builder helpers', () => {
1
);
const expected: FormattedBuilderEntry = {
+ id: '123',
entryIndex: 0,
field: {
aggregatable: false,
@@ -419,9 +458,10 @@ describe('Exception builder helpers', () => {
operator: isOperator,
parent: {
parent: {
+ id: '123',
entries: [{ ...payloadItem }],
field: 'nestedField',
- type: 'nested',
+ type: OperatorTypeEnum.NESTED,
},
parentIndex: 1,
},
@@ -433,7 +473,11 @@ describe('Exception builder helpers', () => {
test('it returns non nested "FormattedBuilderEntry" when "parent" and "parentIndex" are not defined', () => {
const payloadIndexPattern: IIndexPattern = getMockIndexPattern();
- const payloadItem: BuilderEntry = { ...getEntryMatchMock(), field: 'ip', value: 'some ip' };
+ const payloadItem: BuilderEntry = {
+ ...getEntryMatchWithIdMock(),
+ field: 'ip',
+ value: 'some ip',
+ };
const output = getFormattedBuilderEntry(
payloadIndexPattern,
payloadItem,
@@ -442,6 +486,7 @@ describe('Exception builder helpers', () => {
undefined
);
const expected: FormattedBuilderEntry = {
+ id: '123',
entryIndex: 0,
field: {
aggregatable: true,
@@ -465,14 +510,14 @@ describe('Exception builder helpers', () => {
describe('#isEntryNested', () => {
test('it returns "false" if payload is not of type EntryNested', () => {
- const payload: BuilderEntry = getEntryMatchMock();
+ const payload: BuilderEntry = getEntryMatchWithIdMock();
const output = isEntryNested(payload);
const expected = false;
expect(output).toEqual(expected);
});
test('it returns "true if payload is of type EntryNested', () => {
- const payload: EntryNested = getEntryNestedMock();
+ const payload: EntryNested = getEntryNestedWithIdMock();
const output = isEntryNested(payload);
const expected = true;
expect(output).toEqual(expected);
@@ -482,10 +527,11 @@ describe('Exception builder helpers', () => {
describe('#getFormattedBuilderEntries', () => {
test('it returns formatted entry with field undefined if it unable to find a matching index pattern field', () => {
const payloadIndexPattern: IIndexPattern = getMockIndexPattern();
- const payloadItems: BuilderEntry[] = [getEntryMatchMock()];
+ const payloadItems: BuilderEntry[] = [getEntryMatchWithIdMock()];
const output = getFormattedBuilderEntries(payloadIndexPattern, payloadItems);
const expected: FormattedBuilderEntry[] = [
{
+ id: '123',
entryIndex: 0,
field: undefined,
nested: undefined,
@@ -501,12 +547,13 @@ describe('Exception builder helpers', () => {
test('it returns formatted entries when no nested entries exist', () => {
const payloadIndexPattern: IIndexPattern = getMockIndexPattern();
const payloadItems: BuilderEntry[] = [
- { ...getEntryMatchMock(), field: 'ip', value: 'some ip' },
- { ...getEntryMatchAnyMock(), field: 'extension', value: ['some extension'] },
+ { ...getEntryMatchWithIdMock(), field: 'ip', value: 'some ip' },
+ { ...getEntryMatchAnyWithIdMock(), field: 'extension', value: ['some extension'] },
];
const output = getFormattedBuilderEntries(payloadIndexPattern, payloadItems);
const expected: FormattedBuilderEntry[] = [
{
+ id: '123',
entryIndex: 0,
field: {
aggregatable: true,
@@ -525,6 +572,7 @@ describe('Exception builder helpers', () => {
correspondingKeywordField: undefined,
},
{
+ id: '123',
entryIndex: 1,
field: {
aggregatable: true,
@@ -549,18 +597,19 @@ describe('Exception builder helpers', () => {
test('it returns formatted entries when nested entries exist', () => {
const payloadIndexPattern: IIndexPattern = getMockIndexPattern();
const payloadParent: EntryNested = {
- ...getEntryNestedMock(),
+ ...getEntryNestedWithIdMock(),
field: 'nestedField',
- entries: [{ ...getEntryMatchMock(), field: 'child' }],
+ entries: [{ ...getEntryMatchWithIdMock(), field: 'child' }],
};
const payloadItems: BuilderEntry[] = [
- { ...getEntryMatchMock(), field: 'ip', value: 'some ip' },
+ { ...getEntryMatchWithIdMock(), field: 'ip', value: 'some ip' },
{ ...payloadParent },
];
const output = getFormattedBuilderEntries(payloadIndexPattern, payloadItems);
const expected: FormattedBuilderEntry[] = [
{
+ id: '123',
entryIndex: 0,
field: {
aggregatable: true,
@@ -579,6 +628,7 @@ describe('Exception builder helpers', () => {
correspondingKeywordField: undefined,
},
{
+ id: '123',
entryIndex: 1,
field: {
aggregatable: false,
@@ -594,6 +644,7 @@ describe('Exception builder helpers', () => {
correspondingKeywordField: undefined,
},
{
+ id: '123',
entryIndex: 0,
field: {
aggregatable: false,
@@ -614,16 +665,18 @@ describe('Exception builder helpers', () => {
operator: isOperator,
parent: {
parent: {
+ id: '123',
entries: [
{
+ id: '123',
field: 'child',
- operator: 'included',
- type: 'match',
+ operator: OperatorEnum.INCLUDED,
+ type: OperatorTypeEnum.MATCH,
value: 'some host name',
},
],
field: 'nestedField',
- type: 'nested',
+ type: OperatorTypeEnum.NESTED,
},
parentIndex: 1,
},
@@ -637,15 +690,19 @@ describe('Exception builder helpers', () => {
describe('#getUpdatedEntriesOnDelete', () => {
test('it removes entry corresponding to "entryIndex"', () => {
- const payloadItem: ExceptionsBuilderExceptionItem = { ...getExceptionListItemSchemaMock() };
+ const payloadItem: ExceptionsBuilderExceptionItem = {
+ ...getExceptionListItemSchemaMock(),
+ entries: ENTRIES_WITH_IDS,
+ };
const output = getUpdatedEntriesOnDelete(payloadItem, 0, null);
const expected: ExceptionsBuilderExceptionItem = {
...getExceptionListItemSchemaMock(),
entries: [
{
+ id: '123',
field: 'some.not.nested.field',
- operator: 'included',
- type: 'match',
+ operator: OperatorEnum.INCLUDED,
+ type: OperatorTypeEnum.MATCH,
value: 'some value',
},
],
@@ -658,15 +715,17 @@ describe('Exception builder helpers', () => {
...getExceptionListItemSchemaMock(),
entries: [
{
- ...getEntryNestedMock(),
- entries: [{ ...getEntryExistsMock() }, { ...getEntryMatchAnyMock() }],
+ ...getEntryNestedWithIdMock(),
+ entries: [{ ...getEntryExistsWithIdMock() }, { ...getEntryMatchAnyWithIdMock() }],
},
],
};
const output = getUpdatedEntriesOnDelete(payloadItem, 0, 0);
const expected: ExceptionsBuilderExceptionItem = {
...getExceptionListItemSchemaMock(),
- entries: [{ ...getEntryNestedMock(), entries: [{ ...getEntryMatchAnyMock() }] }],
+ entries: [
+ { ...getEntryNestedWithIdMock(), entries: [{ ...getEntryMatchAnyWithIdMock() }] },
+ ],
};
expect(output).toEqual(expected);
});
@@ -676,8 +735,8 @@ describe('Exception builder helpers', () => {
...getExceptionListItemSchemaMock(),
entries: [
{
- ...getEntryNestedMock(),
- entries: [{ ...getEntryExistsMock() }],
+ ...getEntryNestedWithIdMock(),
+ entries: [{ ...getEntryExistsWithIdMock() }],
},
],
};
@@ -698,10 +757,11 @@ describe('Exception builder helpers', () => {
value: 'I should stay the same',
};
const output = getEntryFromOperator(payloadOperator, payloadEntry);
- const expected: Entry = {
+ const expected: EntryMatch & { id?: string } = {
+ id: '123',
field: 'ip',
operator: 'excluded',
- type: 'match',
+ type: OperatorTypeEnum.MATCH,
value: 'I should stay the same',
};
expect(output).toEqual(expected);
@@ -715,10 +775,11 @@ describe('Exception builder helpers', () => {
value: 'I should stay the same',
};
const output = getEntryFromOperator(payloadOperator, payloadEntry);
- const expected: Entry = {
+ const expected: EntryMatch & { id?: string } = {
+ id: '123',
field: 'ip',
- operator: 'included',
- type: 'match',
+ operator: OperatorEnum.INCLUDED,
+ type: OperatorTypeEnum.MATCH,
value: 'I should stay the same',
};
expect(output).toEqual(expected);
@@ -732,10 +793,11 @@ describe('Exception builder helpers', () => {
value: ['I should stay the same'],
};
const output = getEntryFromOperator(payloadOperator, payloadEntry);
- const expected: Entry = {
+ const expected: EntryMatch & { id?: string } = {
+ id: '123',
field: 'ip',
- operator: 'included',
- type: 'match',
+ operator: OperatorEnum.INCLUDED,
+ type: OperatorTypeEnum.MATCH,
value: '',
};
expect(output).toEqual(expected);
@@ -749,10 +811,11 @@ describe('Exception builder helpers', () => {
value: ['I should stay the same'],
};
const output = getEntryFromOperator(payloadOperator, payloadEntry);
- const expected: Entry = {
+ const expected: EntryMatchAny & { id?: string } = {
+ id: '123',
field: 'ip',
operator: 'excluded',
- type: 'match_any',
+ type: OperatorTypeEnum.MATCH_ANY,
value: ['I should stay the same'],
};
expect(output).toEqual(expected);
@@ -766,10 +829,11 @@ describe('Exception builder helpers', () => {
value: ['I should stay the same'],
};
const output = getEntryFromOperator(payloadOperator, payloadEntry);
- const expected: Entry = {
+ const expected: EntryMatchAny & { id?: string } = {
+ id: '123',
field: 'ip',
- operator: 'included',
- type: 'match_any',
+ operator: OperatorEnum.INCLUDED,
+ type: OperatorTypeEnum.MATCH_ANY,
value: ['I should stay the same'],
};
expect(output).toEqual(expected);
@@ -783,10 +847,11 @@ describe('Exception builder helpers', () => {
value: 'I should stay the same',
};
const output = getEntryFromOperator(payloadOperator, payloadEntry);
- const expected: Entry = {
+ const expected: EntryMatchAny & { id?: string } = {
+ id: '123',
field: 'ip',
- operator: 'included',
- type: 'match_any',
+ operator: OperatorEnum.INCLUDED,
+ type: OperatorTypeEnum.MATCH_ANY,
value: [],
};
expect(output).toEqual(expected);
@@ -799,7 +864,8 @@ describe('Exception builder helpers', () => {
operator: existsOperator,
};
const output = getEntryFromOperator(payloadOperator, payloadEntry);
- const expected: Entry = {
+ const expected: EntryExists & { id?: string } = {
+ id: '123',
field: 'ip',
operator: 'excluded',
type: 'exists',
@@ -814,9 +880,10 @@ describe('Exception builder helpers', () => {
operator: doesNotExistOperator,
};
const output = getEntryFromOperator(payloadOperator, payloadEntry);
- const expected: Entry = {
+ const expected: EntryExists & { id?: string } = {
+ id: '123',
field: 'ip',
- operator: 'included',
+ operator: OperatorEnum.INCLUDED,
type: 'exists',
};
expect(output).toEqual(expected);
@@ -830,9 +897,10 @@ describe('Exception builder helpers', () => {
value: 'I should stay the same',
};
const output = getEntryFromOperator(payloadOperator, payloadEntry);
- const expected: Entry = {
+ const expected: EntryExists & { id?: string } = {
+ id: '123',
field: 'ip',
- operator: 'included',
+ operator: OperatorEnum.INCLUDED,
type: 'exists',
};
expect(output).toEqual(expected);
@@ -846,9 +914,10 @@ describe('Exception builder helpers', () => {
value: 'I should stay the same',
};
const output = getEntryFromOperator(payloadOperator, payloadEntry);
- const expected: Entry = {
+ const expected: EntryList & { id?: string } = {
+ id: '123',
field: 'ip',
- operator: 'included',
+ operator: OperatorEnum.INCLUDED,
type: 'list',
list: { id: '', type: 'ip' },
};
@@ -943,12 +1012,21 @@ describe('Exception builder helpers', () => {
const payloadItem: FormattedBuilderEntry = getMockNestedParentBuilderEntry();
const payloadIFieldType: IFieldType = getField('nestedField.child');
const output = getEntryOnFieldChange(payloadItem, payloadIFieldType);
- const expected: { updatedEntry: BuilderEntry; index: number } = {
+ const expected: { updatedEntry: BuilderEntry & { id?: string }; index: number } = {
index: 0,
updatedEntry: {
- entries: [{ field: 'child', operator: 'included', type: 'match', value: '' }],
+ id: '123',
+ entries: [
+ {
+ id: '123',
+ field: 'child',
+ operator: OperatorEnum.INCLUDED,
+ type: OperatorTypeEnum.MATCH,
+ value: '',
+ },
+ ],
field: 'nestedField',
- type: 'nested',
+ type: OperatorTypeEnum.NESTED,
},
};
expect(output).toEqual(expected);
@@ -959,24 +1037,34 @@ describe('Exception builder helpers', () => {
...getMockNestedBuilderEntry(),
parent: {
parent: {
- ...getEntryNestedMock(),
+ ...getEntryNestedWithIdMock(),
field: 'nestedField',
- entries: [{ ...getEntryMatchMock(), field: 'child' }, getEntryMatchAnyMock()],
+ entries: [
+ { ...getEntryMatchWithIdMock(), field: 'child' },
+ getEntryMatchAnyWithIdMock(),
+ ],
},
parentIndex: 0,
},
};
const payloadIFieldType: IFieldType = getField('nestedField.child');
const output = getEntryOnFieldChange(payloadItem, payloadIFieldType);
- const expected: { updatedEntry: BuilderEntry; index: number } = {
+ const expected: { updatedEntry: BuilderEntry & { id?: string }; index: number } = {
index: 0,
updatedEntry: {
+ id: '123',
entries: [
- { field: 'child', operator: 'included', type: 'match', value: '' },
- getEntryMatchAnyMock(),
+ {
+ id: '123',
+ field: 'child',
+ operator: OperatorEnum.INCLUDED,
+ type: OperatorTypeEnum.MATCH,
+ value: '',
+ },
+ getEntryMatchAnyWithIdMock(),
],
field: 'nestedField',
- type: 'nested',
+ type: OperatorTypeEnum.NESTED,
},
};
expect(output).toEqual(expected);
@@ -986,12 +1074,13 @@ describe('Exception builder helpers', () => {
const payloadItem: FormattedBuilderEntry = getMockBuilderEntry();
const payloadIFieldType: IFieldType = getField('ip');
const output = getEntryOnFieldChange(payloadItem, payloadIFieldType);
- const expected: { updatedEntry: BuilderEntry; index: number } = {
+ const expected: { updatedEntry: BuilderEntry & { id?: string }; index: number } = {
index: 0,
updatedEntry: {
+ id: '123',
field: 'ip',
- operator: 'included',
- type: 'match',
+ operator: OperatorEnum.INCLUDED,
+ type: OperatorTypeEnum.MATCH,
value: '',
},
};
@@ -1004,8 +1093,14 @@ describe('Exception builder helpers', () => {
const payloadItem: FormattedBuilderEntry = getMockBuilderEntry();
const payloadOperator: OperatorOption = isNotOperator;
const output = getEntryOnOperatorChange(payloadItem, payloadOperator);
- const expected: { updatedEntry: BuilderEntry; index: number } = {
- updatedEntry: { field: 'ip', type: 'match', value: 'some value', operator: 'excluded' },
+ const expected: { updatedEntry: BuilderEntry & { id?: string }; index: number } = {
+ updatedEntry: {
+ id: '123',
+ field: 'ip',
+ type: OperatorTypeEnum.MATCH,
+ value: 'some value',
+ operator: 'excluded',
+ },
index: 0,
};
expect(output).toEqual(expected);
@@ -1015,8 +1110,14 @@ describe('Exception builder helpers', () => {
const payloadItem: FormattedBuilderEntry = getMockBuilderEntry();
const payloadOperator: OperatorOption = isOneOfOperator;
const output = getEntryOnOperatorChange(payloadItem, payloadOperator);
- const expected: { updatedEntry: BuilderEntry; index: number } = {
- updatedEntry: { field: 'ip', type: 'match_any', value: [], operator: 'included' },
+ const expected: { updatedEntry: BuilderEntry & { id?: string }; index: number } = {
+ updatedEntry: {
+ id: '123',
+ field: 'ip',
+ type: OperatorTypeEnum.MATCH_ANY,
+ value: [],
+ operator: OperatorEnum.INCLUDED,
+ },
index: 0,
};
expect(output).toEqual(expected);
@@ -1026,19 +1127,21 @@ describe('Exception builder helpers', () => {
const payloadItem: FormattedBuilderEntry = getMockNestedBuilderEntry();
const payloadOperator: OperatorOption = isNotOperator;
const output = getEntryOnOperatorChange(payloadItem, payloadOperator);
- const expected: { updatedEntry: BuilderEntry; index: number } = {
+ const expected: { updatedEntry: BuilderEntry & { id?: string }; index: number } = {
index: 0,
updatedEntry: {
+ id: '123',
entries: [
{
+ id: '123',
field: 'child',
- operator: 'excluded',
- type: 'match',
+ operator: OperatorEnum.EXCLUDED,
+ type: OperatorTypeEnum.MATCH,
value: 'some value',
},
],
field: 'nestedField',
- type: 'nested',
+ type: OperatorTypeEnum.NESTED,
},
};
expect(output).toEqual(expected);
@@ -1048,19 +1151,21 @@ describe('Exception builder helpers', () => {
const payloadItem: FormattedBuilderEntry = getMockNestedBuilderEntry();
const payloadOperator: OperatorOption = isOneOfOperator;
const output = getEntryOnOperatorChange(payloadItem, payloadOperator);
- const expected: { updatedEntry: BuilderEntry; index: number } = {
+ const expected: { updatedEntry: BuilderEntry & { id?: string }; index: number } = {
index: 0,
updatedEntry: {
+ id: '123',
entries: [
{
+ id: '123',
field: 'child',
- operator: 'included',
- type: 'match_any',
+ operator: OperatorEnum.INCLUDED,
+ type: OperatorTypeEnum.MATCH_ANY,
value: [],
},
],
field: 'nestedField',
- type: 'nested',
+ type: OperatorTypeEnum.NESTED,
},
};
expect(output).toEqual(expected);
@@ -1071,8 +1176,14 @@ describe('Exception builder helpers', () => {
test('it returns entry with updated value', () => {
const payload: FormattedBuilderEntry = getMockBuilderEntry();
const output = getEntryOnMatchChange(payload, 'jibber jabber');
- const expected: { updatedEntry: BuilderEntry; index: number } = {
- updatedEntry: { field: 'ip', type: 'match', value: 'jibber jabber', operator: 'included' },
+ const expected: { updatedEntry: BuilderEntry & { id?: string }; index: number } = {
+ updatedEntry: {
+ id: '123',
+ field: 'ip',
+ type: OperatorTypeEnum.MATCH,
+ value: 'jibber jabber',
+ operator: OperatorEnum.INCLUDED,
+ },
index: 0,
};
expect(output).toEqual(expected);
@@ -1081,8 +1192,14 @@ describe('Exception builder helpers', () => {
test('it returns entry with updated value and "field" of empty string if entry does not have a "field" defined', () => {
const payload: FormattedBuilderEntry = { ...getMockBuilderEntry(), field: undefined };
const output = getEntryOnMatchChange(payload, 'jibber jabber');
- const expected: { updatedEntry: BuilderEntry; index: number } = {
- updatedEntry: { field: '', type: 'match', value: 'jibber jabber', operator: 'included' },
+ const expected: { updatedEntry: BuilderEntry & { id?: string }; index: number } = {
+ updatedEntry: {
+ id: '123',
+ field: '',
+ type: OperatorTypeEnum.MATCH,
+ value: 'jibber jabber',
+ operator: OperatorEnum.INCLUDED,
+ },
index: 0,
};
expect(output).toEqual(expected);
@@ -1091,19 +1208,21 @@ describe('Exception builder helpers', () => {
test('it returns nested entry with updated value', () => {
const payload: FormattedBuilderEntry = getMockNestedBuilderEntry();
const output = getEntryOnMatchChange(payload, 'jibber jabber');
- const expected: { updatedEntry: BuilderEntry; index: number } = {
+ const expected: { updatedEntry: BuilderEntry & { id?: string }; index: number } = {
index: 0,
updatedEntry: {
+ id: '123',
entries: [
{
+ id: '123',
field: 'child',
- operator: 'included',
- type: 'match',
+ operator: OperatorEnum.INCLUDED,
+ type: OperatorTypeEnum.MATCH,
value: 'jibber jabber',
},
],
field: 'nestedField',
- type: 'nested',
+ type: OperatorTypeEnum.NESTED,
},
};
expect(output).toEqual(expected);
@@ -1112,19 +1231,21 @@ describe('Exception builder helpers', () => {
test('it returns nested entry with updated value and "field" of empty string if entry does not have a "field" defined', () => {
const payload: FormattedBuilderEntry = { ...getMockNestedBuilderEntry(), field: undefined };
const output = getEntryOnMatchChange(payload, 'jibber jabber');
- const expected: { updatedEntry: BuilderEntry; index: number } = {
+ const expected: { updatedEntry: BuilderEntry & { id?: string }; index: number } = {
index: 0,
updatedEntry: {
+ id: '123',
entries: [
{
+ id: '123',
field: '',
- operator: 'included',
- type: 'match',
+ operator: OperatorEnum.INCLUDED,
+ type: OperatorTypeEnum.MATCH,
value: 'jibber jabber',
},
],
field: 'nestedField',
- type: 'nested',
+ type: OperatorTypeEnum.NESTED,
},
};
expect(output).toEqual(expected);
@@ -1139,12 +1260,13 @@ describe('Exception builder helpers', () => {
value: ['some value'],
};
const output = getEntryOnMatchAnyChange(payload, ['jibber jabber']);
- const expected: { updatedEntry: BuilderEntry; index: number } = {
+ const expected: { updatedEntry: BuilderEntry & { id?: string }; index: number } = {
updatedEntry: {
+ id: '123',
field: 'ip',
- type: 'match_any',
+ type: OperatorTypeEnum.MATCH_ANY,
value: ['jibber jabber'],
- operator: 'included',
+ operator: OperatorEnum.INCLUDED,
},
index: 0,
};
@@ -1159,12 +1281,13 @@ describe('Exception builder helpers', () => {
field: undefined,
};
const output = getEntryOnMatchAnyChange(payload, ['jibber jabber']);
- const expected: { updatedEntry: BuilderEntry; index: number } = {
+ const expected: { updatedEntry: BuilderEntry & { id?: string }; index: number } = {
updatedEntry: {
+ id: '123',
field: '',
- type: 'match_any',
+ type: OperatorTypeEnum.MATCH_ANY,
value: ['jibber jabber'],
- operator: 'included',
+ operator: OperatorEnum.INCLUDED,
},
index: 0,
};
@@ -1176,27 +1299,29 @@ describe('Exception builder helpers', () => {
...getMockNestedBuilderEntry(),
parent: {
parent: {
- ...getEntryNestedMock(),
+ ...getEntryNestedWithIdMock(),
field: 'nestedField',
- entries: [{ ...getEntryMatchAnyMock(), field: 'child' }],
+ entries: [{ ...getEntryMatchAnyWithIdMock(), field: 'child' }],
},
parentIndex: 0,
},
};
const output = getEntryOnMatchAnyChange(payload, ['jibber jabber']);
- const expected: { updatedEntry: BuilderEntry; index: number } = {
+ const expected: { updatedEntry: BuilderEntry & { id?: string }; index: number } = {
index: 0,
updatedEntry: {
+ id: '123',
entries: [
{
+ id: '123',
field: 'child',
- operator: 'included',
- type: 'match_any',
+ type: OperatorTypeEnum.MATCH_ANY,
value: ['jibber jabber'],
+ operator: OperatorEnum.INCLUDED,
},
],
field: 'nestedField',
- type: 'nested',
+ type: OperatorTypeEnum.NESTED,
},
};
expect(output).toEqual(expected);
@@ -1208,27 +1333,29 @@ describe('Exception builder helpers', () => {
field: undefined,
parent: {
parent: {
- ...getEntryNestedMock(),
+ ...getEntryNestedWithIdMock(),
field: 'nestedField',
- entries: [{ ...getEntryMatchAnyMock(), field: 'child' }],
+ entries: [{ ...getEntryMatchAnyWithIdMock(), field: 'child' }],
},
parentIndex: 0,
},
};
const output = getEntryOnMatchAnyChange(payload, ['jibber jabber']);
- const expected: { updatedEntry: BuilderEntry; index: number } = {
+ const expected: { updatedEntry: BuilderEntry & { id?: string }; index: number } = {
index: 0,
updatedEntry: {
+ id: '123',
entries: [
{
+ id: '123',
field: '',
- operator: 'included',
- type: 'match_any',
+ operator: OperatorEnum.INCLUDED,
+ type: OperatorTypeEnum.MATCH_ANY,
value: ['jibber jabber'],
},
],
field: 'nestedField',
- type: 'nested',
+ type: OperatorTypeEnum.NESTED,
},
};
expect(output).toEqual(expected);
@@ -1243,12 +1370,13 @@ describe('Exception builder helpers', () => {
value: '1234',
};
const output = getEntryOnListChange(payload, getListResponseMock());
- const expected: { updatedEntry: BuilderEntry; index: number } = {
+ const expected: { updatedEntry: BuilderEntry & { id?: string }; index: number } = {
updatedEntry: {
+ id: '123',
field: 'ip',
type: 'list',
list: { id: 'some-list-id', type: 'ip' },
- operator: 'included',
+ operator: OperatorEnum.INCLUDED,
},
index: 0,
};
@@ -1263,12 +1391,13 @@ describe('Exception builder helpers', () => {
field: undefined,
};
const output = getEntryOnListChange(payload, getListResponseMock());
- const expected: { updatedEntry: BuilderEntry; index: number } = {
+ const expected: { updatedEntry: BuilderEntry & { id?: string }; index: number } = {
updatedEntry: {
+ id: '123',
field: '',
type: 'list',
list: { id: 'some-list-id', type: 'ip' },
- operator: 'included',
+ operator: OperatorEnum.INCLUDED,
},
index: 0,
};
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx
index a08f869b41d6f..8afdbce68c69a 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx
@@ -5,15 +5,15 @@
* 2.0.
*/
+import uuid from 'uuid';
+
+import { addIdToItem } from '../../../../../common/add_remove_id_to_item';
import { IIndexPattern, IFieldType } from '../../../../../../../../src/plugins/data/common';
import {
Entry,
OperatorTypeEnum,
EntryNested,
ExceptionListType,
- EntryMatch,
- EntryMatchAny,
- EntryExists,
entriesList,
ListSchema,
OperatorEnum,
@@ -160,6 +160,7 @@ export const getFormattedBuilderEntry = (
? { ...foundField, name: foundField.name.split('.').slice(-1)[0] }
: foundField,
correspondingKeywordField,
+ id: item.id ?? `${itemIndex}`,
operator: getExceptionOperatorSelect(item),
value: getEntryValue(item),
nested: 'child',
@@ -169,6 +170,7 @@ export const getFormattedBuilderEntry = (
} else {
return {
field: foundField,
+ id: item.id ?? `${itemIndex}`,
correspondingKeywordField,
operator: getExceptionOperatorSelect(item),
value: getEntryValue(item),
@@ -215,6 +217,7 @@ export const getFormattedBuilderEntries = (
} else {
const parentEntry: FormattedBuilderEntry = {
operator: isOperator,
+ id: item.id ?? `${index}`,
nested: 'parent',
field: isNewNestedEntry
? undefined
@@ -265,7 +268,7 @@ export const getUpdatedEntriesOnDelete = (
const itemOfInterest: BuilderEntry = exceptionItem.entries[nestedParentIndex ?? entryIndex];
if (nestedParentIndex != null && itemOfInterest.type === OperatorTypeEnum.NESTED) {
- const updatedEntryEntries: Array = [
+ const updatedEntryEntries = [
...itemOfInterest.entries.slice(0, entryIndex),
...itemOfInterest.entries.slice(entryIndex + 1),
];
@@ -282,6 +285,7 @@ export const getUpdatedEntriesOnDelete = (
const { field } = itemOfInterest;
const updatedItemOfInterest: EntryNested | EmptyNestedEntry = {
field,
+ id: itemOfInterest.id ?? `${entryIndex}`,
type: OperatorTypeEnum.NESTED,
entries: updatedEntryEntries,
};
@@ -317,12 +321,13 @@ export const getUpdatedEntriesOnDelete = (
export const getEntryFromOperator = (
selectedOperator: OperatorOption,
currentEntry: FormattedBuilderEntry
-): Entry => {
+): Entry & { id?: string } => {
const isSameOperatorType = currentEntry.operator.type === selectedOperator.type;
const fieldValue = currentEntry.field != null ? currentEntry.field.name : '';
switch (selectedOperator.type) {
case 'match':
return {
+ id: currentEntry.id,
field: fieldValue,
type: OperatorTypeEnum.MATCH,
operator: selectedOperator.operator,
@@ -331,6 +336,7 @@ export const getEntryFromOperator = (
};
case 'match_any':
return {
+ id: currentEntry.id,
field: fieldValue,
type: OperatorTypeEnum.MATCH_ANY,
operator: selectedOperator.operator,
@@ -338,6 +344,7 @@ export const getEntryFromOperator = (
};
case 'list':
return {
+ id: currentEntry.id,
field: fieldValue,
type: OperatorTypeEnum.LIST,
operator: selectedOperator.operator,
@@ -345,6 +352,7 @@ export const getEntryFromOperator = (
};
default:
return {
+ id: currentEntry.id,
field: fieldValue,
type: OperatorTypeEnum.EXISTS,
operator: selectedOperator.operator,
@@ -397,7 +405,7 @@ export const getEntryOnFieldChange = (
if (nested === 'parent') {
// For nested entries, when user first selects to add a nested
- // entry, they first see a row similiar to what is shown for when
+ // entry, they first see a row similar to what is shown for when
// a user selects "exists", as soon as they make a selection
// we can now identify the 'parent' and 'child' this is where
// we first convert the entry into type "nested"
@@ -408,15 +416,16 @@ export const getEntryOnFieldChange = (
return {
updatedEntry: {
+ id: item.id,
field: newParentFieldValue,
type: OperatorTypeEnum.NESTED,
entries: [
- {
+ addIdToItem({
field: newChildFieldValue ?? '',
type: OperatorTypeEnum.MATCH,
operator: isOperator.operator,
value: '',
- },
+ }),
],
},
index: entryIndex,
@@ -428,6 +437,7 @@ export const getEntryOnFieldChange = (
entries: [
...parent.parent.entries.slice(0, entryIndex),
{
+ id: item.id,
field: newChildFieldValue ?? '',
type: OperatorTypeEnum.MATCH,
operator: isOperator.operator,
@@ -441,6 +451,7 @@ export const getEntryOnFieldChange = (
} else {
return {
updatedEntry: {
+ id: item.id,
field: newField != null ? newField.name : '',
type: OperatorTypeEnum.MATCH,
operator: isOperator.operator,
@@ -508,6 +519,7 @@ export const getEntryOnMatchChange = (
entries: [
...parent.parent.entries.slice(0, entryIndex),
{
+ id: item.id,
field: fieldName,
type: OperatorTypeEnum.MATCH,
operator: operator.operator,
@@ -521,6 +533,7 @@ export const getEntryOnMatchChange = (
} else {
return {
updatedEntry: {
+ id: item.id,
field: field != null ? field.name : '',
type: OperatorTypeEnum.MATCH,
operator: operator.operator,
@@ -554,6 +567,7 @@ export const getEntryOnMatchAnyChange = (
entries: [
...parent.parent.entries.slice(0, entryIndex),
{
+ id: item.id,
field: fieldName,
type: OperatorTypeEnum.MATCH_ANY,
operator: operator.operator,
@@ -567,6 +581,7 @@ export const getEntryOnMatchAnyChange = (
} else {
return {
updatedEntry: {
+ id: item.id,
field: field != null ? field.name : '',
type: OperatorTypeEnum.MATCH_ANY,
operator: operator.operator,
@@ -594,6 +609,7 @@ export const getEntryOnListChange = (
return {
updatedEntry: {
+ id: item.id,
field: field != null ? field.name : '',
type: OperatorTypeEnum.LIST,
operator: operator.operator,
@@ -604,6 +620,7 @@ export const getEntryOnListChange = (
};
export const getDefaultEmptyEntry = (): EmptyEntry => ({
+ id: uuid.v4(),
field: '',
type: OperatorTypeEnum.MATCH,
operator: OperatorEnum.INCLUDED,
@@ -611,6 +628,7 @@ export const getDefaultEmptyEntry = (): EmptyEntry => ({
});
export const getDefaultNestedEmptyEntry = (): EmptyNestedEntry => ({
+ id: uuid.v4(),
field: '',
type: OperatorTypeEnum.NESTED,
entries: [],
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx
index 99ce4566daff6..d9402272bd9a2 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx
@@ -8,7 +8,6 @@
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { ReactWrapper, mount } from 'enzyme';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { waitFor } from '@testing-library/react';
import {
@@ -23,6 +22,12 @@ import { getEmptyValue } from '../../empty_value';
import { ExceptionBuilderComponent } from './';
+const mockTheme = {
+ eui: {
+ euiColorLightShade: '#ece',
+ },
+};
+
jest.mock('../../../../common/lib/kibana');
describe('ExceptionBuilderComponent', () => {
@@ -50,7 +55,7 @@ describe('ExceptionBuilderComponent', () => {
test('it displays empty entry if no "exceptionListItems" are passed in', () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it displays "exceptionListItems" that are passed in', async () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it displays "or", "and" and "add nested button" enabled', () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it adds an entry when "and" clicked', async () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it adds an exception item when "or" clicked', async () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it displays empty entry if user deletes last remaining entry', () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it displays "and" badge if at least one exception item includes more than one entry', () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it does not display "and" badge if none of the exception items include more than one entry', () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
describe('nested entry', () => {
test('it adds a nested entry when "add nested entry" clicked', async () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
1);
-
setUpdateExceptions([...exceptions.slice(0, exceptions.length - 1), { ...updatedException }]);
},
[setUpdateExceptions, exceptions]
@@ -287,12 +286,12 @@ export const ExceptionBuilderComponent = ({
...lastEntry,
entries: [
...lastEntry.entries,
- {
+ addIdToItem({
field: '',
type: OperatorTypeEnum.MATCH,
operator: OperatorEnum.INCLUDED,
value: '',
- },
+ }),
],
},
],
@@ -352,7 +351,7 @@ export const ExceptionBuilderComponent = ({
}, []);
return (
-
+
{exceptions.map((exceptionListItem, index) => (
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts
index 0741f561c1933..dbac7d325b63a 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts
@@ -14,6 +14,10 @@ import { ExceptionsBuilderExceptionItem } from '../types';
import { Action, State, exceptionsBuilderReducer } from './reducer';
import { getDefaultEmptyEntry } from './helpers';
+jest.mock('uuid', () => ({
+ v4: jest.fn().mockReturnValue('123'),
+}));
+
const initialState: State = {
disableAnd: false,
disableNested: false,
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx
index 00491ba495cfc..ec44f815ae243 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx
@@ -9,7 +9,6 @@ import React from 'react';
import { waitFor } from '@testing-library/react';
import { ThemeProvider } from 'styled-components';
import { mount, ReactWrapper } from 'enzyme';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { EditExceptionModal } from './';
import { useCurrentUser } from '../../../../common/lib/kibana';
@@ -29,6 +28,17 @@ import {
} from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks';
import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async';
+const mockTheme = {
+ eui: {
+ euiBreakpoints: {
+ l: '1200px',
+ },
+ paddingSizes: {
+ m: '10px',
+ },
+ },
+};
+
jest.mock('../../../../common/lib/kibana');
jest.mock('../../../../detections/containers/detection_engine/rules');
jest.mock('../use_add_exception');
@@ -84,7 +94,7 @@ describe('When the edit exception modal is opened', () => {
},
]);
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
] as EntriesArray,
};
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
let wrapper: ReactWrapper;
beforeEach(async () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
},
}));
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
let wrapper: ReactWrapper;
beforeEach(async () => {
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
beforeEach(async () => {
const exceptionItemMock = { ...getExceptionListItemSchemaMock(), entries: [] };
wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('when there are exception builder errors has the add exception button disabled', async () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
- {i18n.CANCEL}
+
+ {i18n.CANCEL}
+
{
it('it renders error details', () => {
const wrapper = mountWithIntl(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -57,21 +53,19 @@ describe('ErrorCallout', () => {
it('it invokes "onCancel" when cancel button clicked', () => {
const mockOnCancel = jest.fn();
const wrapper = mountWithIntl(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper.find('[data-test-subj="errorCalloutCancelButton"]').at(0).simulate('click');
@@ -81,21 +75,19 @@ describe('ErrorCallout', () => {
it('it does not render status code if not available', () => {
const wrapper = mountWithIntl(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -109,21 +101,19 @@ describe('ErrorCallout', () => {
it('it renders specific missing exceptions list error', () => {
const wrapper = mountWithIntl(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -137,21 +127,19 @@ describe('ErrorCallout', () => {
it('it dissasociates list from rule when remove exception list clicked ', () => {
const wrapper = mountWithIntl(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').at(0).simulate('click');
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx
index fdf7594a550a2..8651dac8c8cfd 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx
@@ -48,7 +48,11 @@ import { getEntryMatchAnyMock } from '../../../../../lists/common/schemas/types/
import { getEntryExistsMock } from '../../../../../lists/common/schemas/types/entry_exists.mock';
import { getEntryListMock } from '../../../../../lists/common/schemas/types/entry_list.mock';
import { getCommentsArrayMock } from '../../../../../lists/common/schemas/types/comment.mock';
-import { ENTRIES, OLD_DATE_RELATIVE_TO_DATE_NOW } from '../../../../../lists/common/constants.mock';
+import {
+ ENTRIES,
+ ENTRIES_WITH_IDS,
+ OLD_DATE_RELATIVE_TO_DATE_NOW,
+} from '../../../../../lists/common/constants.mock';
import {
CreateExceptionListItemSchema,
ExceptionListItemSchema,
@@ -57,6 +61,10 @@ import {
} from '../../../../../lists/common/schemas';
import { IIndexPattern } from 'src/plugins/data/common';
+jest.mock('uuid', () => ({
+ v4: jest.fn().mockReturnValue('123'),
+}));
+
describe('Exception helpers', () => {
beforeEach(() => {
moment.tz.setDefault('UTC');
@@ -229,9 +237,22 @@ describe('Exception helpers', () => {
});
describe('#filterExceptionItems', () => {
+ // Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes
+ // for context around the temporary `id`
+ test('it correctly validates entries that include a temporary `id`', () => {
+ const output: Array<
+ ExceptionListItemSchema | CreateExceptionListItemSchema
+ > = filterExceptionItems([
+ { ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS },
+ ]);
+
+ expect(output).toEqual([{ ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS }]);
+ });
+
test('it removes entry items with "value" of "undefined"', () => {
const { entries, ...rest } = getExceptionListItemSchemaMock();
const mockEmptyException: EmptyEntry = {
+ id: '123',
field: 'host.name',
type: OperatorTypeEnum.MATCH,
operator: OperatorEnum.INCLUDED,
@@ -250,6 +271,7 @@ describe('Exception helpers', () => {
test('it removes "match" entry items with "value" of empty string', () => {
const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
const mockEmptyException: EmptyEntry = {
+ id: '123',
field: 'host.name',
type: OperatorTypeEnum.MATCH,
operator: OperatorEnum.INCLUDED,
@@ -270,6 +292,7 @@ describe('Exception helpers', () => {
test('it removes "match" entry items with "field" of empty string', () => {
const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
const mockEmptyException: EmptyEntry = {
+ id: '123',
field: '',
type: OperatorTypeEnum.MATCH,
operator: OperatorEnum.INCLUDED,
@@ -290,6 +313,7 @@ describe('Exception helpers', () => {
test('it removes "match_any" entry items with "field" of empty string', () => {
const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
const mockEmptyException: EmptyEntry = {
+ id: '123',
field: '',
type: OperatorTypeEnum.MATCH_ANY,
operator: OperatorEnum.INCLUDED,
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx
index 13ee06e8cbac9..c44de4f05e7f6 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx
@@ -44,6 +44,7 @@ import { validate } from '../../../../common/validate';
import { Ecs } from '../../../../common/ecs';
import { CodeSignature } from '../../../../common/ecs/file';
import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard';
+import { addIdToItem, removeIdFromItem } from '../../../../common';
/**
* Returns the operator type, may not need this if using io-ts types
@@ -150,12 +151,12 @@ export const getNewExceptionItem = ({
comments: [],
description: `${ruleName} - exception list item`,
entries: [
- {
+ addIdToItem({
field: '',
operator: 'included',
type: 'match',
value: '',
- },
+ }),
],
item_id: undefined,
list_id: listId,
@@ -175,26 +176,32 @@ export const filterExceptionItems = (
return exceptions.reduce>(
(acc, exception) => {
const entries = exception.entries.reduce((nestedAcc, singleEntry) => {
- if (singleEntry.type === 'nested') {
- const nestedEntriesArray = singleEntry.entries.filter((singleNestedEntry) => {
- const [validatedNestedEntry] = validate(singleNestedEntry, nestedEntryItem);
+ const strippedSingleEntry = removeIdFromItem(singleEntry);
+
+ if (entriesNested.is(strippedSingleEntry)) {
+ const nestedEntriesArray = strippedSingleEntry.entries.filter((singleNestedEntry) => {
+ const noIdSingleNestedEntry = removeIdFromItem(singleNestedEntry);
+ const [validatedNestedEntry] = validate(noIdSingleNestedEntry, nestedEntryItem);
return validatedNestedEntry != null;
});
+ const noIdNestedEntries = nestedEntriesArray.map((singleNestedEntry) =>
+ removeIdFromItem(singleNestedEntry)
+ );
const [validatedNestedEntry] = validate(
- { ...singleEntry, entries: nestedEntriesArray },
+ { ...strippedSingleEntry, entries: noIdNestedEntries },
entriesNested
);
if (validatedNestedEntry != null) {
- return [...nestedAcc, validatedNestedEntry];
+ return [...nestedAcc, { ...singleEntry, entries: nestedEntriesArray }];
}
return nestedAcc;
} else {
- const [validatedEntry] = validate(singleEntry, entry);
+ const [validatedEntry] = validate(strippedSingleEntry, entry);
if (validatedEntry != null) {
- return [...nestedAcc, validatedEntry];
+ return [...nestedAcc, singleEntry];
}
return nestedAcc;
}
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts
index 6108a21ce5624..c7a125daa54f8 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts
@@ -60,16 +60,18 @@ export interface ExceptionsPagination {
}
export interface FormattedBuilderEntry {
+ id: string;
field: IFieldType | undefined;
operator: OperatorOption;
value: string | string[] | undefined;
nested: 'parent' | 'child' | undefined;
entryIndex: number;
- parent: { parent: EntryNested; parentIndex: number } | undefined;
+ parent: { parent: BuilderEntryNested; parentIndex: number } | undefined;
correspondingKeywordField: IFieldType | undefined;
}
export interface EmptyEntry {
+ id: string;
field: string | undefined;
operator: OperatorEnum;
type: OperatorTypeEnum.MATCH | OperatorTypeEnum.MATCH_ANY;
@@ -77,6 +79,7 @@ export interface EmptyEntry {
}
export interface EmptyListEntry {
+ id: string;
field: string | undefined;
operator: OperatorEnum;
type: OperatorTypeEnum.LIST;
@@ -84,12 +87,31 @@ export interface EmptyListEntry {
}
export interface EmptyNestedEntry {
+ id: string;
field: string | undefined;
type: OperatorTypeEnum.NESTED;
- entries: Array;
+ entries: Array<
+ | (EntryMatch & { id?: string })
+ | (EntryMatchAny & { id?: string })
+ | (EntryExists & { id?: string })
+ >;
}
-export type BuilderEntry = Entry | EmptyListEntry | EmptyEntry | EntryNested | EmptyNestedEntry;
+export type BuilderEntry =
+ | (Entry & { id?: string })
+ | EmptyListEntry
+ | EmptyEntry
+ | BuilderEntryNested
+ | EmptyNestedEntry;
+
+export type BuilderEntryNested = Omit & {
+ id?: string;
+ entries: Array<
+ | (EntryMatch & { id?: string })
+ | (EntryMatchAny & { id?: string })
+ | (EntryExists & { id?: string })
+ >;
+};
export type ExceptionListItemBuilderSchema = Omit & {
entries: BuilderEntry[];
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx
index c7d7d2d39393c..b96ae5c06dd22 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx
@@ -8,13 +8,18 @@
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { mount } from 'enzyme';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import moment from 'moment-timezone';
import { ExceptionDetails } from './exception_details';
import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { getCommentsArrayMock } from '../../../../../../../lists/common/schemas/types/comment.mock';
+const mockTheme = {
+ eui: {
+ euiColorLightestShade: '#ece',
+ },
+};
+
describe('ExceptionDetails', () => {
beforeEach(() => {
moment.tz.setDefault('UTC');
@@ -29,7 +34,7 @@ describe('ExceptionDetails', () => {
exceptionItem.comments = [];
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.comments = getCommentsArrayMock();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.comments = [getCommentsArrayMock()[0]];
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.comments = getCommentsArrayMock();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.comments = getCommentsArrayMock();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.comments = getCommentsArrayMock();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.comments = getCommentsArrayMock();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it renders the operating system if one is specified in the exception item', () => {
const exceptionItem = getExceptionListItemSchemaMock();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it renders the exception item creator', () => {
const exceptionItem = getExceptionListItemSchemaMock();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it renders the exception item creation timestamp', () => {
const exceptionItem = getExceptionListItemSchemaMock();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it renders the description if one is included on the exception item', () => {
const exceptionItem = getExceptionListItemSchemaMock();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it does NOT render the and badge if only one exception item entry exists', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it renders the and badge if more than one exception item exists', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it invokes "onEdit" when edit button clicked', () => {
const mockOnEdit = jest.fn();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it invokes "onDelete" when delete button clicked', () => {
const mockOnDelete = jest.fn();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it renders edit button disabled if "disableDelete" is "true"', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it renders delete button in loading state if "disableDelete" is "true"', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
parentEntry.value = undefined;
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it renders non-nested entries', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
it('it renders ExceptionDetails and ExceptionEntries', () => {
const exceptionItem = getExceptionListItemSchemaMock();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
const exceptionItem = getExceptionListItemSchemaMock();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
const exceptionItem = getExceptionListItemSchemaMock();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.comments = getCommentsArrayMock();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.comments = getCommentsArrayMock();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
it('it renders passed in "pageSize" as selected option', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(wrapper.find('[data-test-subj="exceptionsPerPageBtn"]').at(0).text()).toEqual(
@@ -35,17 +31,15 @@ describe('ExceptionsViewerPagination', () => {
it('it renders all passed in page size options when per page button clicked', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper.find('[data-test-subj="exceptionsPerPageBtn"] button').simulate('click');
@@ -64,17 +58,15 @@ describe('ExceptionsViewerPagination', () => {
it('it invokes "onPaginationChange" when per page item is clicked', () => {
const mockOnPaginationChange = jest.fn();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper.find('[data-test-subj="exceptionsPerPageBtn"] button').simulate('click');
@@ -87,17 +79,15 @@ describe('ExceptionsViewerPagination', () => {
it('it renders correct total page count', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(wrapper.find('[data-test-subj="exceptionsPagination"]').at(0).prop('pageCount')).toEqual(
@@ -111,17 +101,15 @@ describe('ExceptionsViewerPagination', () => {
it('it invokes "onPaginationChange" when next clicked', () => {
const mockOnPaginationChange = jest.fn();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper.find('[data-test-subj="pagination-button-next"]').at(1).simulate('click');
@@ -134,17 +122,15 @@ describe('ExceptionsViewerPagination', () => {
it('it invokes "onPaginationChange" when page clicked', () => {
const mockOnPaginationChange = jest.fn();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper.find('button[data-test-subj="pagination-button-3"]').simulate('click');
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx
index 6167a29a4a17d..42ce0c792dfa3 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx
@@ -8,14 +8,25 @@
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { mountWithIntl } from '@kbn/test/jest';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { ExceptionsViewerUtility } from './exceptions_utility';
+const mockTheme = {
+ eui: {
+ euiBreakpoints: {
+ l: '1200px',
+ },
+ paddingSizes: {
+ m: '10px',
+ },
+ euiBorderThin: '1px solid #ece',
+ },
+};
+
describe('ExceptionsViewerUtility', () => {
it('it renders correct pluralized text when more than one exception exists', () => {
const wrapper = mountWithIntl(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
it('it renders correct singular text when less than two exceptions exists', () => {
const wrapper = mountWithIntl(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
it('it invokes "onRefreshClick" when refresh button clicked', () => {
const mockOnRefreshClick = jest.fn();
const wrapper = mountWithIntl(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
it('it does not render any messages when "showEndpointList" and "showDetectionsList" are "false"', () => {
const wrapper = mountWithIntl(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
it('it does render detections messages when "showDetectionsList" is "true"', () => {
const wrapper = mountWithIntl(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
it('it does render endpoint messages when "showEndpointList" is "true"', () => {
const wrapper = mountWithIntl(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
it('it renders all disabled if "isInitLoading" is true', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -47,16 +43,14 @@ describe('ExceptionsViewerHeader', () => {
it('it displays toggles and add exception popover when more than one list type available', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(wrapper.find('[data-test-subj="exceptionsFilterGroupBtns"]').exists()).toBeTruthy();
@@ -67,16 +61,14 @@ describe('ExceptionsViewerHeader', () => {
it('it does not display toggles and add exception popover if only one list type is available', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(wrapper.find('[data-test-subj="exceptionsFilterGroupBtns"]')).toHaveLength(0);
@@ -87,16 +79,14 @@ describe('ExceptionsViewerHeader', () => {
it('it displays add exception button without popover if only one list type is available', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(
@@ -107,16 +97,14 @@ describe('ExceptionsViewerHeader', () => {
it('it renders detections filter toggle selected when clicked', () => {
const mockOnFilterChange = jest.fn();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper.find('[data-test-subj="exceptionsDetectionFilterBtn"] button').simulate('click');
@@ -149,16 +137,14 @@ describe('ExceptionsViewerHeader', () => {
it('it renders endpoint filter toggle selected and invokes "onFilterChange" when clicked', () => {
const mockOnFilterChange = jest.fn();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper.find('[data-test-subj="exceptionsEndpointFilterBtn"] button').simulate('click');
@@ -191,16 +177,14 @@ describe('ExceptionsViewerHeader', () => {
it('it invokes "onAddExceptionClick" when user selects to add an exception item and only endpoint exception lists are available', () => {
const mockOnAddExceptionClick = jest.fn();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"] button').simulate('click');
@@ -211,16 +195,14 @@ describe('ExceptionsViewerHeader', () => {
it('it invokes "onAddDetectionsExceptionClick" when user selects to add an exception item and only endpoint detections lists are available', () => {
const mockOnAddExceptionClick = jest.fn();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"] button').simulate('click');
@@ -231,16 +213,14 @@ describe('ExceptionsViewerHeader', () => {
it('it invokes "onAddEndpointExceptionClick" when user selects to add an exception item to endpoint list from popover', () => {
const mockOnAddExceptionClick = jest.fn();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper
@@ -254,16 +234,14 @@ describe('ExceptionsViewerHeader', () => {
it('it invokes "onAddDetectionsExceptionClick" when user selects to add an exception item to endpoint list from popover', () => {
const mockOnAddExceptionClick = jest.fn();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper
@@ -277,16 +255,14 @@ describe('ExceptionsViewerHeader', () => {
it('it invokes "onFilterChange" when search used and "Enter" pressed', () => {
const mockOnFilterChange = jest.fn();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
wrapper.find('EuiFieldSearch').at(0).simulate('keyup', {
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx
index 3171735d905de..167b95995212b 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx
@@ -8,27 +8,32 @@
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { mount } from 'enzyme';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import * as i18n from '../translations';
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { ExceptionsViewerItems } from './exceptions_viewer_items';
+const mockTheme = {
+ eui: {
+ euiSize: '10px',
+ euiColorPrimary: '#ece',
+ euiColorDanger: '#ece',
+ },
+};
+
describe('ExceptionsViewerItems', () => {
it('it renders empty prompt if "showEmpty" is "true"', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
+
);
expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy();
@@ -43,7 +48,7 @@ describe('ExceptionsViewerItems', () => {
it('it renders no search results found prompt if "showNoResults" is "true"', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
it('it renders exceptions if "showEmpty" and "isInitLoading" is "false", and exceptions exist', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
it('it does not render exceptions if "isInitLoading" is "true"', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
exception2.id = 'newId';
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
exception2.id = 'newId';
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
const mockOnDeleteException = jest.fn();
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
jest.fn(),
]);
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
it('it renders empty prompt if no "exceptionListMeta" passed in', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
]);
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
- const theme = () => ({ eui: euiDarkVars, darkMode: true });
const closeModal = jest.fn();
describe('rendering', () => {
test('when isShowing is positive and request and response are not null', () => {
const wrapper = mount(
-
+
{
describe('functionality from tab statistics/request/response', () => {
test('Click on statistic Tab', () => {
const wrapper = mount(
-
+
{
test('Click on request Tab', () => {
const wrapper = mount(
-
+
{
test('Click on response Tab', () => {
const wrapper = mount(
-
+
{
describe('events', () => {
test('Make sure that toggle function has been called when you click on the close button', () => {
const wrapper = mount(
-
+
-
+
My test supplement.
- }
- headerTitle="Hosts"
- headerTooltip="My test tooltip"
- headerUnit="Test Unit"
- itemsPerRow={
- Array [
- Object {
- "numberOfRow": 2,
- "text": "2 rows",
- },
- Object {
- "numberOfRow": 5,
- "text": "5 rows",
- },
- Object {
- "numberOfRow": 10,
- "text": "10 rows",
- },
- Object {
- "numberOfRow": 20,
- "text": "20 rows",
- },
- Object {
- "numberOfRow": 50,
- "text": "50 rows",
- },
- ]
- }
- limit={1}
- loadPage={[MockFunction]}
- loading={false}
- pageOfItems={
- Array [
- Object {
- "cursor": Object {
- "value": "98966fa2013c396155c460d35c0902be",
- },
- "host": Object {
- "_id": "cPsuhGcB0WOhS6qyTKC0",
- "firstSeen": "2018-12-06T15:40:53.319Z",
- "name": "elrond.elstc.co",
- "os": "Ubuntu",
- "version": "18.04.1 LTS (Bionic Beaver)",
- },
- },
- Object {
- "cursor": Object {
- "value": "aa7ca589f1b8220002f2fc61c64cfbf1",
- },
- "host": Object {
- "_id": "KwQDiWcB0WOhS6qyXmrW",
- "firstSeen": "2018-12-07T14:12:38.560Z",
- "name": "siem-kibana",
- "os": "Debian GNU/Linux",
- "version": "9 (stretch)",
- },
- },
- ]
- }
- showMorePagesIndicator={true}
- totalCount={10}
- updateActivePage={[MockFunction]}
- updateLimitPagination={[Function]}
- />
-
+
+
+
+
+
+ Rows per page: 1
+
+ }
+ closePopover={[Function]}
+ data-test-subj="loadingMoreSizeRowPopover"
+ display="inlineBlock"
+ hasArrow={true}
+ id="customizablePagination"
+ isOpen={false}
+ ownFocus={false}
+ panelPaddingSize="none"
+ repositionOnScroll={true}
+ >
+
+ 2 rows
+ ,
+
+ 5 rows
+ ,
+
+ 10 rows
+ ,
+
+ 20 rows
+ ,
+
+ 50 rows
+ ,
+ ]
+ }
+ />
+
+
+
+
+
+
+
+
`;
diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx
index 57d4c8451de24..c20f1ae66c797 100644
--- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx
@@ -14,7 +14,6 @@ import { Direction } from '../../../graphql/types';
import { BasicTableProps, PaginatedTable } from './index';
import { getHostsColumns, mockData, rowItems, sortedHosts } from './index.mock';
import { ThemeProvider } from 'styled-components';
-import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
jest.mock('react', () => {
const r = jest.requireActual('react');
@@ -22,8 +21,20 @@ jest.mock('react', () => {
return { ...r, memo: (x: any) => x };
});
+const mockTheme = {
+ eui: {
+ euiColorEmptyShade: '#ece',
+ euiSizeL: '10px',
+ euiBreakpoints: {
+ s: '450px',
+ },
+ paddingSizes: {
+ m: '10px',
+ },
+ },
+};
+
describe('Paginated Table Component', () => {
- const theme = () => ({ eui: euiDarkVars, darkMode: true });
let loadPage: jest.Mock;
let updateLimitPagination: jest.Mock;
let updateActivePage: jest.Mock;
@@ -36,26 +47,24 @@ describe('Paginated Table Component', () => {
describe('rendering', () => {
test('it renders the default load more table', () => {
const wrapper = shallow(
-
- {'My test supplement.'}}
- headerTitle="Hosts"
- headerTooltip="My test tooltip"
- headerUnit="Test Unit"
- itemsPerRow={rowItems}
- limit={1}
- loading={false}
- loadPage={loadPage}
- pageOfItems={mockData.Hosts.edges}
- showMorePagesIndicator={true}
- totalCount={10}
- updateActivePage={updateActivePage}
- updateLimitPagination={(limit) => updateLimitPagination({ limit })}
- />
-
+ {'My test supplement.'}}
+ headerTitle="Hosts"
+ headerTooltip="My test tooltip"
+ headerUnit="Test Unit"
+ itemsPerRow={rowItems}
+ limit={1}
+ loading={false}
+ loadPage={loadPage}
+ pageOfItems={mockData.Hosts.edges}
+ showMorePagesIndicator={true}
+ totalCount={10}
+ updateActivePage={updateActivePage}
+ updateLimitPagination={(limit) => updateLimitPagination({ limit })}
+ />
);
expect(wrapper).toMatchSnapshot();
@@ -63,7 +72,7 @@ describe('Paginated Table Component', () => {
test('it renders the loading panel at the beginning ', () => {
const wrapper = mount(
-
+
{
test('it renders the over loading panel after data has been in the table ', () => {
const wrapper = mount(
-
+
{
test('it renders the correct amount of pages and starts at activePage: 0', () => {
const wrapper = mount(
-
+
{
test('it render popover to select new limit in table', () => {
const wrapper = mount(
-
+
{
test('it will NOT render popover to select new limit in table if props itemsPerRow is empty', () => {
const wrapper = mount(
-
+
{
test('It should render a sort icon if sorting is defined', () => {
const mockOnChange = jest.fn();
const wrapper = mount(
-
+
{
test('Should display toast when user reaches end of results max', () => {
const wrapper = mount(
-
+
{
test('Should show items per row if totalCount is greater than items', () => {
const wrapper = mount(
-
+
{
test('Should hide items per row if totalCount is less than items', () => {
const wrapper = mount(
-
+
{
describe('Events', () => {
test('should call updateActivePage with 1 when clicking to the first page', () => {
const wrapper = mount(
-
+
{
test('Should call updateActivePage with 0 when you pick a new limit', () => {
const wrapper = mount(
-
+
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ComponentWithContext = (props: BasicTableProps) => {
return (
-
+
);
@@ -424,7 +433,7 @@ describe('Paginated Table Component', () => {
test('Should call updateLimitPagination when you pick a new limit', () => {
const wrapper = mount(
-
+
{
test('Should call onChange when you choose a new sort in the table', () => {
const mockOnChange = jest.fn();
const wrapper = mount(
-
+
{
});
describe('Stat Items Component', () => {
- const theme = () => ({ eui: euiDarkVars, darkMode: true });
+ const mockTheme = { eui: { euiColorMediumShade: '#ece' } };
const state: State = mockGlobalState;
const { storage } = createSecuritySolutionStorageMock();
const store = createStore(
@@ -71,7 +70,7 @@ describe('Stat Items Component', () => {
describe.each([
[
mount(
-
+
{
],
[
mount(
-
+
{
test('it renders entryItemIndexItemEntryFirstRowAndBadge for very first item', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
);
@@ -25,7 +30,7 @@ describe('AndBadgeComponent', () => {
test('it renders entryItemEntryInvisibleAndBadge if "entriesLength" is 1 or less', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
);
@@ -37,7 +42,7 @@ describe('AndBadgeComponent', () => {
test('it renders regular "and" badge if item is not the first one and includes more than one entry', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
);
diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx
index eced8d785792d..ef3e9280e6e6b 100644
--- a/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx
@@ -14,7 +14,7 @@ import {
import { IndexPattern, IFieldType } from '../../../../../../../src/plugins/data/common';
import { Entry, FormattedEntry, ThreatMapEntries, EmptyEntry } from './types';
-import { addIdToItem } from '../../utils/add_remove_id_to_item';
+import { addIdToItem } from '../../../../common/add_remove_id_to_item';
/**
* Formats the entry into one that is easily usable for the UI.
diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/index.test.tsx
index 73174bc5fc113..6aa33c3bcf4ca 100644
--- a/x-pack/plugins/security_solution/public/common/components/threat_match/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/threat_match/index.test.tsx
@@ -8,7 +8,6 @@
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { mount } from 'enzyme';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { waitFor } from '@testing-library/react';
import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
@@ -19,6 +18,12 @@ import { ThreatMatchComponent } from './';
import { ThreatMapEntries } from './types';
import { IndexPattern } from 'src/plugins/data/public';
+const mockTheme = {
+ eui: {
+ euiColorLightShade: '#ece',
+ },
+};
+
jest.mock('../../../common/lib/kibana');
const getPayLoad = (): ThreatMapEntries[] => [
@@ -51,7 +56,7 @@ describe('ThreatMatchComponent', () => {
test('it displays empty entry if no "listItems" are passed in', () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+
{
test('it displays "Search" for "listItems" that are passed in', async () => {
const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
+