diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc
index b32c340df4adf..79fa9a642428a 100644
--- a/docs/settings/apm-settings.asciidoc
+++ b/docs/settings/apm-settings.asciidoc
@@ -43,6 +43,9 @@ Changing these settings may disable features of the APM App.
| `xpack.apm.enabled`
| Set to `false` to disable the APM app. Defaults to `true`.
+| `xpack.apm.maxServiceEnvironments`
+ | Maximum number of unique service environments recognized by the UI. Defaults to `100`.
+
| `xpack.apm.serviceMapFingerprintBucketSize`
| Maximum number of unique transaction combinations sampled for generating service map focused on a specific service. Defaults to `100`.
diff --git a/package.json b/package.json
index 821975d11c638..ade567c840da7 100644
--- a/package.json
+++ b/package.json
@@ -844,8 +844,8 @@
"vinyl-fs": "^3.0.3",
"wait-on": "^5.0.1",
"watchpack": "^1.6.0",
- "webpack-cli": "^3.3.10",
- "webpack-dev-server": "^3.8.2",
+ "webpack-cli": "^3.3.12",
+ "webpack-dev-server": "^3.11.0",
"webpack-merge": "^4.2.2",
"write-pkg": "^4.0.0",
"xml-crypto": "^2.0.0",
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts
index 333f5caf72525..a8c5df8d64630 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts
@@ -21,28 +21,64 @@
import { esKuery } from '../../../es_query';
type KueryNode = any;
-import { typeRegistryMock } from '../../../saved_objects_type_registry.mock';
+import { SavedObjectTypeRegistry } from '../../../saved_objects_type_registry';
import { ALL_NAMESPACES_STRING } from '../utils';
import { getQueryParams, getClauseForReference } from './query_params';
-const registry = typeRegistryMock.create();
+const registerTypes = (registry: SavedObjectTypeRegistry) => {
+ registry.registerType({
+ name: 'pending',
+ hidden: false,
+ namespaceType: 'single',
+ mappings: {
+ properties: { title: { type: 'text' } },
+ },
+ management: {
+ defaultSearchField: 'title',
+ },
+ });
-const MAPPINGS = {
- properties: {
- pending: { properties: { title: { type: 'text' } } },
- saved: {
+ registry.registerType({
+ name: 'saved',
+ hidden: false,
+ namespaceType: 'single',
+ mappings: {
properties: {
title: { type: 'text', fields: { raw: { type: 'keyword' } } },
obj: { properties: { key1: { type: 'text' } } },
},
},
- // mock registry returns isMultiNamespace=true for 'shared' type
- shared: { properties: { name: { type: 'keyword' } } },
- // mock registry returns isNamespaceAgnostic=true for 'global' type
- global: { properties: { name: { type: 'keyword' } } },
- },
+ management: {
+ defaultSearchField: 'title',
+ },
+ });
+
+ registry.registerType({
+ name: 'shared',
+ hidden: false,
+ namespaceType: 'multiple',
+ mappings: {
+ properties: { name: { type: 'keyword' } },
+ },
+ management: {
+ defaultSearchField: 'name',
+ },
+ });
+
+ registry.registerType({
+ name: 'global',
+ hidden: false,
+ namespaceType: 'agnostic',
+ mappings: {
+ properties: { name: { type: 'keyword' } },
+ },
+ management: {
+ defaultSearchField: 'name',
+ },
+ });
};
-const ALL_TYPES = Object.keys(MAPPINGS.properties);
+
+const ALL_TYPES = ['pending', 'saved', 'shared', 'global'];
// get all possible subsets (combination) of all types
const ALL_TYPE_SUBSETS = ALL_TYPES.reduce(
(subsets, value) => subsets.concat(subsets.map((set) => [...set, value])),
@@ -51,48 +87,53 @@ const ALL_TYPE_SUBSETS = ALL_TYPES.reduce(
.filter((x) => x.length) // exclude empty set
.map((x) => (x.length === 1 ? x[0] : x)); // if a subset is a single string, destructure it
-const createTypeClause = (type: string, namespaces?: string[]) => {
- if (registry.isMultiNamespace(type)) {
- const array = [...(namespaces ?? ['default']), ALL_NAMESPACES_STRING];
- return {
- bool: {
- must: expect.arrayContaining([{ terms: { namespaces: array } }]),
- must_not: [{ exists: { field: 'namespace' } }],
- },
- };
- } else if (registry.isSingleNamespace(type)) {
- const nonDefaultNamespaces = namespaces?.filter((n) => n !== 'default') ?? [];
- const should: any = [];
- if (nonDefaultNamespaces.length > 0) {
- should.push({ terms: { namespace: nonDefaultNamespaces } });
- }
- if (namespaces?.includes('default')) {
- should.push({ bool: { must_not: [{ exists: { field: 'namespace' } }] } });
- }
- return {
- bool: {
- must: [{ term: { type } }],
- should: expect.arrayContaining(should),
- minimum_should_match: 1,
- must_not: [{ exists: { field: 'namespaces' } }],
- },
- };
- }
- // isNamespaceAgnostic
- return {
- bool: expect.objectContaining({
- must_not: [{ exists: { field: 'namespace' } }, { exists: { field: 'namespaces' } }],
- }),
- };
-};
-
/**
* Note: these tests cases are defined in the order they appear in the source code, for readability's sake
*/
describe('#getQueryParams', () => {
- const mappings = MAPPINGS;
+ let registry: SavedObjectTypeRegistry;
type Result = ReturnType;
+ beforeEach(() => {
+ registry = new SavedObjectTypeRegistry();
+ registerTypes(registry);
+ });
+
+ const createTypeClause = (type: string, namespaces?: string[]) => {
+ if (registry.isMultiNamespace(type)) {
+ const array = [...(namespaces ?? ['default']), ALL_NAMESPACES_STRING];
+ return {
+ bool: {
+ must: expect.arrayContaining([{ terms: { namespaces: array } }]),
+ must_not: [{ exists: { field: 'namespace' } }],
+ },
+ };
+ } else if (registry.isSingleNamespace(type)) {
+ const nonDefaultNamespaces = namespaces?.filter((n) => n !== 'default') ?? [];
+ const should: any = [];
+ if (nonDefaultNamespaces.length > 0) {
+ should.push({ terms: { namespace: nonDefaultNamespaces } });
+ }
+ if (namespaces?.includes('default')) {
+ should.push({ bool: { must_not: [{ exists: { field: 'namespace' } }] } });
+ }
+ return {
+ bool: {
+ must: [{ term: { type } }],
+ should: expect.arrayContaining(should),
+ minimum_should_match: 1,
+ must_not: [{ exists: { field: 'namespaces' } }],
+ },
+ };
+ }
+ // isNamespaceAgnostic
+ return {
+ bool: expect.objectContaining({
+ must_not: [{ exists: { field: 'namespace' } }, { exists: { field: 'namespaces' } }],
+ }),
+ };
+ };
+
describe('kueryNode filter clause', () => {
const expectResult = (result: Result, expected: any) => {
expect(result.query.bool.filter).toEqual(expect.arrayContaining([expected]));
@@ -100,13 +141,13 @@ describe('#getQueryParams', () => {
describe('`kueryNode` parameter', () => {
it('does not include the clause when `kueryNode` is not specified', () => {
- const result = getQueryParams({ mappings, registry, kueryNode: undefined });
+ const result = getQueryParams({ registry, kueryNode: undefined });
expect(result.query.bool.filter).toHaveLength(1);
});
it('includes the specified Kuery clause', () => {
const test = (kueryNode: KueryNode) => {
- const result = getQueryParams({ mappings, registry, kueryNode });
+ const result = getQueryParams({ registry, kueryNode });
const expected = esKuery.toElasticsearchQuery(kueryNode);
expect(result.query.bool.filter).toHaveLength(2);
expectResult(result, expected);
@@ -165,7 +206,6 @@ describe('#getQueryParams', () => {
it('does not include the clause when `hasReference` is not specified', () => {
const result = getQueryParams({
- mappings,
registry,
hasReference: undefined,
});
@@ -176,7 +216,6 @@ describe('#getQueryParams', () => {
it('creates a should clause for specified reference when operator is `OR`', () => {
const hasReference = { id: 'foo', type: 'bar' };
const result = getQueryParams({
- mappings,
registry,
hasReference,
hasReferenceOperator: 'OR',
@@ -192,7 +231,6 @@ describe('#getQueryParams', () => {
it('creates a must clause for specified reference when operator is `AND`', () => {
const hasReference = { id: 'foo', type: 'bar' };
const result = getQueryParams({
- mappings,
registry,
hasReference,
hasReferenceOperator: 'AND',
@@ -210,7 +248,6 @@ describe('#getQueryParams', () => {
{ id: 'hello', type: 'dolly' },
];
const result = getQueryParams({
- mappings,
registry,
hasReference,
hasReferenceOperator: 'OR',
@@ -229,7 +266,6 @@ describe('#getQueryParams', () => {
{ id: 'hello', type: 'dolly' },
];
const result = getQueryParams({
- mappings,
registry,
hasReference,
hasReferenceOperator: 'AND',
@@ -244,7 +280,6 @@ describe('#getQueryParams', () => {
it('defaults to `OR` when operator is not specified', () => {
const hasReference = { id: 'foo', type: 'bar' };
const result = getQueryParams({
- mappings,
registry,
hasReference,
});
@@ -278,14 +313,13 @@ describe('#getQueryParams', () => {
};
it('searches for all known types when `type` is not specified', () => {
- const result = getQueryParams({ mappings, registry, type: undefined });
+ const result = getQueryParams({ registry, type: undefined });
expectResult(result, ...ALL_TYPES);
});
it('searches for specified type/s', () => {
const test = (typeOrTypes: string | string[]) => {
const result = getQueryParams({
- mappings,
registry,
type: typeOrTypes,
});
@@ -309,18 +343,17 @@ describe('#getQueryParams', () => {
const test = (namespaces?: string[]) => {
for (const typeOrTypes of ALL_TYPE_SUBSETS) {
- const result = getQueryParams({ mappings, registry, type: typeOrTypes, namespaces });
+ const result = getQueryParams({ registry, type: typeOrTypes, namespaces });
const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes];
expectResult(result, ...types.map((x) => createTypeClause(x, namespaces)));
}
// also test with no specified type/s
- const result = getQueryParams({ mappings, registry, type: undefined, namespaces });
+ const result = getQueryParams({ registry, type: undefined, namespaces });
expectResult(result, ...ALL_TYPES.map((x) => createTypeClause(x, namespaces)));
};
it('normalizes and deduplicates provided namespaces', () => {
const result = getQueryParams({
- mappings,
registry,
search: '*',
namespaces: ['foo', '*', 'foo', 'bar', 'default'],
@@ -360,7 +393,6 @@ describe('#getQueryParams', () => {
it('supersedes `type` and `namespaces` parameters', () => {
const result = getQueryParams({
- mappings,
registry,
type: ['pending', 'saved', 'shared', 'global'],
namespaces: ['foo', 'bar', 'default'],
@@ -381,148 +413,266 @@ describe('#getQueryParams', () => {
});
});
- describe('search clause (query.bool.must.simple_query_string)', () => {
- const search = 'foo*';
+ describe('search clause (query.bool)', () => {
+ describe('when using simple search (query.bool.must.simple_query_string)', () => {
+ const search = 'foo';
- const expectResult = (result: Result, sqsClause: any) => {
- expect(result.query.bool.must).toEqual([{ simple_query_string: sqsClause }]);
- };
+ const expectResult = (result: Result, sqsClause: any) => {
+ expect(result.query.bool.must).toEqual([{ simple_query_string: sqsClause }]);
+ };
- describe('`search` parameter', () => {
- it('does not include clause when `search` is not specified', () => {
- const result = getQueryParams({
- mappings,
- registry,
- search: undefined,
+ describe('`search` parameter', () => {
+ it('does not include clause when `search` is not specified', () => {
+ const result = getQueryParams({
+ registry,
+ search: undefined,
+ });
+ expect(result.query.bool.must).toBeUndefined();
});
- expect(result.query.bool.must).toBeUndefined();
- });
- it('creates a clause with query for specified search', () => {
- const result = getQueryParams({
- mappings,
- registry,
- search,
+ it('creates a clause with query for specified search', () => {
+ const result = getQueryParams({
+ registry,
+ search,
+ });
+ expectResult(result, expect.objectContaining({ query: search }));
});
- expectResult(result, expect.objectContaining({ query: search }));
});
- });
- describe('`searchFields` and `rootSearchFields` parameters', () => {
- const getExpectedFields = (searchFields: string[], typeOrTypes: string | string[]) => {
- const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes];
- return searchFields.map((x) => types.map((y) => `${y}.${x}`)).flat();
- };
+ describe('`searchFields` and `rootSearchFields` parameters', () => {
+ const getExpectedFields = (searchFields: string[], typeOrTypes: string | string[]) => {
+ const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes];
+ return searchFields.map((x) => types.map((y) => `${y}.${x}`)).flat();
+ };
- const test = ({
- searchFields,
- rootSearchFields,
- }: {
- searchFields?: string[];
- rootSearchFields?: string[];
- }) => {
- for (const typeOrTypes of ALL_TYPE_SUBSETS) {
+ const test = ({
+ searchFields,
+ rootSearchFields,
+ }: {
+ searchFields?: string[];
+ rootSearchFields?: string[];
+ }) => {
+ for (const typeOrTypes of ALL_TYPE_SUBSETS) {
+ const result = getQueryParams({
+ registry,
+ type: typeOrTypes,
+ search,
+ searchFields,
+ rootSearchFields,
+ });
+ let fields = rootSearchFields || [];
+ if (searchFields) {
+ fields = fields.concat(getExpectedFields(searchFields, typeOrTypes));
+ }
+ expectResult(result, expect.objectContaining({ fields }));
+ }
+ // also test with no specified type/s
const result = getQueryParams({
- mappings,
registry,
- type: typeOrTypes,
+ type: undefined,
search,
searchFields,
rootSearchFields,
});
let fields = rootSearchFields || [];
if (searchFields) {
- fields = fields.concat(getExpectedFields(searchFields, typeOrTypes));
+ fields = fields.concat(getExpectedFields(searchFields, ALL_TYPES));
}
expectResult(result, expect.objectContaining({ fields }));
- }
- // also test with no specified type/s
- const result = getQueryParams({
- mappings,
- registry,
- type: undefined,
- search,
- searchFields,
- rootSearchFields,
+ };
+
+ it('throws an error if a raw search field contains a "." character', () => {
+ expect(() =>
+ getQueryParams({
+ registry,
+ type: undefined,
+ search,
+ searchFields: undefined,
+ rootSearchFields: ['foo', 'bar.baz'],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"rootSearchFields entry \\"bar.baz\\" is invalid: cannot contain \\".\\" character"`
+ );
});
- let fields = rootSearchFields || [];
- if (searchFields) {
- fields = fields.concat(getExpectedFields(searchFields, ALL_TYPES));
- }
- expectResult(result, expect.objectContaining({ fields }));
- };
- it('throws an error if a raw search field contains a "." character', () => {
- expect(() =>
- getQueryParams({
- mappings,
+ it('includes lenient flag and all fields when `searchFields` and `rootSearchFields` are not specified', () => {
+ const result = getQueryParams({
registry,
- type: undefined,
search,
searchFields: undefined,
- rootSearchFields: ['foo', 'bar.baz'],
- })
- ).toThrowErrorMatchingInlineSnapshot(
- `"rootSearchFields entry \\"bar.baz\\" is invalid: cannot contain \\".\\" character"`
- );
+ rootSearchFields: undefined,
+ });
+ expectResult(result, expect.objectContaining({ lenient: true, fields: ['*'] }));
+ });
+
+ it('includes specified search fields for appropriate type/s', () => {
+ test({ searchFields: ['title'] });
+ });
+
+ it('supports boosting', () => {
+ test({ searchFields: ['title^3'] });
+ });
+
+ it('supports multiple search fields', () => {
+ test({ searchFields: ['title, title.raw'] });
+ });
+
+ it('includes specified raw search fields', () => {
+ test({ rootSearchFields: ['_id'] });
+ });
+
+ it('supports multiple raw search fields', () => {
+ test({ rootSearchFields: ['_id', 'originId'] });
+ });
+
+ it('supports search fields and raw search fields', () => {
+ test({ searchFields: ['title'], rootSearchFields: ['_id'] });
+ });
});
- it('includes lenient flag and all fields when `searchFields` and `rootSearchFields` are not specified', () => {
- const result = getQueryParams({
- mappings,
+ describe('`defaultSearchOperator` parameter', () => {
+ it('does not include default_operator when `defaultSearchOperator` is not specified', () => {
+ const result = getQueryParams({
+ registry,
+ search,
+ defaultSearchOperator: undefined,
+ });
+ expectResult(
+ result,
+ expect.not.objectContaining({ default_operator: expect.anything() })
+ );
+ });
+
+ it('includes specified default operator', () => {
+ const defaultSearchOperator = 'AND';
+ const result = getQueryParams({
+ registry,
+ search,
+ defaultSearchOperator,
+ });
+ expectResult(
+ result,
+ expect.objectContaining({ default_operator: defaultSearchOperator })
+ );
+ });
+ });
+ });
+
+ describe('when using prefix search (query.bool.should)', () => {
+ const searchQuery = 'foo*';
+
+ const getQueryParamForSearch = ({
+ search,
+ searchFields,
+ type,
+ }: {
+ search?: string;
+ searchFields?: string[];
+ type?: string[];
+ }) =>
+ getQueryParams({
registry,
search,
- searchFields: undefined,
- rootSearchFields: undefined,
+ searchFields,
+ type,
});
- expectResult(result, expect.objectContaining({ lenient: true, fields: ['*'] }));
- });
- it('includes specified search fields for appropriate type/s', () => {
- test({ searchFields: ['title'] });
- });
+ it('uses a `should` clause instead of `must`', () => {
+ const result = getQueryParamForSearch({ search: searchQuery, searchFields: ['title'] });
- it('supports boosting', () => {
- test({ searchFields: ['title^3'] });
+ expect(result.query.bool.must).toBeUndefined();
+ expect(result.query.bool.should).toEqual(expect.any(Array));
+ expect(result.query.bool.should.length).toBeGreaterThanOrEqual(1);
+ expect(result.query.bool.minimum_should_match).toBe(1);
});
-
- it('supports multiple search fields', () => {
- test({ searchFields: ['title, title.raw'] });
+ it('includes the `simple_query_string` in the `should` clauses', () => {
+ const result = getQueryParamForSearch({ search: searchQuery, searchFields: ['title'] });
+ expect(result.query.bool.should[0]).toEqual({
+ simple_query_string: expect.objectContaining({
+ query: searchQuery,
+ }),
+ });
});
- it('includes specified raw search fields', () => {
- test({ rootSearchFields: ['_id'] });
+ it('adds a should clause for each `searchFields` / `type` tuple', () => {
+ const result = getQueryParamForSearch({
+ search: searchQuery,
+ searchFields: ['title', 'desc'],
+ type: ['saved', 'pending'],
+ });
+ const shouldClauses = result.query.bool.should;
+
+ expect(shouldClauses.length).toBe(5);
+
+ const mppClauses = shouldClauses.slice(1);
+
+ expect(
+ mppClauses.map((clause: any) => Object.keys(clause.match_phrase_prefix)[0])
+ ).toEqual(['saved.title', 'pending.title', 'saved.desc', 'pending.desc']);
});
- it('supports multiple raw search fields', () => {
- test({ rootSearchFields: ['_id', 'originId'] });
+ it('uses all registered types when `type` is not provided', () => {
+ const result = getQueryParamForSearch({
+ search: searchQuery,
+ searchFields: ['title'],
+ type: undefined,
+ });
+ const shouldClauses = result.query.bool.should;
+
+ expect(shouldClauses.length).toBe(5);
+
+ const mppClauses = shouldClauses.slice(1);
+
+ expect(
+ mppClauses.map((clause: any) => Object.keys(clause.match_phrase_prefix)[0])
+ ).toEqual(['pending.title', 'saved.title', 'shared.title', 'global.title']);
});
- it('supports search fields and raw search fields', () => {
- test({ searchFields: ['title'], rootSearchFields: ['_id'] });
+ it('removes the prefix search wildcard from the query', () => {
+ const result = getQueryParamForSearch({
+ search: searchQuery,
+ searchFields: ['title'],
+ type: ['saved'],
+ });
+ const shouldClauses = result.query.bool.should;
+ const mppClauses = shouldClauses.slice(1);
+
+ expect(mppClauses[0].match_phrase_prefix['saved.title'].query).toEqual('foo');
});
- });
- describe('`defaultSearchOperator` parameter', () => {
- it('does not include default_operator when `defaultSearchOperator` is not specified', () => {
- const result = getQueryParams({
- mappings,
- registry,
- search,
- defaultSearchOperator: undefined,
+ it("defaults to the type's default search field when `searchFields` is not specified", () => {
+ const result = getQueryParamForSearch({
+ search: searchQuery,
+ searchFields: undefined,
+ type: ['saved', 'global'],
});
- expectResult(result, expect.not.objectContaining({ default_operator: expect.anything() }));
+ const shouldClauses = result.query.bool.should;
+
+ expect(shouldClauses.length).toBe(3);
+
+ const mppClauses = shouldClauses.slice(1);
+
+ expect(
+ mppClauses.map((clause: any) => Object.keys(clause.match_phrase_prefix)[0])
+ ).toEqual(['saved.title', 'global.name']);
});
- it('includes specified default operator', () => {
- const defaultSearchOperator = 'AND';
- const result = getQueryParams({
- mappings,
- registry,
- search,
- defaultSearchOperator,
+ it('supports boosting', () => {
+ const result = getQueryParamForSearch({
+ search: searchQuery,
+ searchFields: ['title^3', 'description'],
+ type: ['saved'],
});
- expectResult(result, expect.objectContaining({ default_operator: defaultSearchOperator }));
+ const shouldClauses = result.query.bool.should;
+
+ expect(shouldClauses.length).toBe(3);
+
+ const mppClauses = shouldClauses.slice(1);
+
+ expect(mppClauses.map((clause: any) => clause.match_phrase_prefix)).toEqual([
+ { 'saved.title': { query: 'foo', boost: 3 } },
+ { 'saved.description': { query: 'foo', boost: 1 } },
+ ]);
});
});
});
@@ -532,7 +682,6 @@ describe('#getQueryParams', () => {
it(`throws for ${type} when namespaces is an empty array`, () => {
expect(() =>
getQueryParams({
- mappings,
registry,
namespaces: [],
})
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts
index 8d4fe13b9bede..f73777c4f454f 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts
@@ -20,7 +20,6 @@
import { esKuery } from '../../../es_query';
type KueryNode = any;
-import { getRootPropertiesObjects, IndexMapping } from '../../../mappings';
import { ISavedObjectTypeRegistry } from '../../../saved_objects_type_registry';
import { ALL_NAMESPACES_STRING, DEFAULT_NAMESPACE_STRING } from '../utils';
@@ -28,22 +27,17 @@ import { ALL_NAMESPACES_STRING, DEFAULT_NAMESPACE_STRING } from '../utils';
* Gets the types based on the type. Uses mappings to support
* null type (all types), a single type string or an array
*/
-function getTypes(mappings: IndexMapping, type?: string | string[]) {
+function getTypes(registry: ISavedObjectTypeRegistry, type?: string | string[]) {
if (!type) {
- return Object.keys(getRootPropertiesObjects(mappings));
+ return registry.getAllTypes().map((registeredType) => registeredType.name);
}
-
- if (Array.isArray(type)) {
- return type;
- }
-
- return [type];
+ return Array.isArray(type) ? type : [type];
}
/**
* Get the field params based on the types, searchFields, and rootSearchFields
*/
-function getFieldsForTypes(
+function getSimpleQueryStringTypeFields(
types: string[],
searchFields: string[] = [],
rootSearchFields: string[] = []
@@ -130,7 +124,6 @@ export interface HasReferenceQueryParams {
export type SearchOperator = 'AND' | 'OR';
interface QueryParams {
- mappings: IndexMapping;
registry: ISavedObjectTypeRegistry;
namespaces?: string[];
type?: string | string[];
@@ -188,11 +181,26 @@ export function getClauseForReference(reference: HasReferenceQueryParams) {
};
}
+// A de-duplicated set of namespaces makes for a more efficient query.
+//
+// Additionally, we treat the `*` namespace as the `default` namespace.
+// In the Default Distribution, the `*` is automatically expanded to include all available namespaces.
+// However, the OSS distribution (and certain configurations of the Default Distribution) can allow the `*`
+// to pass through to the SO Repository, and eventually to this module. When this happens, we translate to `default`,
+// since that is consistent with how a single-namespace search behaves in the OSS distribution. Leaving the wildcard in place
+// would result in no results being returned, as the wildcard is treated as a literal, and not _actually_ as a wildcard.
+// We had a good discussion around the tradeoffs here: https://github.com/elastic/kibana/pull/67644#discussion_r441055716
+const normalizeNamespaces = (namespacesToNormalize?: string[]) =>
+ namespacesToNormalize
+ ? Array.from(
+ new Set(namespacesToNormalize.map((x) => (x === '*' ? DEFAULT_NAMESPACE_STRING : x)))
+ )
+ : undefined;
+
/**
* Get the "query" related keys for the search body
*/
export function getQueryParams({
- mappings,
registry,
namespaces,
type,
@@ -206,7 +214,7 @@ export function getQueryParams({
kueryNode,
}: QueryParams) {
const types = getTypes(
- mappings,
+ registry,
typeToNamespacesMap ? Array.from(typeToNamespacesMap.keys()) : type
);
@@ -214,28 +222,10 @@ export function getQueryParams({
hasReference = [hasReference];
}
- // A de-duplicated set of namespaces makes for a more effecient query.
- //
- // Additonally, we treat the `*` namespace as the `default` namespace.
- // In the Default Distribution, the `*` is automatically expanded to include all available namespaces.
- // However, the OSS distribution (and certain configurations of the Default Distribution) can allow the `*`
- // to pass through to the SO Repository, and eventually to this module. When this happens, we translate to `default`,
- // since that is consistent with how a single-namespace search behaves in the OSS distribution. Leaving the wildcard in place
- // would result in no results being returned, as the wildcard is treated as a literal, and not _actually_ as a wildcard.
- // We had a good discussion around the tradeoffs here: https://github.com/elastic/kibana/pull/67644#discussion_r441055716
- const normalizeNamespaces = (namespacesToNormalize?: string[]) =>
- namespacesToNormalize
- ? Array.from(
- new Set(namespacesToNormalize.map((x) => (x === '*' ? DEFAULT_NAMESPACE_STRING : x)))
- )
- : undefined;
-
const bool: any = {
filter: [
...(kueryNode != null ? [esKuery.toElasticsearchQuery(kueryNode)] : []),
- ...(hasReference && hasReference.length
- ? [getReferencesFilter(hasReference, hasReferenceOperator)]
- : []),
+ ...(hasReference?.length ? [getReferencesFilter(hasReference, hasReferenceOperator)] : []),
{
bool: {
should: types.map((shouldType) => {
@@ -251,16 +241,133 @@ export function getQueryParams({
};
if (search) {
- bool.must = [
- {
- simple_query_string: {
- query: search,
- ...getFieldsForTypes(types, searchFields, rootSearchFields),
- ...(defaultSearchOperator ? { default_operator: defaultSearchOperator } : {}),
- },
- },
- ];
+ const useMatchPhrasePrefix = shouldUseMatchPhrasePrefix(search);
+ const simpleQueryStringClause = getSimpleQueryStringClause({
+ search,
+ types,
+ searchFields,
+ rootSearchFields,
+ defaultSearchOperator,
+ });
+
+ if (useMatchPhrasePrefix) {
+ bool.should = [
+ simpleQueryStringClause,
+ ...getMatchPhrasePrefixClauses({ search, searchFields, types, registry }),
+ ];
+ bool.minimum_should_match = 1;
+ } else {
+ bool.must = [simpleQueryStringClause];
+ }
}
return { query: { bool } };
}
+
+// we only want to add match_phrase_prefix clauses
+// if the search is a prefix search
+const shouldUseMatchPhrasePrefix = (search: string): boolean => {
+ return search.trim().endsWith('*');
+};
+
+const getMatchPhrasePrefixClauses = ({
+ search,
+ searchFields,
+ registry,
+ types,
+}: {
+ search: string;
+ searchFields?: string[];
+ types: string[];
+ registry: ISavedObjectTypeRegistry;
+}) => {
+ // need to remove the prefix search operator
+ const query = search.replace(/[*]$/, '');
+ const mppFields = getMatchPhrasePrefixFields({ searchFields, types, registry });
+ return mppFields.map(({ field, boost }) => {
+ return {
+ match_phrase_prefix: {
+ [field]: {
+ query,
+ boost,
+ },
+ },
+ };
+ });
+};
+
+interface FieldWithBoost {
+ field: string;
+ boost?: number;
+}
+
+const getMatchPhrasePrefixFields = ({
+ searchFields = [],
+ types,
+ registry,
+}: {
+ searchFields?: string[];
+ types: string[];
+ registry: ISavedObjectTypeRegistry;
+}): FieldWithBoost[] => {
+ const output: FieldWithBoost[] = [];
+
+ searchFields = searchFields.filter((field) => field !== '*');
+ let fields: string[];
+ if (searchFields.length === 0) {
+ fields = types.reduce((typeFields, type) => {
+ const defaultSearchField = registry.getType(type)?.management?.defaultSearchField;
+ if (defaultSearchField) {
+ return [...typeFields, `${type}.${defaultSearchField}`];
+ }
+ return typeFields;
+ }, [] as string[]);
+ } else {
+ fields = [];
+ for (const field of searchFields) {
+ fields = fields.concat(types.map((type) => `${type}.${field}`));
+ }
+ }
+
+ fields.forEach((rawField) => {
+ const [field, rawBoost] = rawField.split('^');
+ let boost: number = 1;
+ if (rawBoost) {
+ try {
+ boost = parseInt(rawBoost, 10);
+ } catch (e) {
+ boost = 1;
+ }
+ }
+ if (isNaN(boost)) {
+ boost = 1;
+ }
+ output.push({
+ field,
+ boost,
+ });
+ });
+ return output;
+};
+
+const getSimpleQueryStringClause = ({
+ search,
+ types,
+ searchFields,
+ rootSearchFields,
+ defaultSearchOperator,
+}: {
+ search: string;
+ types: string[];
+ searchFields?: string[];
+ rootSearchFields?: string[];
+ defaultSearchOperator?: SearchOperator;
+}) => {
+ return {
+ simple_query_string: {
+ query: search,
+ ...getSimpleQueryStringTypeFields(types, searchFields, rootSearchFields),
+ ...(defaultSearchOperator ? { default_operator: defaultSearchOperator } : {}),
+ },
+ };
+};
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 a9f26f71a3f2b..3522ab9ef1736 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
@@ -76,7 +76,6 @@ describe('getSearchDsl', () => {
getSearchDsl(mappings, registry, opts);
expect(getQueryParams).toHaveBeenCalledTimes(1);
expect(getQueryParams).toHaveBeenCalledWith({
- mappings,
registry,
namespaces: opts.namespaces,
type: opts.type,
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 d5da82e5617be..bddecc4d7f649 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
@@ -71,7 +71,6 @@ export function getSearchDsl(
return {
...getQueryParams({
- mappings,
registry,
namespaces,
type,
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts
index 9085ae07bbe3e..145901509d1c5 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts
@@ -196,6 +196,24 @@ describe('IndexPattern', () => {
});
});
+ describe('getFormatterForField', () => {
+ test('should return the default one for empty objects', () => {
+ indexPattern.setFieldFormat('scriptedFieldWithEmptyFormatter', {});
+ expect(
+ indexPattern.getFormatterForField({
+ name: 'scriptedFieldWithEmptyFormatter',
+ type: 'number',
+ esTypes: ['long'],
+ })
+ ).toEqual(
+ expect.objectContaining({
+ convert: expect.any(Function),
+ getConverterFor: expect.any(Function),
+ })
+ );
+ });
+ });
+
describe('toSpec', () => {
test('should match snapshot', () => {
const formatter = {
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
index a0f27078543a9..4508d7b1d9082 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
@@ -291,15 +291,15 @@ export class IndexPattern implements IIndexPattern {
getFormatterForField(
field: IndexPatternField | IndexPatternField['spec'] | IFieldType
): FieldFormat {
- const formatSpec = this.fieldFormatMap[field.name];
- if (formatSpec) {
- return this.fieldFormats.getInstance(formatSpec.id, formatSpec.params);
- } else {
- return this.fieldFormats.getDefaultInstance(
- field.type as KBN_FIELD_TYPES,
- field.esTypes as ES_FIELD_TYPES[]
- );
+ const fieldFormat = this.getFormatterForFieldNoDefault(field.name);
+ if (fieldFormat) {
+ return fieldFormat;
}
+
+ return this.fieldFormats.getDefaultInstance(
+ field.type as KBN_FIELD_TYPES,
+ field.esTypes as ES_FIELD_TYPES[]
+ );
}
/**
@@ -308,7 +308,7 @@ export class IndexPattern implements IIndexPattern {
*/
getFormatterForFieldNoDefault(fieldname: string) {
const formatSpec = this.fieldFormatMap[fieldname];
- if (formatSpec) {
+ if (formatSpec?.id) {
return this.fieldFormats.getInstance(formatSpec.id, formatSpec.params);
}
}
diff --git a/src/plugins/vis_type_timeseries/common/types.ts b/src/plugins/vis_type_timeseries/common/types.ts
index 4520069244527..8973060848b41 100644
--- a/src/plugins/vis_type_timeseries/common/types.ts
+++ b/src/plugins/vis_type_timeseries/common/types.ts
@@ -18,8 +18,9 @@
*/
import { TypeOf } from '@kbn/config-schema';
-import { metricsItems, panel, seriesItems } from './vis_schema';
+import { metricsItems, panel, seriesItems, visPayloadSchema } from './vis_schema';
export type SeriesItemsSchema = TypeOf;
export type MetricsItemsSchema = TypeOf;
export type PanelSchema = TypeOf;
+export type VisPayload = TypeOf;
diff --git a/src/plugins/vis_type_timeseries/common/vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts
index 40f776050617e..27f09fb574b0f 100644
--- a/src/plugins/vis_type_timeseries/common/vis_schema.ts
+++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts
@@ -273,4 +273,5 @@ export const visPayloadSchema = schema.object({
min: stringRequired,
max: stringRequired,
}),
+ sessionId: schema.maybe(schema.string()),
});
diff --git a/src/plugins/vis_type_timeseries/public/request_handler.js b/src/plugins/vis_type_timeseries/public/request_handler.js
index e33d0e254f609..12b7f3d417ef6 100644
--- a/src/plugins/vis_type_timeseries/public/request_handler.js
+++ b/src/plugins/vis_type_timeseries/public/request_handler.js
@@ -32,7 +32,8 @@ export const metricsRequestHandler = async ({
const config = getUISettings();
const timezone = getTimezone(config);
const uiStateObj = uiState.get(visParams.type, {});
- const parsedTimeRange = getDataStart().query.timefilter.timefilter.calculateBounds(timeRange);
+ const dataSearch = getDataStart();
+ const parsedTimeRange = dataSearch.query.timefilter.timefilter.calculateBounds(timeRange);
const scaledDataFormat = config.get('dateFormat:scaled');
const dateFormat = config.get('dateFormat');
@@ -53,6 +54,7 @@ export const metricsRequestHandler = async ({
panels: [visParams],
state: uiStateObj,
savedObjectId: savedObjectId || 'unsaved',
+ sessionId: dataSearch.search.session.getSessionId(),
}),
});
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js
index 9710f7daf69b6..2c38e883cd69f 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js
@@ -28,6 +28,7 @@ describe('AbstractSearchStrategy', () => {
beforeEach(() => {
mockedFields = {};
req = {
+ payload: {},
pre: {
indexPatternsService: {
getFieldsForWildcard: jest.fn().mockReturnValue(mockedFields),
@@ -60,6 +61,9 @@ describe('AbstractSearchStrategy', () => {
const responses = await abstractSearchStrategy.search(
{
+ payload: {
+ sessionId: 1,
+ },
requestContext: {
search: { search: searchFn },
},
@@ -76,7 +80,9 @@ describe('AbstractSearchStrategy', () => {
},
indexType: undefined,
},
- {}
+ {
+ sessionId: 1,
+ }
);
});
});
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts
index eb22fcb1dd689..b1e21edf8b588 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts
@@ -23,8 +23,10 @@ import {
IUiSettingsClient,
SavedObjectsClientContract,
} from 'kibana/server';
+
import { Framework } from '../../../plugin';
import { IndexPatternsFetcher } from '../../../../../data/server';
+import { VisPayload } from '../../../../common/types';
/**
* ReqFacade is a regular KibanaRequest object extended with additional service
@@ -32,17 +34,17 @@ import { IndexPatternsFetcher } from '../../../../../data/server';
*
* This will be replaced by standard KibanaRequest and RequestContext objects in a later version.
*/
-export type ReqFacade = FakeRequest & {
+export interface ReqFacade extends FakeRequest {
requestContext: RequestHandlerContext;
framework: Framework;
- payload: unknown;
+ payload: T;
pre: {
indexPatternsService?: IndexPatternsFetcher;
};
getUiSettingsService: () => IUiSettingsClient;
getSavedObjectsClient: () => SavedObjectsClientContract;
getEsShardTimeout: () => Promise;
-};
+}
export class AbstractSearchStrategy {
public indexType?: string;
@@ -53,8 +55,10 @@ export class AbstractSearchStrategy {
this.additionalParams = additionalParams;
}
- async search(req: ReqFacade, bodies: any[], options = {}) {
+ async search(req: ReqFacade, bodies: any[], options = {}) {
const requests: any[] = [];
+ const { sessionId } = req.payload;
+
bodies.forEach((body) => {
requests.push(
req.requestContext
@@ -67,6 +71,7 @@ export class AbstractSearchStrategy {
indexType: this.indexType,
},
{
+ sessionId,
...options,
}
)
diff --git a/test/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js
index c2e36b4a669ff..e5da46644672b 100644
--- a/test/api_integration/apis/saved_objects/find.js
+++ b/test/api_integration/apis/saved_objects/find.js
@@ -334,6 +334,70 @@ export default function ({ getService }) {
});
});
+ describe('searching for special characters', () => {
+ before(() => esArchiver.load('saved_objects/find_edgecases'));
+ after(() => esArchiver.unload('saved_objects/find_edgecases'));
+
+ it('can search for objects with dashes', async () =>
+ await supertest
+ .get('/api/saved_objects/_find')
+ .query({
+ type: 'visualization',
+ search_fields: 'title',
+ search: 'my-vis*',
+ })
+ .expect(200)
+ .then((resp) => {
+ const savedObjects = resp.body.saved_objects;
+ expect(savedObjects.map((so) => so.attributes.title)).to.eql(['my-visualization']);
+ }));
+
+ it('can search with the prefix search character just after a special one', async () =>
+ await supertest
+ .get('/api/saved_objects/_find')
+ .query({
+ type: 'visualization',
+ search_fields: 'title',
+ search: 'my-*',
+ })
+ .expect(200)
+ .then((resp) => {
+ const savedObjects = resp.body.saved_objects;
+ expect(savedObjects.map((so) => so.attributes.title)).to.eql(['my-visualization']);
+ }));
+
+ it('can search for objects with asterisk', async () =>
+ await supertest
+ .get('/api/saved_objects/_find')
+ .query({
+ type: 'visualization',
+ search_fields: 'title',
+ search: 'some*vi*',
+ })
+ .expect(200)
+ .then((resp) => {
+ const savedObjects = resp.body.saved_objects;
+ expect(savedObjects.map((so) => so.attributes.title)).to.eql(['some*visualization']);
+ }));
+
+ it('can still search tokens by prefix', async () =>
+ await supertest
+ .get('/api/saved_objects/_find')
+ .query({
+ type: 'visualization',
+ search_fields: 'title',
+ search: 'visuali*',
+ })
+ .expect(200)
+ .then((resp) => {
+ const savedObjects = resp.body.saved_objects;
+ expect(savedObjects.map((so) => so.attributes.title)).to.eql([
+ 'my-visualization',
+ 'some*visualization',
+ ]);
+ }));
+ });
+
describe('without kibana index', () => {
before(
async () =>
diff --git a/test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/data.json b/test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/data.json
new file mode 100644
index 0000000000000..0c8b35fd3f499
--- /dev/null
+++ b/test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/data.json
@@ -0,0 +1,93 @@
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "visualization:title-with-dash",
+ "source": {
+ "type": "visualization",
+ "updated_at": "2017-09-21T18:51:23.794Z",
+ "visualization": {
+ "title": "my-visualization",
+ "visState": "{}",
+ "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}",
+ "description": "",
+ "version": 1,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ }
+ },
+ "references": []
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "visualization:title-with-asterisk",
+ "source": {
+ "type": "visualization",
+ "updated_at": "2017-09-21T18:51:23.794Z",
+ "visualization": {
+ "title": "some*visualization",
+ "visState": "{}",
+ "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}",
+ "description": "",
+ "version": 1,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ }
+ },
+ "references": []
+ }
+ }
+}
+
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "visualization:noise-1",
+ "source": {
+ "type": "visualization",
+ "updated_at": "2017-09-21T18:51:23.794Z",
+ "visualization": {
+ "title": "Just some noise in the dataset",
+ "visState": "{}",
+ "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}",
+ "description": "",
+ "version": 1,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ }
+ },
+ "references": []
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "visualization:noise-2",
+ "source": {
+ "type": "visualization",
+ "updated_at": "2017-09-21T18:51:23.794Z",
+ "visualization": {
+ "title": "Just some noise in the dataset",
+ "visState": "{}",
+ "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}",
+ "description": "",
+ "version": 1,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ }
+ },
+ "references": []
+ }
+ }
+}
+
diff --git a/test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/mappings.json b/test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/mappings.json
new file mode 100644
index 0000000000000..e601c43431437
--- /dev/null
+++ b/test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/mappings.json
@@ -0,0 +1,267 @@
+{
+ "type": "index",
+ "value": {
+ "index": ".kibana",
+ "settings": {
+ "index": {
+ "number_of_shards": "1",
+ "number_of_replicas": "1"
+ }
+ },
+ "mappings": {
+ "dynamic": "strict",
+ "properties": {
+ "config": {
+ "dynamic": "true",
+ "properties": {
+ "buildNum": {
+ "type": "keyword"
+ },
+ "defaultIndex": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ }
+ }
+ }
+ }
+ },
+ "dashboard": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "optionsJSON": {
+ "type": "text"
+ },
+ "panelsJSON": {
+ "type": "text"
+ },
+ "refreshInterval": {
+ "properties": {
+ "display": {
+ "type": "keyword"
+ },
+ "pause": {
+ "type": "boolean"
+ },
+ "section": {
+ "type": "integer"
+ },
+ "value": {
+ "type": "integer"
+ }
+ }
+ },
+ "timeFrom": {
+ "type": "keyword"
+ },
+ "timeRestore": {
+ "type": "boolean"
+ },
+ "timeTo": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "index-pattern": {
+ "properties": {
+ "fieldFormatMap": {
+ "type": "text"
+ },
+ "fields": {
+ "type": "text"
+ },
+ "intervalName": {
+ "type": "keyword"
+ },
+ "notExpandable": {
+ "type": "boolean"
+ },
+ "sourceFilters": {
+ "type": "text"
+ },
+ "timeFieldName": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ }
+ }
+ },
+ "search": {
+ "properties": {
+ "columns": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "sort": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "server": {
+ "properties": {
+ "uuid": {
+ "type": "keyword"
+ }
+ }
+ },
+ "timelion-sheet": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "timelion_chart_height": {
+ "type": "integer"
+ },
+ "timelion_columns": {
+ "type": "integer"
+ },
+ "timelion_interval": {
+ "type": "keyword"
+ },
+ "timelion_other_interval": {
+ "type": "keyword"
+ },
+ "timelion_rows": {
+ "type": "integer"
+ },
+ "timelion_sheet": {
+ "type": "text"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "namespace": {
+ "type": "keyword"
+ },
+ "references": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ },
+ "type": "nested"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "updated_at": {
+ "type": "date"
+ },
+ "url": {
+ "properties": {
+ "accessCount": {
+ "type": "long"
+ },
+ "accessDate": {
+ "type": "date"
+ },
+ "createDate": {
+ "type": "date"
+ },
+ "url": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 2048
+ }
+ }
+ }
+ }
+ },
+ "visualization": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "savedSearchId": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ },
+ "visState": {
+ "type": "text"
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js
index e597cc14654bc..3c9996ca44ff8 100644
--- a/test/functional/apps/discover/_discover.js
+++ b/test/functional/apps/discover/_discover.js
@@ -32,7 +32,8 @@ export default function ({ getService, getPageObjects }) {
defaultIndex: 'logstash-*',
};
- describe('discover test', function describeIndexTests() {
+ // Failing: See https://github.com/elastic/kibana/issues/82915
+ describe.skip('discover test', function describeIndexTests() {
before(async function () {
log.debug('load kibana index with default index pattern');
await esArchiver.load('discover');
diff --git a/x-pack/examples/alerting_example/public/alert_types/astros.tsx b/x-pack/examples/alerting_example/public/alert_types/astros.tsx
index 73c7dfea1263b..54f989b93e22f 100644
--- a/x-pack/examples/alerting_example/public/alert_types/astros.tsx
+++ b/x-pack/examples/alerting_example/public/alert_types/astros.tsx
@@ -127,9 +127,9 @@ export const PeopleinSpaceExpression: React.FunctionComponent
- errs.map((e) => (
-
+ Object.entries(errors).map(([field, errs]: [string, string[]], fieldIndex) =>
+ errs.map((e, index) => (
+
{field}: `: ${errs}`
))
diff --git a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts
index bb1cb0d97689b..d02406a23045e 100644
--- a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts
+++ b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts
@@ -5,25 +5,31 @@
*/
import uuid from 'uuid';
-import { range } from 'lodash';
+import { range, random } from 'lodash';
import { AlertType } from '../../../../plugins/alerts/server';
import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID } from '../../common/constants';
+const ACTION_GROUPS = [
+ { id: 'small', name: 'small' },
+ { id: 'medium', name: 'medium' },
+ { id: 'large', name: 'large' },
+];
+
export const alertType: AlertType = {
id: 'example.always-firing',
name: 'Always firing',
- actionGroups: [{ id: 'default', name: 'default' }],
- defaultActionGroupId: 'default',
+ actionGroups: ACTION_GROUPS,
+ defaultActionGroupId: 'small',
async executor({ services, params: { instances = DEFAULT_INSTANCES_TO_GENERATE }, state }) {
const count = (state.count ?? 0) + 1;
range(instances)
- .map(() => ({ id: uuid.v4() }))
- .forEach((instance: { id: string }) => {
+ .map(() => ({ id: uuid.v4(), tshirtSize: ACTION_GROUPS[random(0, 2)].id! }))
+ .forEach((instance: { id: string; tshirtSize: string }) => {
services
.alertInstanceFactory(instance.id)
.replaceState({ triggerdOnCycle: count })
- .scheduleActions('default');
+ .scheduleActions(instance.tshirtSize);
});
return {
diff --git a/x-pack/plugins/alerts/common/alert.ts b/x-pack/plugins/alerts/common/alert.ts
index 79e6bb8f2cbba..97a9a58400e38 100644
--- a/x-pack/plugins/alerts/common/alert.ts
+++ b/x-pack/plugins/alerts/common/alert.ts
@@ -20,13 +20,12 @@ export interface IntervalSchedule extends SavedObjectAttributes {
export const AlertExecutionStatusValues = ['ok', 'active', 'error', 'pending', 'unknown'] as const;
export type AlertExecutionStatuses = typeof AlertExecutionStatusValues[number];
-export const AlertExecutionStatusErrorReasonValues = [
- 'read',
- 'decrypt',
- 'execute',
- 'unknown',
-] as const;
-export type AlertExecutionStatusErrorReasons = typeof AlertExecutionStatusErrorReasonValues[number];
+export enum AlertExecutionStatusErrorReasons {
+ Read = 'read',
+ Decrypt = 'decrypt',
+ Execute = 'execute',
+ Unknown = 'unknown',
+}
export interface AlertExecutionStatus {
status: AlertExecutionStatuses;
@@ -74,3 +73,24 @@ export interface Alert {
}
export type SanitizedAlert = Omit;
+
+export enum HealthStatus {
+ OK = 'ok',
+ Warning = 'warn',
+ Error = 'error',
+}
+
+export interface AlertsHealth {
+ decryptionHealth: {
+ status: HealthStatus;
+ timestamp: string;
+ };
+ executionHealth: {
+ status: HealthStatus;
+ timestamp: string;
+ };
+ readHealth: {
+ status: HealthStatus;
+ timestamp: string;
+ };
+}
diff --git a/x-pack/plugins/alerts/common/index.ts b/x-pack/plugins/alerts/common/index.ts
index ab71f77a049f6..65aeec840da7e 100644
--- a/x-pack/plugins/alerts/common/index.ts
+++ b/x-pack/plugins/alerts/common/index.ts
@@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { AlertsHealth } from './alert';
+
export * from './alert';
export * from './alert_type';
export * from './alert_instance';
@@ -19,6 +21,7 @@ export interface ActionGroup {
export interface AlertingFrameworkHealth {
isSufficientlySecure: boolean;
hasPermanentEncryptionKey: boolean;
+ alertingFrameworkHeath: AlertsHealth;
}
export const BASE_ALERT_API_PATH = '/api/alerts';
diff --git a/x-pack/plugins/alerts/server/config.test.ts b/x-pack/plugins/alerts/server/config.test.ts
new file mode 100644
index 0000000000000..93aa3c38a0460
--- /dev/null
+++ b/x-pack/plugins/alerts/server/config.test.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { configSchema } from './config';
+
+describe('config validation', () => {
+ test('alerts defaults', () => {
+ const config: Record = {};
+ expect(configSchema.validate(config)).toMatchInlineSnapshot(`
+ Object {
+ "healthCheck": Object {
+ "interval": "60m",
+ },
+ }
+ `);
+ });
+});
diff --git a/x-pack/plugins/alerts/server/config.ts b/x-pack/plugins/alerts/server/config.ts
new file mode 100644
index 0000000000000..a6d2196a407b5
--- /dev/null
+++ b/x-pack/plugins/alerts/server/config.ts
@@ -0,0 +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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { schema, TypeOf } from '@kbn/config-schema';
+import { validateDurationSchema } from './lib';
+
+export const configSchema = schema.object({
+ healthCheck: schema.object({
+ interval: schema.string({ validate: validateDurationSchema, defaultValue: '60m' }),
+ }),
+});
+
+export type AlertsConfig = TypeOf;
diff --git a/x-pack/plugins/alerts/server/health/get_health.test.ts b/x-pack/plugins/alerts/server/health/get_health.test.ts
new file mode 100644
index 0000000000000..34517a89f04d9
--- /dev/null
+++ b/x-pack/plugins/alerts/server/health/get_health.test.ts
@@ -0,0 +1,221 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { savedObjectsRepositoryMock } from '../../../../../src/core/server/mocks';
+import { AlertExecutionStatusErrorReasons, HealthStatus } from '../types';
+import { getHealth } from './get_health';
+
+const savedObjectsRepository = savedObjectsRepositoryMock.create();
+
+describe('getHealth()', () => {
+ test('return true if some of alerts has a decryption error', async () => {
+ const lastExecutionDateError = new Date().toISOString();
+ const lastExecutionDate = new Date().toISOString();
+ savedObjectsRepository.find.mockResolvedValueOnce({
+ total: 1,
+ per_page: 1,
+ page: 1,
+ saved_objects: [
+ {
+ id: '1',
+ type: 'alert',
+ attributes: {
+ alertTypeId: 'myType',
+ schedule: { interval: '10s' },
+ params: {
+ bar: true,
+ },
+ createdAt: new Date().toISOString(),
+ actions: [
+ {
+ group: 'default',
+ actionRef: 'action_0',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ executionStatus: {
+ status: 'error',
+ lastExecutionDate: lastExecutionDateError,
+ error: {
+ reason: AlertExecutionStatusErrorReasons.Decrypt,
+ message: 'Failed decrypt',
+ },
+ },
+ },
+ score: 1,
+ references: [
+ {
+ name: 'action_0',
+ type: 'action',
+ id: '1',
+ },
+ ],
+ },
+ ],
+ });
+ savedObjectsRepository.find.mockResolvedValueOnce({
+ total: 0,
+ per_page: 10,
+ page: 1,
+ saved_objects: [],
+ });
+
+ savedObjectsRepository.find.mockResolvedValueOnce({
+ total: 0,
+ per_page: 10,
+ page: 1,
+ saved_objects: [],
+ });
+
+ savedObjectsRepository.find.mockResolvedValueOnce({
+ total: 1,
+ per_page: 1,
+ page: 1,
+ saved_objects: [
+ {
+ id: '2',
+ type: 'alert',
+ attributes: {
+ alertTypeId: 'myType',
+ schedule: { interval: '1s' },
+ params: {
+ bar: true,
+ },
+ createdAt: new Date().toISOString(),
+ actions: [],
+ executionStatus: {
+ status: 'ok',
+ lastExecutionDate,
+ },
+ },
+ score: 1,
+ references: [],
+ },
+ ],
+ });
+ const result = await getHealth(savedObjectsRepository);
+ expect(result).toStrictEqual({
+ executionHealth: {
+ status: HealthStatus.OK,
+ timestamp: lastExecutionDate,
+ },
+ readHealth: {
+ status: HealthStatus.OK,
+ timestamp: lastExecutionDate,
+ },
+ decryptionHealth: {
+ status: HealthStatus.Warning,
+ timestamp: lastExecutionDateError,
+ },
+ });
+ expect(savedObjectsRepository.find).toHaveBeenCalledTimes(4);
+ });
+
+ test('return false if no alerts with a decryption error', async () => {
+ const lastExecutionDateError = new Date().toISOString();
+ const lastExecutionDate = new Date().toISOString();
+ savedObjectsRepository.find.mockResolvedValueOnce({
+ total: 0,
+ per_page: 10,
+ page: 1,
+ saved_objects: [],
+ });
+
+ savedObjectsRepository.find.mockResolvedValueOnce({
+ total: 1,
+ per_page: 1,
+ page: 1,
+ saved_objects: [
+ {
+ id: '1',
+ type: 'alert',
+ attributes: {
+ alertTypeId: 'myType',
+ schedule: { interval: '10s' },
+ params: {
+ bar: true,
+ },
+ createdAt: new Date().toISOString(),
+ actions: [
+ {
+ group: 'default',
+ actionRef: 'action_0',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ executionStatus: {
+ status: 'error',
+ lastExecutionDate: lastExecutionDateError,
+ error: {
+ reason: AlertExecutionStatusErrorReasons.Execute,
+ message: 'Failed',
+ },
+ },
+ },
+ score: 1,
+ references: [
+ {
+ name: 'action_0',
+ type: 'action',
+ id: '1',
+ },
+ ],
+ },
+ ],
+ });
+ savedObjectsRepository.find.mockResolvedValueOnce({
+ total: 0,
+ per_page: 10,
+ page: 1,
+ saved_objects: [],
+ });
+
+ savedObjectsRepository.find.mockResolvedValueOnce({
+ total: 1,
+ per_page: 1,
+ page: 1,
+ saved_objects: [
+ {
+ id: '2',
+ type: 'alert',
+ attributes: {
+ alertTypeId: 'myType',
+ schedule: { interval: '1s' },
+ params: {
+ bar: true,
+ },
+ createdAt: new Date().toISOString(),
+ actions: [],
+ executionStatus: {
+ status: 'ok',
+ lastExecutionDate,
+ },
+ },
+ score: 1,
+ references: [],
+ },
+ ],
+ });
+ const result = await getHealth(savedObjectsRepository);
+ expect(result).toStrictEqual({
+ executionHealth: {
+ status: HealthStatus.Warning,
+ timestamp: lastExecutionDateError,
+ },
+ readHealth: {
+ status: HealthStatus.OK,
+ timestamp: lastExecutionDate,
+ },
+ decryptionHealth: {
+ status: HealthStatus.OK,
+ timestamp: lastExecutionDate,
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/alerts/server/health/get_health.ts b/x-pack/plugins/alerts/server/health/get_health.ts
new file mode 100644
index 0000000000000..b7b4582aa8d10
--- /dev/null
+++ b/x-pack/plugins/alerts/server/health/get_health.ts
@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { ISavedObjectsRepository } from 'src/core/server';
+import { AlertsHealth, HealthStatus, RawAlert, AlertExecutionStatusErrorReasons } from '../types';
+
+export const getHealth = async (
+ internalSavedObjectsRepository: ISavedObjectsRepository
+): Promise => {
+ const healthStatuses = {
+ decryptionHealth: {
+ status: HealthStatus.OK,
+ timestamp: '',
+ },
+ executionHealth: {
+ status: HealthStatus.OK,
+ timestamp: '',
+ },
+ readHealth: {
+ status: HealthStatus.OK,
+ timestamp: '',
+ },
+ };
+
+ const { saved_objects: decryptErrorData } = await internalSavedObjectsRepository.find({
+ filter: `alert.attributes.executionStatus.status:error and alert.attributes.executionStatus.error.reason:${AlertExecutionStatusErrorReasons.Decrypt}`,
+ fields: ['executionStatus'],
+ type: 'alert',
+ sortField: 'executionStatus.lastExecutionDate',
+ sortOrder: 'desc',
+ page: 1,
+ perPage: 1,
+ });
+
+ if (decryptErrorData.length > 0) {
+ healthStatuses.decryptionHealth = {
+ status: HealthStatus.Warning,
+ timestamp: decryptErrorData[0].attributes.executionStatus.lastExecutionDate,
+ };
+ }
+
+ const { saved_objects: executeErrorData } = await internalSavedObjectsRepository.find({
+ filter: `alert.attributes.executionStatus.status:error and alert.attributes.executionStatus.error.reason:${AlertExecutionStatusErrorReasons.Execute}`,
+ fields: ['executionStatus'],
+ type: 'alert',
+ sortField: 'executionStatus.lastExecutionDate',
+ sortOrder: 'desc',
+ page: 1,
+ perPage: 1,
+ });
+
+ if (executeErrorData.length > 0) {
+ healthStatuses.executionHealth = {
+ status: HealthStatus.Warning,
+ timestamp: executeErrorData[0].attributes.executionStatus.lastExecutionDate,
+ };
+ }
+
+ const { saved_objects: readErrorData } = await internalSavedObjectsRepository.find({
+ filter: `alert.attributes.executionStatus.status:error and alert.attributes.executionStatus.error.reason:${AlertExecutionStatusErrorReasons.Read}`,
+ fields: ['executionStatus'],
+ type: 'alert',
+ sortField: 'executionStatus.lastExecutionDate',
+ sortOrder: 'desc',
+ page: 1,
+ perPage: 1,
+ });
+
+ if (readErrorData.length > 0) {
+ healthStatuses.readHealth = {
+ status: HealthStatus.Warning,
+ timestamp: readErrorData[0].attributes.executionStatus.lastExecutionDate,
+ };
+ }
+
+ const { saved_objects: noErrorData } = await internalSavedObjectsRepository.find({
+ filter: 'not alert.attributes.executionStatus.status:error',
+ fields: ['executionStatus'],
+ type: 'alert',
+ sortField: 'executionStatus.lastExecutionDate',
+ sortOrder: 'desc',
+ });
+ const lastExecutionDate =
+ noErrorData.length > 0
+ ? noErrorData[0].attributes.executionStatus.lastExecutionDate
+ : new Date().toISOString();
+
+ for (const [, statusItem] of Object.entries(healthStatuses)) {
+ if (statusItem.status === HealthStatus.OK) {
+ statusItem.timestamp = lastExecutionDate;
+ }
+ }
+
+ return healthStatuses;
+};
diff --git a/x-pack/plugins/alerts/server/health/get_state.test.ts b/x-pack/plugins/alerts/server/health/get_state.test.ts
new file mode 100644
index 0000000000000..86981c486da0f
--- /dev/null
+++ b/x-pack/plugins/alerts/server/health/get_state.test.ts
@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { taskManagerMock } from '../../../task_manager/server/mocks';
+import { getHealthStatusStream } from '.';
+import { TaskStatus } from '../../../task_manager/server';
+import { HealthStatus } from '../types';
+
+describe('getHealthStatusStream()', () => {
+ const mockTaskManager = taskManagerMock.createStart();
+
+ it('should return an object with the "unavailable" level and proper summary of "Alerting framework is unhealthy"', async () => {
+ mockTaskManager.get.mockReturnValue(
+ new Promise((_resolve, _reject) => {
+ return {
+ id: 'test',
+ attempts: 0,
+ status: TaskStatus.Running,
+ version: '123',
+ runAt: new Date(),
+ scheduledAt: new Date(),
+ startedAt: new Date(),
+ retryAt: new Date(Date.now() + 5 * 60 * 1000),
+ state: {
+ runs: 1,
+ health_status: HealthStatus.Warning,
+ },
+ taskType: 'alerting:alerting_health_check',
+ params: {
+ alertId: '1',
+ },
+ ownerId: null,
+ };
+ })
+ );
+ getHealthStatusStream(mockTaskManager).subscribe(
+ (val: { level: Readonly; summary: string }) => {
+ expect(val.level).toBe(false);
+ }
+ );
+ });
+
+ it('should return an object with the "available" level and proper summary of "Alerting framework is healthy"', async () => {
+ mockTaskManager.get.mockReturnValue(
+ new Promise((_resolve, _reject) => {
+ return {
+ id: 'test',
+ attempts: 0,
+ status: TaskStatus.Running,
+ version: '123',
+ runAt: new Date(),
+ scheduledAt: new Date(),
+ startedAt: new Date(),
+ retryAt: new Date(Date.now() + 5 * 60 * 1000),
+ state: {
+ runs: 1,
+ health_status: HealthStatus.OK,
+ },
+ taskType: 'alerting:alerting_health_check',
+ params: {
+ alertId: '1',
+ },
+ ownerId: null,
+ };
+ })
+ );
+ getHealthStatusStream(mockTaskManager).subscribe(
+ (val: { level: Readonly; summary: string }) => {
+ expect(val.level).toBe(true);
+ }
+ );
+ });
+});
diff --git a/x-pack/plugins/alerts/server/health/get_state.ts b/x-pack/plugins/alerts/server/health/get_state.ts
new file mode 100644
index 0000000000000..476456ecad88a
--- /dev/null
+++ b/x-pack/plugins/alerts/server/health/get_state.ts
@@ -0,0 +1,73 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { interval, Observable } from 'rxjs';
+import { catchError, switchMap } from 'rxjs/operators';
+import { ServiceStatus, ServiceStatusLevels } from '../../../../../src/core/server';
+import { TaskManagerStartContract } from '../../../task_manager/server';
+import { HEALTH_TASK_ID } from './task';
+import { HealthStatus } from '../types';
+
+async function getLatestTaskState(taskManager: TaskManagerStartContract) {
+ try {
+ const result = await taskManager.get(HEALTH_TASK_ID);
+ return result;
+ } catch (err) {
+ const errMessage = err && err.message ? err.message : err.toString();
+ if (!errMessage.includes('NotInitialized')) {
+ throw err;
+ }
+ }
+
+ return null;
+}
+
+const LEVEL_SUMMARY = {
+ [ServiceStatusLevels.available.toString()]: i18n.translate(
+ 'xpack.alerts.server.healthStatus.available',
+ {
+ defaultMessage: 'Alerting framework is available',
+ }
+ ),
+ [ServiceStatusLevels.degraded.toString()]: i18n.translate(
+ 'xpack.alerts.server.healthStatus.degraded',
+ {
+ defaultMessage: 'Alerting framework is degraded',
+ }
+ ),
+ [ServiceStatusLevels.unavailable.toString()]: i18n.translate(
+ 'xpack.alerts.server.healthStatus.unavailable',
+ {
+ defaultMessage: 'Alerting framework is unavailable',
+ }
+ ),
+};
+
+export const getHealthStatusStream = (
+ taskManager: TaskManagerStartContract
+): Observable> => {
+ return interval(60000 * 5).pipe(
+ switchMap(async () => {
+ const doc = await getLatestTaskState(taskManager);
+ const level =
+ doc?.state?.health_status === HealthStatus.OK
+ ? ServiceStatusLevels.available
+ : doc?.state?.health_status === HealthStatus.Warning
+ ? ServiceStatusLevels.degraded
+ : ServiceStatusLevels.unavailable;
+ return {
+ level,
+ summary: LEVEL_SUMMARY[level.toString()],
+ };
+ }),
+ catchError(async (error) => ({
+ level: ServiceStatusLevels.unavailable,
+ summary: LEVEL_SUMMARY[ServiceStatusLevels.unavailable.toString()],
+ meta: { error },
+ }))
+ );
+};
diff --git a/x-pack/plugins/alerts/server/health/index.ts b/x-pack/plugins/alerts/server/health/index.ts
new file mode 100644
index 0000000000000..730c4596aa550
--- /dev/null
+++ b/x-pack/plugins/alerts/server/health/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { getHealthStatusStream } from './get_state';
+export { scheduleAlertingHealthCheck, initializeAlertingHealth } from './task';
diff --git a/x-pack/plugins/alerts/server/health/task.ts b/x-pack/plugins/alerts/server/health/task.ts
new file mode 100644
index 0000000000000..6ea01a1083c13
--- /dev/null
+++ b/x-pack/plugins/alerts/server/health/task.ts
@@ -0,0 +1,94 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { CoreStart, Logger } from 'kibana/server';
+import {
+ RunContext,
+ TaskManagerSetupContract,
+ TaskManagerStartContract,
+} from '../../../task_manager/server';
+import { AlertsConfig } from '../config';
+import { AlertingPluginsStart } from '../plugin';
+import { HealthStatus } from '../types';
+import { getHealth } from './get_health';
+
+export const HEALTH_TASK_TYPE = 'alerting_health_check';
+
+export const HEALTH_TASK_ID = `Alerting-${HEALTH_TASK_TYPE}`;
+
+export function initializeAlertingHealth(
+ logger: Logger,
+ taskManager: TaskManagerSetupContract,
+ coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>
+) {
+ registerAlertingHealthCheckTask(logger, taskManager, coreStartServices);
+}
+
+export async function scheduleAlertingHealthCheck(
+ logger: Logger,
+ config: Promise,
+ taskManager: TaskManagerStartContract
+) {
+ try {
+ const interval = (await config).healthCheck.interval;
+ await taskManager.ensureScheduled({
+ id: HEALTH_TASK_ID,
+ taskType: HEALTH_TASK_TYPE,
+ schedule: {
+ interval,
+ },
+ state: {},
+ params: {},
+ });
+ } catch (e) {
+ logger.debug(`Error scheduling task, received ${e.message}`);
+ }
+}
+
+function registerAlertingHealthCheckTask(
+ logger: Logger,
+ taskManager: TaskManagerSetupContract,
+ coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>
+) {
+ taskManager.registerTaskDefinitions({
+ [HEALTH_TASK_TYPE]: {
+ title: 'Alerting framework health check task',
+ createTaskRunner: healthCheckTaskRunner(logger, coreStartServices),
+ },
+ });
+}
+
+export function healthCheckTaskRunner(
+ logger: Logger,
+ coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>
+) {
+ return ({ taskInstance }: RunContext) => {
+ const { state } = taskInstance;
+ return {
+ async run() {
+ try {
+ const alertingHealthStatus = await getHealth(
+ (await coreStartServices)[0].savedObjects.createInternalRepository(['alert'])
+ );
+ return {
+ state: {
+ runs: (state.runs || 0) + 1,
+ health_status: alertingHealthStatus.decryptionHealth.status,
+ },
+ };
+ } catch (errMsg) {
+ logger.warn(`Error executing alerting health check task: ${errMsg}`);
+ return {
+ state: {
+ runs: (state.runs || 0) + 1,
+ health_status: HealthStatus.Error,
+ },
+ };
+ }
+ },
+ };
+ };
+}
diff --git a/x-pack/plugins/alerts/server/index.ts b/x-pack/plugins/alerts/server/index.ts
index 1e442c5196cf2..64e585da5c654 100644
--- a/x-pack/plugins/alerts/server/index.ts
+++ b/x-pack/plugins/alerts/server/index.ts
@@ -5,8 +5,10 @@
*/
import type { PublicMethodsOf } from '@kbn/utility-types';
import { AlertsClient as AlertsClientClass } from './alerts_client';
-import { PluginInitializerContext } from '../../../../src/core/server';
+import { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server';
import { AlertingPlugin } from './plugin';
+import { configSchema } from './config';
+import { AlertsConfigType } from './types';
export type AlertsClient = PublicMethodsOf;
@@ -30,3 +32,7 @@ export { AlertInstance } from './alert_instance';
export { parseDuration } from './lib';
export const plugin = (initContext: PluginInitializerContext) => new AlertingPlugin(initContext);
+
+export const config: PluginConfigDescriptor = {
+ schema: configSchema,
+};
diff --git a/x-pack/plugins/alerts/server/lib/alert_execution_status.test.ts b/x-pack/plugins/alerts/server/lib/alert_execution_status.test.ts
index 3372d19cd4090..bb24ab034d0dd 100644
--- a/x-pack/plugins/alerts/server/lib/alert_execution_status.test.ts
+++ b/x-pack/plugins/alerts/server/lib/alert_execution_status.test.ts
@@ -57,7 +57,9 @@ describe('AlertExecutionStatus', () => {
});
test('error with a reason', () => {
- const status = executionStatusFromError(new ErrorWithReason('execute', new Error('hoo!')));
+ const status = executionStatusFromError(
+ new ErrorWithReason(AlertExecutionStatusErrorReasons.Execute, new Error('hoo!'))
+ );
expect(status.status).toBe('error');
expect(status.error).toMatchInlineSnapshot(`
Object {
@@ -71,7 +73,7 @@ describe('AlertExecutionStatus', () => {
describe('alertExecutionStatusToRaw()', () => {
const date = new Date('2020-09-03T16:26:58Z');
const status = 'ok';
- const reason: AlertExecutionStatusErrorReasons = 'decrypt';
+ const reason = AlertExecutionStatusErrorReasons.Decrypt;
const error = { reason, message: 'wops' };
test('status without an error', () => {
@@ -102,7 +104,7 @@ describe('AlertExecutionStatus', () => {
describe('alertExecutionStatusFromRaw()', () => {
const date = new Date('2020-09-03T16:26:58Z').toISOString();
const status = 'active';
- const reason: AlertExecutionStatusErrorReasons = 'execute';
+ const reason = AlertExecutionStatusErrorReasons.Execute;
const error = { reason, message: 'wops' };
test('no input', () => {
diff --git a/x-pack/plugins/alerts/server/lib/error_with_reason.test.ts b/x-pack/plugins/alerts/server/lib/error_with_reason.test.ts
index f31f584400308..eff935966345f 100644
--- a/x-pack/plugins/alerts/server/lib/error_with_reason.test.ts
+++ b/x-pack/plugins/alerts/server/lib/error_with_reason.test.ts
@@ -5,20 +5,21 @@
*/
import { ErrorWithReason, getReasonFromError, isErrorWithReason } from './error_with_reason';
+import { AlertExecutionStatusErrorReasons } from '../types';
describe('ErrorWithReason', () => {
const plainError = new Error('well, actually');
- const errorWithReason = new ErrorWithReason('decrypt', plainError);
+ const errorWithReason = new ErrorWithReason(AlertExecutionStatusErrorReasons.Decrypt, plainError);
test('ErrorWithReason class', () => {
expect(errorWithReason.message).toBe(plainError.message);
expect(errorWithReason.error).toBe(plainError);
- expect(errorWithReason.reason).toBe('decrypt');
+ expect(errorWithReason.reason).toBe(AlertExecutionStatusErrorReasons.Decrypt);
});
test('getReasonFromError()', () => {
expect(getReasonFromError(plainError)).toBe('unknown');
- expect(getReasonFromError(errorWithReason)).toBe('decrypt');
+ expect(getReasonFromError(errorWithReason)).toBe(AlertExecutionStatusErrorReasons.Decrypt);
});
test('isErrorWithReason()', () => {
diff --git a/x-pack/plugins/alerts/server/lib/error_with_reason.ts b/x-pack/plugins/alerts/server/lib/error_with_reason.ts
index 29eb666e64427..a732b44ef2238 100644
--- a/x-pack/plugins/alerts/server/lib/error_with_reason.ts
+++ b/x-pack/plugins/alerts/server/lib/error_with_reason.ts
@@ -21,7 +21,7 @@ export function getReasonFromError(error: Error): AlertExecutionStatusErrorReaso
if (isErrorWithReason(error)) {
return error.reason;
}
- return 'unknown';
+ return AlertExecutionStatusErrorReasons.Unknown;
}
export function isErrorWithReason(error: Error | ErrorWithReason): error is ErrorWithReason {
diff --git a/x-pack/plugins/alerts/server/lib/is_alert_not_found_error.test.ts b/x-pack/plugins/alerts/server/lib/is_alert_not_found_error.test.ts
index b570957d82de4..ab21dc77fa251 100644
--- a/x-pack/plugins/alerts/server/lib/is_alert_not_found_error.test.ts
+++ b/x-pack/plugins/alerts/server/lib/is_alert_not_found_error.test.ts
@@ -8,6 +8,7 @@ import { isAlertSavedObjectNotFoundError } from './is_alert_not_found_error';
import { ErrorWithReason } from './error_with_reason';
import { SavedObjectsErrorHelpers } from '../../../../../src/core/server';
import uuid from 'uuid';
+import { AlertExecutionStatusErrorReasons } from '../types';
describe('isAlertSavedObjectNotFoundError', () => {
const id = uuid.v4();
@@ -25,7 +26,7 @@ describe('isAlertSavedObjectNotFoundError', () => {
});
test('identifies SavedObjects Not Found errors wrapped in an ErrorWithReason', () => {
- const error = new ErrorWithReason('read', errorSONF);
+ const error = new ErrorWithReason(AlertExecutionStatusErrorReasons.Read, errorSONF);
expect(isAlertSavedObjectNotFoundError(error, id)).toBe(true);
});
});
diff --git a/x-pack/plugins/alerts/server/mocks.ts b/x-pack/plugins/alerts/server/mocks.ts
index 05d64bdbb77f4..cfae4c650bd42 100644
--- a/x-pack/plugins/alerts/server/mocks.ts
+++ b/x-pack/plugins/alerts/server/mocks.ts
@@ -25,6 +25,7 @@ const createStartMock = () => {
const mock: jest.Mocked = {
listTypes: jest.fn(),
getAlertsClientWithRequest: jest.fn().mockResolvedValue(alertsClientMock.create()),
+ getFrameworkHealth: jest.fn(),
};
return mock;
};
diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts
index b13a1c62f6602..715fbc6aeed45 100644
--- a/x-pack/plugins/alerts/server/plugin.test.ts
+++ b/x-pack/plugins/alerts/server/plugin.test.ts
@@ -5,7 +5,7 @@
*/
import { AlertingPlugin, AlertingPluginsSetup, AlertingPluginsStart } from './plugin';
-import { coreMock } from '../../../../src/core/server/mocks';
+import { coreMock, statusServiceMock } from '../../../../src/core/server/mocks';
import { licensingMock } from '../../licensing/server/mocks';
import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks';
import { taskManagerMock } from '../../task_manager/server/mocks';
@@ -13,15 +13,21 @@ import { eventLogServiceMock } from '../../event_log/server/event_log_service.mo
import { KibanaRequest, CoreSetup } from 'kibana/server';
import { featuresPluginMock } from '../../features/server/mocks';
import { KibanaFeature } from '../../features/server';
+import { AlertsConfig } from './config';
describe('Alerting Plugin', () => {
describe('setup()', () => {
it('should log warning when Encrypted Saved Objects plugin is using an ephemeral encryption key', async () => {
- const context = coreMock.createPluginInitializerContext();
+ const context = coreMock.createPluginInitializerContext({
+ healthCheck: {
+ interval: '5m',
+ },
+ });
const plugin = new AlertingPlugin(context);
const coreSetup = coreMock.createSetup();
const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup();
+ const statusMock = statusServiceMock.createSetupContract();
await plugin.setup(
({
...coreSetup,
@@ -29,6 +35,7 @@ describe('Alerting Plugin', () => {
...coreSetup.http,
route: jest.fn(),
},
+ status: statusMock,
} as unknown) as CoreSetup,
({
licensing: licensingMock.createSetup(),
@@ -38,6 +45,7 @@ describe('Alerting Plugin', () => {
} as unknown) as AlertingPluginsSetup
);
+ expect(statusMock.set).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsSetup.usingEphemeralEncryptionKey).toEqual(true);
expect(context.logger.get().warn).toHaveBeenCalledWith(
'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.'
@@ -55,7 +63,11 @@ describe('Alerting Plugin', () => {
*/
describe('getAlertsClientWithRequest()', () => {
it('throws error when encryptedSavedObjects plugin has usingEphemeralEncryptionKey set to true', async () => {
- const context = coreMock.createPluginInitializerContext();
+ const context = coreMock.createPluginInitializerContext({
+ healthCheck: {
+ interval: '5m',
+ },
+ });
const plugin = new AlertingPlugin(context);
const coreSetup = coreMock.createSetup();
@@ -98,7 +110,11 @@ describe('Alerting Plugin', () => {
});
it(`doesn't throw error when encryptedSavedObjects plugin has usingEphemeralEncryptionKey set to false`, async () => {
- const context = coreMock.createPluginInitializerContext();
+ const context = coreMock.createPluginInitializerContext({
+ healthCheck: {
+ interval: '5m',
+ },
+ });
const plugin = new AlertingPlugin(context);
const coreSetup = coreMock.createSetup();
diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts
index 75873a2845c15..1fa89606a76fc 100644
--- a/x-pack/plugins/alerts/server/plugin.ts
+++ b/x-pack/plugins/alerts/server/plugin.ts
@@ -6,6 +6,7 @@
import type { PublicMethodsOf } from '@kbn/utility-types';
import { first, map } from 'rxjs/operators';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
+import { combineLatest } from 'rxjs';
import { SecurityPluginSetup } from '../../security/server';
import {
EncryptedSavedObjectsPluginSetup,
@@ -30,6 +31,8 @@ import {
SharedGlobalConfig,
ElasticsearchServiceStart,
ILegacyClusterClient,
+ StatusServiceSetup,
+ ServiceStatus,
} from '../../../../src/core/server';
import {
@@ -56,12 +59,19 @@ import {
PluginSetupContract as ActionsPluginSetupContract,
PluginStartContract as ActionsPluginStartContract,
} from '../../actions/server';
-import { Services } from './types';
+import { AlertsHealth, Services } from './types';
import { registerAlertsUsageCollector } from './usage';
import { initializeAlertingTelemetry, scheduleAlertingTelemetry } from './usage/task';
import { IEventLogger, IEventLogService, IEventLogClientService } from '../../event_log/server';
import { PluginStartContract as FeaturesPluginStart } from '../../features/server';
import { setupSavedObjects } from './saved_objects';
+import {
+ getHealthStatusStream,
+ scheduleAlertingHealthCheck,
+ initializeAlertingHealth,
+} from './health';
+import { AlertsConfig } from './config';
+import { getHealth } from './health/get_health';
export const EVENT_LOG_PROVIDER = 'alerting';
export const EVENT_LOG_ACTIONS = {
@@ -78,6 +88,7 @@ export interface PluginSetupContract {
export interface PluginStartContract {
listTypes: AlertTypeRegistry['list'];
getAlertsClientWithRequest(request: KibanaRequest): PublicMethodsOf;
+ getFrameworkHealth: () => Promise;
}
export interface AlertingPluginsSetup {
@@ -89,6 +100,7 @@ export interface AlertingPluginsSetup {
spaces?: SpacesPluginSetup;
usageCollection?: UsageCollectionSetup;
eventLog: IEventLogService;
+ statusService: StatusServiceSetup;
}
export interface AlertingPluginsStart {
actions: ActionsPluginStartContract;
@@ -99,6 +111,7 @@ export interface AlertingPluginsStart {
}
export class AlertingPlugin {
+ private readonly config: Promise;
private readonly logger: Logger;
private alertTypeRegistry?: AlertTypeRegistry;
private readonly taskRunnerFactory: TaskRunnerFactory;
@@ -115,6 +128,7 @@ export class AlertingPlugin {
private eventLogger?: IEventLogger;
constructor(initializerContext: PluginInitializerContext) {
+ this.config = initializerContext.config.create().pipe(first()).toPromise();
this.logger = initializerContext.logger.get('plugins', 'alerting');
this.taskRunnerFactory = new TaskRunnerFactory();
this.alertsClientFactory = new AlertsClientFactory();
@@ -186,6 +200,25 @@ export class AlertingPlugin {
});
}
+ core.getStartServices().then(async ([, startPlugins]) => {
+ core.status.set(
+ combineLatest([
+ core.status.derivedStatus$,
+ getHealthStatusStream(startPlugins.taskManager),
+ ]).pipe(
+ map(([derivedStatus, healthStatus]) => {
+ if (healthStatus.level > derivedStatus.level) {
+ return healthStatus as ServiceStatus;
+ } else {
+ return derivedStatus;
+ }
+ })
+ )
+ );
+ });
+
+ initializeAlertingHealth(this.logger, plugins.taskManager, core.getStartServices());
+
core.http.registerRouteHandlerContext('alerting', this.createRouteHandlerContext(core));
// Routes
@@ -275,10 +308,13 @@ export class AlertingPlugin {
});
scheduleAlertingTelemetry(this.telemetryLogger, plugins.taskManager);
+ scheduleAlertingHealthCheck(this.logger, this.config, plugins.taskManager);
return {
listTypes: alertTypeRegistry!.list.bind(this.alertTypeRegistry!),
getAlertsClientWithRequest,
+ getFrameworkHealth: async () =>
+ await getHealth(core.savedObjects.createInternalRepository(['alert'])),
};
}
@@ -293,6 +329,8 @@ export class AlertingPlugin {
return alertsClientFactory!.create(request, savedObjects);
},
listTypes: alertTypeRegistry!.list.bind(alertTypeRegistry!),
+ getFrameworkHealth: async () =>
+ await getHealth(savedObjects.createInternalRepository(['alert'])),
};
};
};
diff --git a/x-pack/plugins/alerts/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/alerts/server/routes/_mock_handler_arguments.ts
index 3d13fc65ab260..b3f407b20c142 100644
--- a/x-pack/plugins/alerts/server/routes/_mock_handler_arguments.ts
+++ b/x-pack/plugins/alerts/server/routes/_mock_handler_arguments.ts
@@ -14,7 +14,7 @@ import { identity } from 'lodash';
import type { MethodKeysOf } from '@kbn/utility-types';
import { httpServerMock } from '../../../../../src/core/server/mocks';
import { alertsClientMock, AlertsClientMock } from '../alerts_client.mock';
-import { AlertType } from '../../common';
+import { AlertsHealth, AlertType } from '../../common';
import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks';
export function mockHandlerArguments(
@@ -22,10 +22,13 @@ export function mockHandlerArguments(
alertsClient = alertsClientMock.create(),
listTypes: listTypesRes = [],
esClient = elasticsearchServiceMock.createLegacyClusterClient(),
+ getFrameworkHealth,
}: {
alertsClient?: AlertsClientMock;
listTypes?: AlertType[];
esClient?: jest.Mocked;
+ getFrameworkHealth?: jest.MockInstance, []> &
+ (() => Promise);
},
req: unknown,
res?: Array>
@@ -39,6 +42,7 @@ export function mockHandlerArguments(
getAlertsClient() {
return alertsClient || alertsClientMock.create();
},
+ getFrameworkHealth,
},
} as unknown) as RequestHandlerContext,
req as KibanaRequest,
diff --git a/x-pack/plugins/alerts/server/routes/health.test.ts b/x-pack/plugins/alerts/server/routes/health.test.ts
index ce782dbd631a5..d1967c6dd9bf8 100644
--- a/x-pack/plugins/alerts/server/routes/health.test.ts
+++ b/x-pack/plugins/alerts/server/routes/health.test.ts
@@ -11,13 +11,34 @@ import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks';
import { verifyApiAccess } from '../lib/license_api_access';
import { mockLicenseState } from '../lib/license_state.mock';
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks';
+import { alertsClientMock } from '../alerts_client.mock';
+import { HealthStatus } from '../types';
+import { alertsMock } from '../mocks';
+const alertsClient = alertsClientMock.create();
jest.mock('../lib/license_api_access.ts', () => ({
verifyApiAccess: jest.fn(),
}));
+const alerting = alertsMock.createStart();
+
+const currentDate = new Date().toISOString();
beforeEach(() => {
jest.resetAllMocks();
+ alerting.getFrameworkHealth.mockResolvedValue({
+ decryptionHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
+ executionHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
+ readHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
+ });
});
describe('healthRoute', () => {
@@ -46,7 +67,7 @@ describe('healthRoute', () => {
const esClient = elasticsearchServiceMock.createLegacyClusterClient();
esClient.callAsInternalUser.mockReturnValue(Promise.resolve({}));
- const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']);
+ const [context, req, res] = mockHandlerArguments({ esClient, alertsClient }, {}, ['ok']);
await handler(context, req, res);
@@ -75,16 +96,32 @@ describe('healthRoute', () => {
const esClient = elasticsearchServiceMock.createLegacyClusterClient();
esClient.callAsInternalUser.mockReturnValue(Promise.resolve({}));
- const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']);
+ const [context, req, res] = mockHandlerArguments(
+ { esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth },
+ {},
+ ['ok']
+ );
- expect(await handler(context, req, res)).toMatchInlineSnapshot(`
- Object {
- "body": Object {
- "hasPermanentEncryptionKey": false,
- "isSufficientlySecure": true,
+ expect(await handler(context, req, res)).toStrictEqual({
+ body: {
+ alertingFrameworkHeath: {
+ decryptionHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
+ executionHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
+ readHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
},
- }
- `);
+ hasPermanentEncryptionKey: false,
+ isSufficientlySecure: true,
+ },
+ });
});
it('evaluates missing security info from the usage api to mean that the security plugin is disbled', async () => {
@@ -99,16 +136,32 @@ describe('healthRoute', () => {
const esClient = elasticsearchServiceMock.createLegacyClusterClient();
esClient.callAsInternalUser.mockReturnValue(Promise.resolve({}));
- const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']);
+ const [context, req, res] = mockHandlerArguments(
+ { esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth },
+ {},
+ ['ok']
+ );
- expect(await handler(context, req, res)).toMatchInlineSnapshot(`
- Object {
- "body": Object {
- "hasPermanentEncryptionKey": true,
- "isSufficientlySecure": true,
+ expect(await handler(context, req, res)).toStrictEqual({
+ body: {
+ alertingFrameworkHeath: {
+ decryptionHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
+ executionHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
+ readHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
},
- }
- `);
+ hasPermanentEncryptionKey: true,
+ isSufficientlySecure: true,
+ },
+ });
});
it('evaluates missing security http info from the usage api to mean that the security plugin is disbled', async () => {
@@ -123,16 +176,32 @@ describe('healthRoute', () => {
const esClient = elasticsearchServiceMock.createLegacyClusterClient();
esClient.callAsInternalUser.mockReturnValue(Promise.resolve({ security: {} }));
- const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']);
+ const [context, req, res] = mockHandlerArguments(
+ { esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth },
+ {},
+ ['ok']
+ );
- expect(await handler(context, req, res)).toMatchInlineSnapshot(`
- Object {
- "body": Object {
- "hasPermanentEncryptionKey": true,
- "isSufficientlySecure": true,
+ expect(await handler(context, req, res)).toStrictEqual({
+ body: {
+ alertingFrameworkHeath: {
+ decryptionHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
+ executionHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
+ readHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
},
- }
- `);
+ hasPermanentEncryptionKey: true,
+ isSufficientlySecure: true,
+ },
+ });
});
it('evaluates security enabled, and missing ssl info from the usage api to mean that the user cannot generate keys', async () => {
@@ -147,16 +216,32 @@ describe('healthRoute', () => {
const esClient = elasticsearchServiceMock.createLegacyClusterClient();
esClient.callAsInternalUser.mockReturnValue(Promise.resolve({ security: { enabled: true } }));
- const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']);
+ const [context, req, res] = mockHandlerArguments(
+ { esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth },
+ {},
+ ['ok']
+ );
- expect(await handler(context, req, res)).toMatchInlineSnapshot(`
- Object {
- "body": Object {
- "hasPermanentEncryptionKey": true,
- "isSufficientlySecure": false,
+ expect(await handler(context, req, res)).toStrictEqual({
+ body: {
+ alertingFrameworkHeath: {
+ decryptionHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
+ executionHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
+ readHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
},
- }
- `);
+ hasPermanentEncryptionKey: true,
+ isSufficientlySecure: false,
+ },
+ });
});
it('evaluates security enabled, SSL info present but missing http info from the usage api to mean that the user cannot generate keys', async () => {
@@ -173,16 +258,32 @@ describe('healthRoute', () => {
Promise.resolve({ security: { enabled: true, ssl: {} } })
);
- const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']);
+ const [context, req, res] = mockHandlerArguments(
+ { esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth },
+ {},
+ ['ok']
+ );
- expect(await handler(context, req, res)).toMatchInlineSnapshot(`
- Object {
- "body": Object {
- "hasPermanentEncryptionKey": true,
- "isSufficientlySecure": false,
+ expect(await handler(context, req, res)).toStrictEqual({
+ body: {
+ alertingFrameworkHeath: {
+ decryptionHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
+ executionHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
+ readHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
},
- }
- `);
+ hasPermanentEncryptionKey: true,
+ isSufficientlySecure: false,
+ },
+ });
});
it('evaluates security and tls enabled to mean that the user can generate keys', async () => {
@@ -199,15 +300,31 @@ describe('healthRoute', () => {
Promise.resolve({ security: { enabled: true, ssl: { http: { enabled: true } } } })
);
- const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']);
+ const [context, req, res] = mockHandlerArguments(
+ { esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth },
+ {},
+ ['ok']
+ );
- expect(await handler(context, req, res)).toMatchInlineSnapshot(`
- Object {
- "body": Object {
- "hasPermanentEncryptionKey": true,
- "isSufficientlySecure": true,
+ expect(await handler(context, req, res)).toStrictEqual({
+ body: {
+ alertingFrameworkHeath: {
+ decryptionHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
+ executionHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
+ readHealth: {
+ status: HealthStatus.OK,
+ timestamp: currentDate,
+ },
},
- }
- `);
+ hasPermanentEncryptionKey: true,
+ isSufficientlySecure: true,
+ },
+ });
});
});
diff --git a/x-pack/plugins/alerts/server/routes/health.ts b/x-pack/plugins/alerts/server/routes/health.ts
index b66e28b24e8a7..bfd5b1e272287 100644
--- a/x-pack/plugins/alerts/server/routes/health.ts
+++ b/x-pack/plugins/alerts/server/routes/health.ts
@@ -43,6 +43,9 @@ export function healthRoute(
res: KibanaResponseFactory
): Promise {
verifyApiAccess(licenseState);
+ if (!context.alerting) {
+ return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' });
+ }
try {
const {
security: {
@@ -57,9 +60,12 @@ export function healthRoute(
path: '/_xpack/usage',
});
+ const alertingFrameworkHeath = await context.alerting.getFrameworkHealth();
+
const frameworkHealth: AlertingFrameworkHealth = {
isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled),
hasPermanentEncryptionKey: !encryptedSavedObjects.usingEphemeralEncryptionKey,
+ alertingFrameworkHeath,
};
return res.ok({
diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts
index 6a49f67268d69..86bf7006e8d09 100644
--- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts
+++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts
@@ -28,6 +28,7 @@ import {
AlertExecutorOptions,
SanitizedAlert,
AlertExecutionStatus,
+ AlertExecutionStatusErrorReasons,
} from '../types';
import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type';
import { taskInstanceToAlertTaskInstance } from './alert_task_instance';
@@ -211,7 +212,7 @@ export class TaskRunner {
event.event = event.event || {};
event.event.outcome = 'failure';
eventLogger.logEvent(event);
- throw new ErrorWithReason('execute', err);
+ throw new ErrorWithReason(AlertExecutionStatusErrorReasons.Execute, err);
}
eventLogger.stopTiming(event);
@@ -288,7 +289,7 @@ export class TaskRunner {
try {
apiKey = await this.getApiKeyForAlertPermissions(alertId, spaceId);
} catch (err) {
- throw new ErrorWithReason('decrypt', err);
+ throw new ErrorWithReason(AlertExecutionStatusErrorReasons.Decrypt, err);
}
const [services, alertsClient] = this.getServicesWithSpaceLevelPermissions(spaceId, apiKey);
@@ -298,7 +299,7 @@ export class TaskRunner {
try {
alert = await alertsClient.get({ id: alertId });
} catch (err) {
- throw new ErrorWithReason('read', err);
+ throw new ErrorWithReason(AlertExecutionStatusErrorReasons.Read, err);
}
return {
diff --git a/x-pack/plugins/alerts/server/types.ts b/x-pack/plugins/alerts/server/types.ts
index 42eef9bba10e5..9226461f6e30a 100644
--- a/x-pack/plugins/alerts/server/types.ts
+++ b/x-pack/plugins/alerts/server/types.ts
@@ -27,6 +27,7 @@ import {
AlertInstanceState,
AlertExecutionStatuses,
AlertExecutionStatusErrorReasons,
+ AlertsHealth,
} from '../common';
export type WithoutQueryAndParams = Pick>;
@@ -39,6 +40,7 @@ declare module 'src/core/server' {
alerting?: {
getAlertsClient: () => AlertsClient;
listTypes: AlertTypeRegistry['list'];
+ getFrameworkHealth: () => Promise;
};
}
}
@@ -172,4 +174,10 @@ export interface AlertingPlugin {
start: PluginStartContract;
}
+export interface AlertsConfigType {
+ healthCheck: {
+ interval: string;
+ };
+}
+
export type AlertTypeRegistry = PublicMethodsOf;
diff --git a/x-pack/plugins/apm/e2e/cypress/integration/apm.feature b/x-pack/plugins/apm/e2e/cypress/integration/apm.feature
index 285615108266b..494a6b5fadb5b 100644
--- a/x-pack/plugins/apm/e2e/cypress/integration/apm.feature
+++ b/x-pack/plugins/apm/e2e/cypress/integration/apm.feature
@@ -3,5 +3,4 @@ Feature: APM
Scenario: Transaction duration charts
Given a user browses the APM UI application
When the user inspects the opbeans-node service
- Then should redirect to correct path with correct params
- And should have correct y-axis ticks
+ Then should redirect to correct path with correct params
\ No newline at end of file
diff --git a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js
index 72b49bb85b7a5..0ecda7a113de7 100644
--- a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js
+++ b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js
@@ -1,3 +1,3 @@
module.exports = {
- __version: '5.5.0',
-};
+ "__version": "5.4.0"
+}
diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts
index 50c620dca9ddf..42c2bc7ffd318 100644
--- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts
+++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts
@@ -29,16 +29,3 @@ Then(`should redirect to correct path with correct params`, () => {
cy.url().should('contain', `/app/apm/services/opbeans-node/transactions`);
cy.url().should('contain', `transactionType=request`);
});
-
-Then(`should have correct y-axis ticks`, () => {
- const yAxisTick =
- '[data-cy=transaction-duration-charts] .rv-xy-plot__axis--vertical .rv-xy-plot__axis__tick__text';
-
- // wait for all loading to finish
- cy.get('kbnLoadingIndicator').should('not.be.visible');
-
- // literal assertions because snapshot() doesn't retry
- cy.get(yAxisTick).eq(2).should('have.text', '55 ms');
- cy.get(yAxisTick).eq(1).should('have.text', '28 ms');
- cy.get(yAxisTick).eq(0).should('have.text', '0 ms');
-});
diff --git a/x-pack/plugins/apm/e2e/package.json b/x-pack/plugins/apm/e2e/package.json
deleted file mode 100644
index 5839f4d58537c..0000000000000
--- a/x-pack/plugins/apm/e2e/package.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "apm-cypress",
- "version": "1.0.0",
- "main": "index.js",
- "license": "MIT",
- "scripts": {
- "cypress:open": "../../../../node_modules/.bin/cypress open",
- "cypress:run": "../../../../node_modules/.bin/cypress run --spec **/*.feature"
- }
-}
\ No newline at end of file
diff --git a/x-pack/plugins/apm/e2e/run-e2e.sh b/x-pack/plugins/apm/e2e/run-e2e.sh
index 6cdae93aec63b..85ab67bbf9a10 100755
--- a/x-pack/plugins/apm/e2e/run-e2e.sh
+++ b/x-pack/plugins/apm/e2e/run-e2e.sh
@@ -20,6 +20,8 @@ normal=$(tput sgr0)
E2E_DIR="${0%/*}"
TMP_DIR="tmp"
APM_IT_DIR="tmp/apm-integration-testing"
+WAIT_ON_BIN="../../../../node_modules/.bin/wait-on"
+CYPRESS_BIN="../../../../node_modules/.bin/cypress"
cd ${E2E_DIR}
@@ -92,14 +94,6 @@ if [ $? -ne 0 ]; then
exit 1
fi
-#
-# Cypress
-##################################################
-echo "" # newline
-echo "${bold}Cypress (logs: ${E2E_DIR}${TMP_DIR}/e2e-yarn.log)${normal}"
-echo "Installing cypress dependencies "
-yarn &> ${TMP_DIR}/e2e-yarn.log
-
#
# Static mock data
##################################################
@@ -148,7 +142,7 @@ fi
echo "" # newline
echo "${bold}Waiting for Kibana to start...${normal}"
echo "Note: you need to start Kibana manually. Find the instructions at the top."
-yarn wait-on -i 500 -w 500 http-get://admin:changeme@localhost:$KIBANA_PORT/api/status > /dev/null
+$WAIT_ON_BIN -i 500 -w 500 http-get://admin:changeme@localhost:$KIBANA_PORT/api/status > /dev/null
## Workaround to wait for the http server running
## See: https://github.com/elastic/kibana/issues/66326
@@ -165,7 +159,7 @@ echo "✅ Setup completed successfully. Running tests..."
#
# run cypress tests
##################################################
-yarn cypress run --config pageLoadTimeout=100000,watchForFileChanges=true
+$CYPRESS_BIN run --config pageLoadTimeout=100000,watchForFileChanges=true
e2e_status=$?
#
@@ -173,7 +167,7 @@ e2e_status=$?
##################################################
echo "${bold}If you want to run the test interactively, run:${normal}"
echo "" # newline
-echo "cd ${E2E_DIR} && yarn cypress open --config pageLoadTimeout=100000,watchForFileChanges=true"
+echo "cd ${E2E_DIR} && ${CYPRESS_BIN} open --config pageLoadTimeout=100000,watchForFileChanges=true"
# Report the e2e status at the very end
if [ $e2e_status -ne 0 ]; then
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx
index e17dd9a9eb038..a17bf7e93e466 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx
@@ -4,31 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import {
+ Axis,
+ Chart,
+ HistogramBarSeries,
+ niceTimeFormatter,
+ Position,
+ ScaleType,
+ Settings,
+ SettingsSpec,
+ TooltipValue,
+} from '@elastic/charts';
import { EuiTitle } from '@elastic/eui';
-import theme from '@elastic/eui/dist/eui_theme_light.json';
-import numeral from '@elastic/numeral';
-import { i18n } from '@kbn/i18n';
import d3 from 'd3';
-import { scaleUtc } from 'd3-scale';
-import { mean } from 'lodash';
import React from 'react';
import { asRelativeDateTimeRange } from '../../../../../common/utils/formatters';
-import { getTimezoneOffsetInMs } from '../../../shared/charts/CustomPlot/getTimezoneOffsetInMs';
-// @ts-expect-error
-import Histogram from '../../../shared/charts/Histogram';
-import { EmptyMessage } from '../../../shared/EmptyMessage';
-
-interface IBucket {
- key: number;
- count: number | undefined;
-}
-
-// TODO: cleanup duplication of this in distribution/get_distribution.ts (ErrorDistributionAPIResponse) and transactions/distribution/index.ts (TransactionDistributionAPIResponse)
-interface IDistribution {
- noHits: boolean;
- buckets: IBucket[];
- bucketSize: number;
-}
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import type { ErrorDistributionAPIResponse } from '../../../../../server/lib/errors/distribution/get_distribution';
+import { useTheme } from '../../../../hooks/useTheme';
interface FormattedBucket {
x0: number;
@@ -37,13 +30,9 @@ interface FormattedBucket {
}
export function getFormattedBuckets(
- buckets: IBucket[],
+ buckets: ErrorDistributionAPIResponse['buckets'],
bucketSize: number
-): FormattedBucket[] | null {
- if (!buckets) {
- return null;
- }
-
+): FormattedBucket[] {
return buckets.map(({ count, key }) => {
return {
x0: key,
@@ -54,76 +43,66 @@ export function getFormattedBuckets(
}
interface Props {
- distribution: IDistribution;
+ distribution: ErrorDistributionAPIResponse;
title: React.ReactNode;
}
-const tooltipHeader = (bucket: FormattedBucket) =>
- asRelativeDateTimeRange(bucket.x0, bucket.x);
-
export function ErrorDistribution({ distribution, title }: Props) {
+ const theme = useTheme();
const buckets = getFormattedBuckets(
distribution.buckets,
distribution.bucketSize
);
- if (!buckets) {
- return (
-
- );
- }
-
- const averageValue = mean(buckets.map((bucket) => bucket.y)) || 0;
const xMin = d3.min(buckets, (d) => d.x0);
- const xMax = d3.max(buckets, (d) => d.x);
- const tickFormat = scaleUtc().domain([xMin, xMax]).tickFormat();
+ const xMax = d3.max(buckets, (d) => d.x0);
+
+ const xFormatter = niceTimeFormatter([xMin, xMax]);
+
+ const tooltipProps: SettingsSpec['tooltip'] = {
+ headerFormatter: (tooltip: TooltipValue) => {
+ const serie = buckets.find((bucket) => bucket.x0 === tooltip.value);
+ if (serie) {
+ return asRelativeDateTimeRange(serie.x0, serie.x);
+ }
+ return `${tooltip.value}`;
+ },
+ };
return (
{title}
-
bucket.x}
- xType="time-utc"
- formatX={(value: Date) => {
- const time = value.getTime();
- return tickFormat(new Date(time - getTimezoneOffsetInMs(time)));
- }}
- buckets={buckets}
- bucketSize={distribution.bucketSize}
- formatYShort={(value: number) =>
- i18n.translate('xpack.apm.errorGroupDetails.occurrencesShortLabel', {
- defaultMessage: '{occCount} occ.',
- values: { occCount: value },
- })
- }
- formatYLong={(value: number) =>
- i18n.translate('xpack.apm.errorGroupDetails.occurrencesLongLabel', {
- defaultMessage:
- '{occCount} {occCount, plural, one {occurrence} other {occurrences}}',
- values: { occCount: value },
- })
- }
- legends={[
- {
- color: theme.euiColorVis1,
- // 0a abbreviates large whole numbers with metric prefixes like: 1000 = 1k, 32000 = 32k, 1000000 = 1m
- legendValue: numeral(averageValue).format('0a'),
- title: i18n.translate('xpack.apm.errorGroupDetails.avgLabel', {
- defaultMessage: 'Avg.',
- }),
- legendClickDisabled: true,
- },
- ]}
- />
+
);
}
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx
index 67125d41635a9..bf1bda793179f 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx
@@ -4,22 +4,37 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import {
+ Axis,
+ Chart,
+ ElementClickListener,
+ GeometryValue,
+ HistogramBarSeries,
+ Position,
+ RectAnnotation,
+ ScaleType,
+ Settings,
+ SettingsSpec,
+ TooltipValue,
+ XYChartSeriesIdentifier,
+} from '@elastic/charts';
import { EuiIconTip, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import d3 from 'd3';
import { isEmpty } from 'lodash';
import React, { useCallback } from 'react';
import { ValuesType } from 'utility-types';
+import { useTheme } from '../../../../../../observability/public';
import { getDurationFormatter } from '../../../../../common/utils/formatters';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution';
+import type { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { DistributionBucket } from '../../../../../server/lib/transactions/distribution/get_buckets';
+import type { DistributionBucket } from '../../../../../server/lib/transactions/distribution/get_buckets';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
-// @ts-expect-error
-import Histogram from '../../../shared/charts/Histogram';
+import { FETCH_STATUS } from '../../../../hooks/useFetcher';
+import { unit } from '../../../../style/variables';
+import { ChartContainer } from '../../../shared/charts/chart_container';
import { EmptyMessage } from '../../../shared/EmptyMessage';
-import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
interface IChartPoint {
x0: number;
@@ -31,10 +46,10 @@ interface IChartPoint {
}
export function getFormattedBuckets(
- buckets: DistributionBucket[],
- bucketSize: number
+ buckets?: DistributionBucket[],
+ bucketSize?: number
) {
- if (!buckets) {
+ if (!buckets || !bucketSize) {
return [];
}
@@ -74,7 +89,7 @@ const getFormatYLong = (transactionType: string | undefined) => (t: number) => {
'xpack.apm.transactionDetails.transactionsDurationDistributionChart.requestTypeUnitLongLabel',
{
defaultMessage:
- '{transCount, plural, =0 {# request} one {# request} other {# requests}}',
+ '{transCount, plural, =0 {request} one {request} other {requests}}',
values: {
transCount: t,
},
@@ -84,7 +99,7 @@ const getFormatYLong = (transactionType: string | undefined) => (t: number) => {
'xpack.apm.transactionDetails.transactionsDurationDistributionChart.transactionTypeUnitLongLabel',
{
defaultMessage:
- '{transCount, plural, =0 {# transaction} one {# transaction} other {# transactions}}',
+ '{transCount, plural, =0 {transaction} one {transaction} other {transactions}}',
values: {
transCount: t,
},
@@ -95,21 +110,21 @@ const getFormatYLong = (transactionType: string | undefined) => (t: number) => {
interface Props {
distribution?: TransactionDistributionAPIResponse;
urlParams: IUrlParams;
- isLoading: boolean;
+ fetchStatus: FETCH_STATUS;
bucketIndex: number;
onBucketClick: (
bucket: ValuesType
) => void;
}
-export function TransactionDistribution(props: Props) {
- const {
- distribution,
- urlParams: { transactionType },
- isLoading,
- bucketIndex,
- onBucketClick,
- } = props;
+export function TransactionDistribution({
+ distribution,
+ urlParams: { transactionType },
+ fetchStatus,
+ bucketIndex,
+ onBucketClick,
+}: Props) {
+ const theme = useTheme();
/* eslint-disable-next-line react-hooks/exhaustive-deps */
const formatYShort = useCallback(getFormatYShort(transactionType), [
@@ -122,12 +137,10 @@ export function TransactionDistribution(props: Props) {
]);
// no data in response
- if (!distribution || distribution.noHits) {
- // only show loading state if there is no data - else show stale data until new data has loaded
- if (isLoading) {
- return ;
- }
-
+ if (
+ (!distribution || distribution.noHits) &&
+ fetchStatus !== FETCH_STATUS.LOADING
+ ) {
return (
{
- return bucket.key === chartPoint.x0;
- });
-
- return clickedBucket;
- }
-
const buckets = getFormattedBuckets(
- distribution.buckets,
- distribution.bucketSize
+ distribution?.buckets,
+ distribution?.bucketSize
);
- const xMax = d3.max(buckets, (d) => d.x) || 0;
+ const xMin = d3.min(buckets, (d) => d.x0) || 0;
+ const xMax = d3.max(buckets, (d) => d.x0) || 0;
const timeFormatter = getDurationFormatter(xMax);
+ const tooltipProps: SettingsSpec['tooltip'] = {
+ headerFormatter: (tooltip: TooltipValue) => {
+ const serie = buckets.find((bucket) => bucket.x0 === tooltip.value);
+ if (serie) {
+ const xFormatted = timeFormatter(serie.x);
+ const x0Formatted = timeFormatter(serie.x0);
+ return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`;
+ }
+ return `${timeFormatter(tooltip.value)}`;
+ },
+ };
+
+ const onBarClick: ElementClickListener = (elements) => {
+ const chartPoint = elements[0][0] as GeometryValue;
+ const clickedBucket = distribution?.buckets.find((bucket) => {
+ return bucket.key === chartPoint.x;
+ });
+ if (clickedBucket) {
+ onBucketClick(clickedBucket);
+ }
+ };
+
+ const selectedBucket = buckets[bucketIndex];
+
return (
@@ -181,42 +211,66 @@ export function TransactionDistribution(props: Props) {
/>
-
-
{
- const clickedBucket = getBucketFromChartPoint(chartPoint);
-
- if (clickedBucket) {
- onBucketClick(clickedBucket);
- }
- }}
- formatX={(time: number) => timeFormatter(time).formatted}
- formatYShort={formatYShort}
- formatYLong={formatYLong}
- verticalLineHover={(point: IChartPoint) =>
- isEmpty(getBucketFromChartPoint(point)?.samples)
- }
- backgroundHover={(point: IChartPoint) =>
- !isEmpty(getBucketFromChartPoint(point)?.samples)
- }
- tooltipHeader={(point: IChartPoint) => {
- const xFormatted = timeFormatter(point.x);
- const x0Formatted = timeFormatter(point.x0);
- return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`;
- }}
- tooltipFooter={(point: IChartPoint) =>
- isEmpty(getBucketFromChartPoint(point)?.samples) &&
- i18n.translate(
- 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip',
- {
- defaultMessage: 'No sample available for this bucket',
- }
- )
- }
- />
+
+
+
+ {selectedBucket && (
+
+ )}
+ timeFormatter(time).formatted}
+ />
+ formatYShort(value)}
+ />
+ value}
+ minBarHeight={2}
+ id="transactionDurationDistribution"
+ name={(series: XYChartSeriesIdentifier) => {
+ const bucketCount = series.splitAccessors.get(
+ series.yAccessor
+ ) as number;
+ return formatYLong(bucketCount);
+ }}
+ splitSeriesAccessors={['y']}
+ xScaleType={ScaleType.Linear}
+ yScaleType={ScaleType.Linear}
+ xAccessor="x0"
+ yAccessors={['y']}
+ data={buckets}
+ color={theme.eui.euiColorVis1}
+ />
+
+
);
}
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
index efdd7b1f34221..e4c36b028e55c 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
@@ -52,7 +52,11 @@ export function TransactionDetails({
status: distributionStatus,
} = useTransactionDistribution(urlParams);
- const { data: transactionChartsData } = useTransactionCharts();
+ const {
+ data: transactionChartsData,
+ status: transactionChartsStatus,
+ } = useTransactionCharts();
+
const { waterfall, exceedsMax, status: waterfallStatus } = useWaterfall(
urlParams
);
@@ -121,6 +125,7 @@ export function TransactionDetails({
@@ -131,7 +136,7 @@ export function TransactionDetails({
{
diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx
index b7d1b93600a73..c530a7e1489ad 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx
@@ -4,12 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- fireEvent,
- getByText,
- queryByLabelText,
- render,
-} from '@testing-library/react';
+import { fireEvent, getByText, queryByLabelText } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { CoreStart } from 'kibana/public';
import React from 'react';
@@ -20,7 +15,10 @@ import { UrlParamsProvider } from '../../../context/UrlParamsContext';
import { IUrlParams } from '../../../context/UrlParamsContext/types';
import * as useFetcherHook from '../../../hooks/useFetcher';
import * as useServiceTransactionTypesHook from '../../../hooks/useServiceTransactionTypes';
-import { disableConsoleWarning } from '../../../utils/testHelpers';
+import {
+ disableConsoleWarning,
+ renderWithTheme,
+} from '../../../utils/testHelpers';
import { fromQuery } from '../../shared/Links/url_helpers';
import { TransactionOverview } from './';
@@ -54,7 +52,7 @@ function setup({
jest.spyOn(useFetcherHook, 'useFetcher').mockReturnValue({} as any);
- return render(
+ return renderWithTheme(
diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx
index 5444d2d521f37..df9e673ed4847 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx
@@ -22,7 +22,7 @@ import React, { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { useTrackPageview } from '../../../../../observability/public';
import { Projection } from '../../../../common/projections';
-import { LegacyChartsSyncContextProvider as ChartsSyncContextProvider } from '../../../context/charts_sync_context';
+import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types';
import { IUrlParams } from '../../../context/UrlParamsContext/types';
import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes';
import { useTransactionCharts } from '../../../hooks/useTransactionCharts';
@@ -33,11 +33,10 @@ import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
import { fromQuery, toQuery } from '../../shared/Links/url_helpers';
import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter';
+import { Correlations } from '../Correlations';
import { TransactionList } from './TransactionList';
import { useRedirect } from './useRedirect';
-import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types';
import { UserExperienceCallout } from './user_experience_callout';
-import { Correlations } from '../Correlations';
function getRedirectLocation({
urlParams,
@@ -83,7 +82,10 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
})
);
- const { data: transactionCharts } = useTransactionCharts();
+ const {
+ data: transactionCharts,
+ status: transactionChartsStatus,
+ } = useTransactionCharts();
useTrackPageview({ app: 'apm', path: 'transaction_overview' });
useTrackPageview({ app: 'apm', path: 'transaction_overview', delay: 15000 });
@@ -135,12 +137,11 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
>
)}
-
-
-
+
@@ -190,7 +191,7 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx
index 342152b572f1e..016ee3daf6b51 100644
--- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx
@@ -11,7 +11,7 @@ import styled from 'styled-components';
import { useTrackPageview } from '../../../../../observability/public';
import { isRumAgentName } from '../../../../common/agent_name';
import { ChartsSyncContextProvider } from '../../../context/charts_sync_context';
-import { ErroneousTransactionsRateChart } from '../../shared/charts/erroneous_transactions_rate_chart';
+import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart';
import { ErrorOverviewLink } from '../../shared/Links/apm/ErrorOverviewLink';
import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink';
import { TransactionOverviewLink } from '../../shared/Links/apm/TransactionOverviewLink';
@@ -125,19 +125,7 @@ export function ServiceOverview({
{!isRumAgentName(agentName) && (
-
-
-
- {i18n.translate(
- 'xpack.apm.serviceOverview.errorRateChartTitle',
- {
- defaultMessage: 'Error rate',
- }
- )}
-
-
-
-
+
)}
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx
index b908eb8da4d03..05cae589c19fc 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx
@@ -4,62 +4,113 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { throttle } from 'lodash';
-import React, { useMemo } from 'react';
+import {
+ AreaSeries,
+ Axis,
+ Chart,
+ niceTimeFormatter,
+ Placement,
+ Position,
+ ScaleType,
+ Settings,
+} from '@elastic/charts';
+import moment from 'moment';
+import React, { useEffect } from 'react';
+import { useHistory } from 'react-router-dom';
import { asPercent } from '../../../../../common/utils/formatters';
-import { useUiTracker } from '../../../../../../observability/public';
-import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
-import { Maybe } from '../../../../../typings/common';
-import { Coordinate, TimeSeries } from '../../../../../typings/timeseries';
+import { TimeSeries } from '../../../../../typings/timeseries';
+import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import { useUrlParams } from '../../../../hooks/useUrlParams';
-import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
-import { getEmptySeries } from '../../charts/CustomPlot/getEmptySeries';
-import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart';
+import { useChartsSync as useChartsSync2 } from '../../../../hooks/use_charts_sync';
+import { unit } from '../../../../style/variables';
+import { Annotations } from '../../charts/annotations';
+import { ChartContainer } from '../../charts/chart_container';
+import { onBrushEnd } from '../../charts/helper/helper';
+
+const XY_HEIGHT = unit * 16;
interface Props {
- timeseries: TimeSeries[];
- noHits: boolean;
+ fetchStatus: FETCH_STATUS;
+ timeseries?: TimeSeries[];
}
-const tickFormatY = (y: Maybe) => {
- return asPercent(y ?? 0, 1);
-};
+export function TransactionBreakdownGraph({ fetchStatus, timeseries }: Props) {
+ const history = useHistory();
+ const chartRef = React.createRef();
+ const { event, setEvent } = useChartsSync2();
+ const { urlParams } = useUrlParams();
+ const { start, end } = urlParams;
-const formatTooltipValue = (coordinate: Coordinate) => {
- return isValidCoordinateValue(coordinate.y)
- ? asPercent(coordinate.y, 1)
- : NOT_AVAILABLE_LABEL;
-};
+ useEffect(() => {
+ if (event.chartId !== 'timeSpentBySpan' && chartRef.current) {
+ chartRef.current.dispatchExternalPointerEvent(event);
+ }
+ }, [chartRef, event]);
-function TransactionBreakdownGraph({ timeseries, noHits }: Props) {
- const { urlParams } = useUrlParams();
- const { rangeFrom, rangeTo } = urlParams;
- const trackApmEvent = useUiTracker({ app: 'apm' });
- const handleHover = useMemo(
- () =>
- throttle(() => trackApmEvent({ metric: 'hover_breakdown_chart' }), 60000),
- [trackApmEvent]
- );
+ const min = moment.utc(start).valueOf();
+ const max = moment.utc(end).valueOf();
- const emptySeries =
- rangeFrom && rangeTo
- ? getEmptySeries(
- new Date(rangeFrom).getTime(),
- new Date(rangeTo).getTime()
- )
- : [];
+ const xFormatter = niceTimeFormatter([min, max]);
return (
-
+
+
+ onBrushEnd({ x, history })}
+ showLegend
+ showLegendExtra
+ legendPosition={Position.Bottom}
+ xDomain={{ min, max }}
+ flatLegend
+ onPointerUpdate={(currEvent: any) => {
+ setEvent(currEvent);
+ }}
+ externalPointerEvents={{
+ tooltip: { visible: true, placement: Placement.Bottom },
+ }}
+ />
+
+ asPercent(y ?? 0, 1)}
+ />
+
+
+
+ {timeseries?.length ? (
+ timeseries.map((serie) => {
+ return (
+
+ );
+ })
+ ) : (
+ // When timeseries is empty, loads an AreaSeries chart to show the default empty message.
+
+ )}
+
+
);
}
-
-export { TransactionBreakdownGraph };
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx
index 55826497ca385..9b0c041aaf7b5 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx
@@ -5,16 +5,13 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { isEmpty } from 'lodash';
import React from 'react';
-import { FETCH_STATUS } from '../../../hooks/useFetcher';
import { useTransactionBreakdown } from '../../../hooks/useTransactionBreakdown';
import { TransactionBreakdownGraph } from './TransactionBreakdownGraph';
function TransactionBreakdown() {
const { data, status } = useTransactionBreakdown();
const { timeseries } = data;
- const noHits = isEmpty(timeseries) && status === FETCH_STATUS.SUCCESS;
return (
@@ -29,7 +26,10 @@ function TransactionBreakdown() {
-
+
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js
deleted file mode 100644
index ca85ee961f5d8..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js
+++ /dev/null
@@ -1,29 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import PropTypes from 'prop-types';
-
-function SingleRect({ innerHeight, marginTop, style, x, width }) {
- return (
-
- );
-}
-
-SingleRect.requiresSVG = true;
-SingleRect.propTypes = {
- x: PropTypes.number.isRequired,
-};
-
-export default SingleRect;
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js
deleted file mode 100644
index 03fd039a3401e..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js
+++ /dev/null
@@ -1,119 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-
-import d3 from 'd3';
-import { HistogramInner } from '../index';
-import response from './response.json';
-import {
- disableConsoleWarning,
- toJson,
- mountWithTheme,
-} from '../../../../../utils/testHelpers';
-import { getFormattedBuckets } from '../../../../app/TransactionDetails/Distribution/index';
-import {
- asInteger,
- getDurationFormatter,
-} from '../../../../../../common/utils/formatters';
-
-describe('Histogram', () => {
- let mockConsole;
- let wrapper;
-
- const onClick = jest.fn();
-
- beforeAll(() => {
- mockConsole = disableConsoleWarning('Warning: componentWillReceiveProps');
- });
-
- afterAll(() => {
- mockConsole.mockRestore();
- });
-
- beforeEach(() => {
- const buckets = getFormattedBuckets(response.buckets, response.bucketSize);
- const xMax = d3.max(buckets, (d) => d.x);
- const timeFormatter = getDurationFormatter(xMax);
-
- wrapper = mountWithTheme(
- timeFormatter(time).formatted}
- formatYShort={(t) => `${asInteger(t)} occ.`}
- formatYLong={(t) => `${asInteger(t)} occurrences`}
- tooltipHeader={(bucket) => {
- const xFormatted = timeFormatter(bucket.x);
- const x0Formatted = timeFormatter(bucket.x0);
- return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`;
- }}
- width={800}
- />
- );
- });
-
- describe('Initially', () => {
- it('should have default markup', () => {
- expect(toJson(wrapper)).toMatchSnapshot();
- });
-
- it('should not show tooltip', () => {
- expect(wrapper.find('Tooltip').length).toBe(0);
- });
- });
-
- describe('when hovering over an empty bucket', () => {
- beforeEach(() => {
- wrapper.find('.rv-voronoi__cell').at(2).simulate('mouseOver');
- });
-
- it('should not display tooltip', () => {
- expect(wrapper.find('Tooltip').length).toBe(0);
- });
- });
-
- describe('when hovering over a non-empty bucket', () => {
- beforeEach(() => {
- wrapper.find('.rv-voronoi__cell').at(7).simulate('mouseOver');
- });
-
- it('should display tooltip', () => {
- const tooltips = wrapper.find('Tooltip');
-
- expect(tooltips.length).toBe(1);
- expect(tooltips.prop('header')).toBe('811 - 927 ms');
- expect(tooltips.prop('tooltipPoints')).toEqual([
- { value: '49 occurrences' },
- ]);
- expect(tooltips.prop('x')).toEqual(869010);
- expect(tooltips.prop('y')).toEqual(27.5);
- });
-
- it('should have correct markup for tooltip', () => {
- const tooltips = wrapper.find('Tooltip');
- expect(toJson(tooltips)).toMatchSnapshot();
- });
- });
-
- describe('when clicking on a non-empty bucket', () => {
- beforeEach(() => {
- wrapper.find('.rv-voronoi__cell').at(7).simulate('click');
- });
-
- it('should call onClick with bucket', () => {
- expect(onClick).toHaveBeenCalledWith({
- style: { cursor: 'pointer' },
- xCenter: 869010,
- x0: 811076,
- x: 926944,
- y: 49,
- });
- });
- });
-});
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap
deleted file mode 100644
index a31b9735628ab..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap
+++ /dev/null
@@ -1,1504 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Histogram Initially should have default markup 1`] = `
-.c0 {
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- position: absolute;
- top: 0;
- left: 0;
-}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0 ms
-
-
-
-
-
- 500 ms
-
-
-
-
-
- 1,000 ms
-
-
-
-
-
- 1,500 ms
-
-
-
-
-
- 2,000 ms
-
-
-
-
-
- 2,500 ms
-
-
-
-
-
- 3,000 ms
-
-
-
-
-
-
-
-
-
- 0 occ.
-
-
-
-
-
- 28 occ.
-
-
-
-
-
- 55 occ.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`Histogram when hovering over a non-empty bucket should have correct markup for tooltip 1`] = `
-.c0 {
- margin: 0 16px;
- -webkit-transform: translateY(-50%);
- -ms-transform: translateY(-50%);
- transform: translateY(-50%);
- border: 1px solid #d3dae6;
- background: #ffffff;
- border-radius: 4px;
- font-size: 14px;
- color: #000000;
-}
-
-.c1 {
- background: #f5f7fa;
- border-bottom: 1px solid #d3dae6;
- border-radius: 4px 4px 0 0;
- padding: 8px;
- color: #98a2b3;
-}
-
-.c2 {
- margin: 8px;
- margin-right: 16px;
- font-size: 12px;
-}
-
-.c4 {
- color: #98a2b3;
- margin: 8px;
- font-size: 12px;
-}
-
-.c3 {
- color: #69707d;
- font-size: 14px;
-}
-
-
-
-
-
-
- 811 - 927 ms
-
-
-
-
-
-
- 49 occurrences
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json
deleted file mode 100644
index 302e105dfa997..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json
+++ /dev/null
@@ -1,106 +0,0 @@
-{
- "buckets": [
- { "key": 0, "count": 0 },
- { "key": 115868, "count": 0 },
- { "key": 231736, "count": 0 },
- { "key": 347604, "count": 0 },
- { "key": 463472, "count": 0 },
- {
- "key": 579340,
- "count": 8,
- "samples": [
- {
- "transactionId": "99437ee4-08d4-41f5-9b2b-93cc32ec3dfb"
- }
- ]
- },
- {
- "key": 695208,
- "count": 23,
- "samples": [
- {
- "transactionId": "d327611b-e999-4942-a94f-c60208940180"
- }
- ]
- },
- {
- "key": 811076,
- "count": 49,
- "samples": [
- {
- "transactionId": "99c50a5b-44b4-4289-a3d1-a2815d128192"
- }
- ]
- },
- {
- "key": 926944,
- "count": 51,
- "transactionId": "9706a1ec-23f5-4ce8-97e8-69ce35fb0a9a"
- },
- {
- "key": 1042812,
- "count": 46,
- "transactionId": "f8d360c3-dd5e-47b6-b082-9e0bf821d3b2"
- },
- {
- "key": 1158680,
- "count": 13,
- "samples": [
- {
- "transactionId": "8486d3e2-7f15-48df-aa37-6ee9955adbd2"
- }
- ]
- },
- {
- "key": 1274548,
- "count": 7,
- "transactionId": "54b4b5a7-f065-4cab-9016-534e58f4fc0a"
- },
- {
- "key": 1390416,
- "count": 4,
- "transactionId": "8cfac2a3-38e7-4d3a-9792-d008b4bcb867"
- },
- {
- "key": 1506284,
- "count": 3,
- "transactionId": "ce3f3bd3-a37c-419e-bb9c-5db956ded149"
- },
- { "key": 1622152, "count": 0 },
- {
- "key": 1738020,
- "count": 4,
- "transactionId": "2300174b-85d8-40ba-a6cb-eeba2a49debf"
- },
- { "key": 1853888, "count": 0 },
- { "key": 1969756, "count": 0 },
- {
- "key": 2085624,
- "count": 1,
- "transactionId": "774955a4-2ba3-4461-81a6-65759db4805d"
- },
- { "key": 2201492, "count": 0 },
- { "key": 2317360, "count": 0 },
- { "key": 2433228, "count": 0 },
- { "key": 2549096, "count": 0 },
- { "key": 2664964, "count": 0 },
- {
- "key": 2780832,
- "count": 1,
- "transactionId": "035d1b9d-af71-46cf-8910-57bd4faf412d"
- },
- {
- "key": 2896700,
- "count": 1,
- "transactionId": "4a845b32-9de4-4796-8ef4-d7bbdedc9099"
- },
- { "key": 3012568, "count": 0 },
- {
- "key": 3128436,
- "count": 1,
- "transactionId": "68620ffb-7a1b-4f8e-b9bb-009fa5b092be"
- }
- ],
- "bucketSize": 115868,
- "defaultBucketIndex": 12
-}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js
deleted file mode 100644
index 3b2109d68c613..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js
+++ /dev/null
@@ -1,319 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React, { PureComponent } from 'react';
-import d3 from 'd3';
-import { isEmpty } from 'lodash';
-import PropTypes from 'prop-types';
-import { scaleLinear } from 'd3-scale';
-import styled from 'styled-components';
-import SingleRect from './SingleRect';
-import {
- XYPlot,
- XAxis,
- YAxis,
- HorizontalGridLines,
- VerticalRectSeries,
- Voronoi,
- makeWidthFlexible,
- VerticalGridLines,
-} from 'react-vis';
-import { unit } from '../../../../style/variables';
-import Tooltip from '../Tooltip';
-import theme from '@elastic/eui/dist/eui_theme_light.json';
-import { tint } from 'polished';
-import { getTimeTicksTZ, getDomainTZ } from '../helper/timezone';
-import Legends from '../CustomPlot/Legends';
-import StatusText from '../CustomPlot/StatusText';
-import { i18n } from '@kbn/i18n';
-import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
-
-const XY_HEIGHT = unit * 10;
-const XY_MARGIN = {
- top: unit,
- left: unit * 5,
- right: unit,
- bottom: unit * 2,
-};
-
-const X_TICK_TOTAL = 8;
-
-// position absolutely to make sure that window resizing/zooming works
-const ChartsWrapper = styled.div`
- user-select: none;
- position: absolute;
- top: 0;
- left: 0;
-`;
-
-export class HistogramInner extends PureComponent {
- constructor(props) {
- super(props);
- this.state = {
- hoveredBucket: {},
- };
- }
-
- onClick = (bucket) => {
- if (this.props.onClick) {
- this.props.onClick(bucket);
- }
- };
-
- onHover = (bucket) => {
- this.setState({ hoveredBucket: bucket });
- };
-
- onBlur = () => {
- this.setState({ hoveredBucket: {} });
- };
-
- getChartData(items, selectedItem) {
- const yMax = d3.max(items, (d) => d.y);
- const MINIMUM_BUCKET_SIZE = yMax * 0.02;
-
- return items.map((item) => {
- const padding = (item.x - item.x0) / 20;
- return {
- ...item,
- color:
- item === selectedItem
- ? theme.euiColorVis1
- : tint(0.5, theme.euiColorVis1),
- x0: item.x0 + padding,
- x: item.x - padding,
- y: item.y > 0 ? Math.max(item.y, MINIMUM_BUCKET_SIZE) : 0,
- };
- });
- }
-
- render() {
- const {
- backgroundHover,
- bucketIndex,
- buckets,
- bucketSize,
- formatX,
- formatYShort,
- formatYLong,
- tooltipFooter,
- tooltipHeader,
- verticalLineHover,
- width: XY_WIDTH,
- height,
- legends,
- } = this.props;
- const { hoveredBucket } = this.state;
- if (isEmpty(buckets) || XY_WIDTH === 0) {
- return null;
- }
-
- const isTimeSeries =
- this.props.xType === 'time' || this.props.xType === 'time-utc';
-
- const xMin = d3.min(buckets, (d) => d.x0);
- const xMax = d3.max(buckets, (d) => d.x);
- const yMin = 0;
- const yMax = d3.max(buckets, (d) => d.y);
- const selectedBucket = buckets[bucketIndex];
- const chartData = this.getChartData(buckets, selectedBucket);
-
- const x = scaleLinear()
- .domain([xMin, xMax])
- .range([XY_MARGIN.left, XY_WIDTH - XY_MARGIN.right]);
-
- const y = scaleLinear().domain([yMin, yMax]).range([XY_HEIGHT, 0]).nice();
-
- const [xMinZone, xMaxZone] = getDomainTZ(xMin, xMax);
- const xTickValues = isTimeSeries
- ? getTimeTicksTZ({
- domain: [xMinZone, xMaxZone],
- totalTicks: X_TICK_TOTAL,
- width: XY_WIDTH,
- })
- : undefined;
-
- const xDomain = x.domain();
- const yDomain = y.domain();
- const yTickValues = [0, yDomain[1] / 2, yDomain[1]];
- const shouldShowTooltip =
- hoveredBucket.x > 0 && (hoveredBucket.y > 0 || isTimeSeries);
-
- const showVerticalLineHover = verticalLineHover(hoveredBucket);
- const showBackgroundHover = backgroundHover(hoveredBucket);
-
- const hasValidCoordinates = buckets.some((bucket) =>
- isValidCoordinateValue(bucket.y)
- );
- const noHits = this.props.noHits || !hasValidCoordinates;
-
- const xyPlotProps = {
- dontCheckIfEmpty: true,
- xType: this.props.xType,
- width: XY_WIDTH,
- height: XY_HEIGHT,
- margin: XY_MARGIN,
- xDomain: xDomain,
- yDomain: yDomain,
- };
-
- const xAxisProps = {
- style: { strokeWidth: '1px' },
- marginRight: 10,
- tickSize: 0,
- tickTotal: X_TICK_TOTAL,
- tickFormat: formatX,
- tickValues: xTickValues,
- };
-
- const emptyStateChart = (
-
-
-
-
- );
-
- return (
-
-
- {noHits ? (
- <>{emptyStateChart}>
- ) : (
- <>
-
-
-
-
-
- {showBackgroundHover && (
-
- )}
-
- {shouldShowTooltip && (
-
- )}
-
- {selectedBucket && (
-
- )}
-
-
-
- {showVerticalLineHover && hoveredBucket?.x && (
-
- )}
-
- {
- return {
- ...bucket,
- xCenter: (bucket.x0 + bucket.x) / 2,
- };
- })}
- onClick={this.onClick}
- onHover={this.onHover}
- onBlur={this.onBlur}
- x={(d) => x(d.xCenter)}
- y={() => 1}
- />
-
-
- {legends && (
- {}}
- truncateLegends={false}
- noHits={noHits}
- />
- )}
- >
- )}
-
-
- );
- }
-}
-
-HistogramInner.propTypes = {
- backgroundHover: PropTypes.func,
- bucketIndex: PropTypes.number,
- buckets: PropTypes.array.isRequired,
- bucketSize: PropTypes.number.isRequired,
- formatX: PropTypes.func,
- formatYLong: PropTypes.func,
- formatYShort: PropTypes.func,
- onClick: PropTypes.func,
- tooltipFooter: PropTypes.func,
- tooltipHeader: PropTypes.func,
- verticalLineHover: PropTypes.func,
- width: PropTypes.number.isRequired,
- height: PropTypes.number,
- xType: PropTypes.string,
- legends: PropTypes.array,
- noHits: PropTypes.bool,
-};
-
-HistogramInner.defaultProps = {
- backgroundHover: () => null,
- formatYLong: (value) => value,
- formatYShort: (value) => value,
- tooltipFooter: () => null,
- tooltipHeader: () => null,
- verticalLineHover: () => null,
- xType: 'linear',
- noHits: false,
- height: XY_HEIGHT,
-};
-
-export default makeWidthFlexible(HistogramInner);
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx
deleted file mode 100644
index 2e4b51af00d6b..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx
+++ /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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React, { useCallback } from 'react';
-import { Coordinate, TimeSeries } from '../../../../../../typings/timeseries';
-import { useLegacyChartsSync as useChartsSync } from '../../../../../hooks/use_charts_sync';
-// @ts-expect-error
-import CustomPlot from '../../CustomPlot';
-
-interface Props {
- series: TimeSeries[];
- truncateLegends?: boolean;
- tickFormatY: (y: number) => React.ReactNode;
- formatTooltipValue: (c: Coordinate) => React.ReactNode;
- yMax?: string | number;
- height?: number;
- stacked?: boolean;
- onHover?: () => void;
- visibleLegendCount?: number;
- onToggleLegend?: (disabledSeriesState: boolean[]) => void;
-}
-
-function TransactionLineChart(props: Props) {
- const {
- series,
- tickFormatY,
- formatTooltipValue,
- yMax = 'max',
- height,
- truncateLegends,
- stacked = false,
- onHover,
- visibleLegendCount,
- onToggleLegend,
- } = props;
-
- const syncedChartsProps = useChartsSync();
-
- // combine callback for syncedChartsProps.onHover and props.onHover
- const combinedOnHover = useCallback(
- (hoverX: number) => {
- if (onHover) {
- onHover();
- }
- return syncedChartsProps.onHover(hoverX);
- },
- [syncedChartsProps, onHover]
- );
-
- return (
-
- );
-}
-
-export { TransactionLineChart };
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
index b3c0c3b6de857..2a5948d0ebf0b 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
@@ -20,104 +20,107 @@ import {
TRANSACTION_REQUEST,
TRANSACTION_ROUTE_CHANGE,
} from '../../../../../common/transaction_types';
+import { asDecimal, tpmUnit } from '../../../../../common/utils/formatters';
import { Coordinate } from '../../../../../typings/timeseries';
+import { ChartsSyncContextProvider } from '../../../../context/charts_sync_context';
import { LicenseContext } from '../../../../context/LicenseContext';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
+import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import { ITransactionChartData } from '../../../../selectors/chartSelectors';
-import { asDecimal, tpmUnit } from '../../../../../common/utils/formatters';
import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
-import { ErroneousTransactionsRateChart } from '../erroneous_transactions_rate_chart/legacy';
import { TransactionBreakdown } from '../../TransactionBreakdown';
-import {
- getResponseTimeTickFormatter,
- getResponseTimeTooltipFormatter,
-} from './helper';
+import { LineChart } from '../line_chart';
+import { TransactionErrorRateChart } from '../transaction_error_rate_chart/';
+import { getResponseTimeTickFormatter } from './helper';
import { MLHeader } from './ml_header';
-import { TransactionLineChart } from './TransactionLineChart';
import { useFormatter } from './use_formatter';
interface TransactionChartProps {
charts: ITransactionChartData;
urlParams: IUrlParams;
+ fetchStatus: FETCH_STATUS;
}
export function TransactionCharts({
charts,
urlParams,
+ fetchStatus,
}: TransactionChartProps) {
const getTPMFormatter = (t: number) => {
- const unit = tpmUnit(urlParams.transactionType);
- return `${asDecimal(t)} ${unit}`;
+ return `${asDecimal(t)} ${tpmUnit(urlParams.transactionType)}`;
};
- const getTPMTooltipFormatter = (p: Coordinate) => {
- return isValidCoordinateValue(p.y)
- ? getTPMFormatter(p.y)
- : NOT_AVAILABLE_LABEL;
+ const getTPMTooltipFormatter = (y: Coordinate['y']) => {
+ return isValidCoordinateValue(y) ? getTPMFormatter(y) : NOT_AVAILABLE_LABEL;
};
const { transactionType } = urlParams;
const { responseTimeSeries, tpmSeries } = charts;
- const { formatter, setDisabledSeriesState } = useFormatter(
- responseTimeSeries
- );
+ const { formatter, toggleSerie } = useFormatter(responseTimeSeries);
return (
<>
-
-
-
-
-
-
- {responseTimeLabel(transactionType)}
-
-
-
- {(license) => (
-
- )}
-
-
-
-
-
+
+
+
+
+
+
+
+ {responseTimeLabel(transactionType)}
+
+
+
+ {(license) => (
+
+ )}
+
+
+ {
+ if (serie) {
+ toggleSerie(serie);
+ }
+ }}
+ />
+
+
-
-
-
- {tpmLabel(transactionType)}
-
-
-
-
-
+
+
+
+ {tpmLabel(transactionType)}
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
>
);
}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx
index fc873cbda7bf2..958a5db6b66c9 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx
@@ -3,38 +3,17 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import { SeriesIdentifier } from '@elastic/charts';
+import { renderHook } from '@testing-library/react-hooks';
+import { act } from 'react-test-renderer';
+import { toMicroseconds } from '../../../../../common/utils/formatters';
import { TimeSeries } from '../../../../../typings/timeseries';
import { useFormatter } from './use_formatter';
-import { render, fireEvent, act } from '@testing-library/react';
-import { toMicroseconds } from '../../../../../common/utils/formatters';
-
-function MockComponent({
- timeSeries,
- disabledSeries,
- value,
-}: {
- timeSeries: TimeSeries[];
- disabledSeries: boolean[];
- value: number;
-}) {
- const { formatter, setDisabledSeriesState } = useFormatter(timeSeries);
-
- const onDisableSeries = () => {
- setDisabledSeriesState(disabledSeries);
- };
-
- return (
-
- disable series
- {formatter(value).formatted}
-
- );
-}
describe('useFormatter', () => {
const timeSeries = ([
{
+ title: 'avg',
data: [
{ x: 1, y: toMicroseconds(11, 'minutes') },
{ x: 2, y: toMicroseconds(1, 'minutes') },
@@ -42,6 +21,7 @@ describe('useFormatter', () => {
],
},
{
+ title: '95th percentile',
data: [
{ x: 1, y: toMicroseconds(120, 'seconds') },
{ x: 2, y: toMicroseconds(1, 'minutes') },
@@ -49,6 +29,7 @@ describe('useFormatter', () => {
],
},
{
+ title: '99th percentile',
data: [
{ x: 1, y: toMicroseconds(60, 'seconds') },
{ x: 2, y: toMicroseconds(5, 'minutes') },
@@ -56,54 +37,47 @@ describe('useFormatter', () => {
],
},
] as unknown) as TimeSeries[];
+
it('returns new formatter when disabled series state changes', () => {
- const { getByText } = render(
-
- );
- expect(getByText('2.0 min')).toBeInTheDocument();
+ const { result } = renderHook(() => useFormatter(timeSeries));
+ expect(
+ result.current.formatter(toMicroseconds(120, 'seconds')).formatted
+ ).toEqual('2.0 min');
+
act(() => {
- fireEvent.click(getByText('disable series'));
+ result.current.toggleSerie({
+ specId: 'avg',
+ } as SeriesIdentifier);
});
- expect(getByText('120 s')).toBeInTheDocument();
+
+ expect(
+ result.current.formatter(toMicroseconds(120, 'seconds')).formatted
+ ).toEqual('120 s');
});
+
it('falls back to the first formatter when disabled series is empty', () => {
- const { getByText } = render(
-
- );
- expect(getByText('2.0 min')).toBeInTheDocument();
+ const { result } = renderHook(() => useFormatter(timeSeries));
+ expect(
+ result.current.formatter(toMicroseconds(120, 'seconds')).formatted
+ ).toEqual('2.0 min');
+
act(() => {
- fireEvent.click(getByText('disable series'));
+ result.current.toggleSerie({
+ specId: 'avg',
+ } as SeriesIdentifier);
});
- expect(getByText('2.0 min')).toBeInTheDocument();
- // const { formatter, setDisabledSeriesState } = useFormatter(timeSeries);
- // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min');
- // setDisabledSeriesState([true, true, false]);
- // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min');
- });
- it('falls back to the first formatter when disabled series is all true', () => {
- const { getByText } = render(
-
- );
- expect(getByText('2.0 min')).toBeInTheDocument();
+
+ expect(
+ result.current.formatter(toMicroseconds(120, 'seconds')).formatted
+ ).toEqual('120 s');
+
act(() => {
- fireEvent.click(getByText('disable series'));
+ result.current.toggleSerie({
+ specId: 'avg',
+ } as SeriesIdentifier);
});
- expect(getByText('2.0 min')).toBeInTheDocument();
- // const { formatter, setDisabledSeriesState } = useFormatter(timeSeries);
- // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min');
- // setDisabledSeriesState([true, true, false]);
- // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min');
+ expect(
+ result.current.formatter(toMicroseconds(120, 'seconds')).formatted
+ ).toEqual('2.0 min');
});
});
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts
index d4694bc3caf1d..1475ec2934e95 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts
+++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts
@@ -4,8 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { useState, Dispatch, SetStateAction } from 'react';
-import { isEmpty } from 'lodash';
+import { SeriesIdentifier } from '@elastic/charts';
+import { omit } from 'lodash';
+import { useState } from 'react';
import {
getDurationFormatter,
TimeFormatter,
@@ -14,17 +15,36 @@ import { TimeSeries } from '../../../../../typings/timeseries';
import { getMaxY } from './helper';
export const useFormatter = (
- series: TimeSeries[]
+ series?: TimeSeries[]
): {
formatter: TimeFormatter;
- setDisabledSeriesState: Dispatch>;
+ toggleSerie: (disabledSerie: SeriesIdentifier) => void;
} => {
- const [disabledSeriesState, setDisabledSeriesState] = useState([]);
- const visibleSeries = series.filter(
- (serie, index) => disabledSeriesState[index] !== true
+ const [disabledSeries, setDisabledSeries] = useState<
+ Record
+ >({});
+
+ const visibleSeries = series?.filter(
+ (serie) => disabledSeries[serie.title] === undefined
);
- const maxY = getMaxY(isEmpty(visibleSeries) ? series : visibleSeries);
+
+ const maxY = getMaxY(visibleSeries || series || []);
const formatter = getDurationFormatter(maxY);
- return { formatter, setDisabledSeriesState };
+ const toggleSerie = ({ specId }: SeriesIdentifier) => {
+ if (disabledSeries[specId] !== undefined) {
+ setDisabledSeries((prevState) => {
+ return omit(prevState, specId);
+ });
+ } else {
+ setDisabledSeries((prevState) => {
+ return { ...prevState, [specId]: 0 };
+ });
+ }
+ };
+
+ return {
+ formatter,
+ toggleSerie,
+ };
};
diff --git a/x-pack/plugins/apm/public/components/shared/charts/annotations/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/annotations/index.tsx
new file mode 100644
index 0000000000000..683c66b2a96fe
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/charts/annotations/index.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ AnnotationDomainTypes,
+ LineAnnotation,
+ Position,
+} from '@elastic/charts';
+import { EuiIcon } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { asAbsoluteDateTime } from '../../../../../common/utils/formatters';
+import { useTheme } from '../../../../hooks/useTheme';
+import { useAnnotations } from '../../../../hooks/use_annotations';
+
+export function Annotations() {
+ const { annotations } = useAnnotations();
+ const theme = useTheme();
+
+ if (!annotations.length) {
+ return null;
+ }
+
+ const color = theme.eui.euiColorSecondary;
+
+ return (
+ ({
+ dataValue: annotation['@timestamp'],
+ header: asAbsoluteDateTime(annotation['@timestamp']),
+ details: `${i18n.translate('xpack.apm.chart.annotation.version', {
+ defaultMessage: 'Version',
+ })} ${annotation.text}`,
+ }))}
+ style={{ line: { strokeWidth: 1, stroke: color, opacity: 1 } }}
+ marker={ }
+ markerPosition={Position.Top}
+ />
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx
index 409cb69575ca9..c0e8f869ce647 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx
@@ -5,30 +5,97 @@
*/
import { render } from '@testing-library/react';
import React from 'react';
+import { FETCH_STATUS } from '../../../hooks/useFetcher';
import { ChartContainer } from './chart_container';
describe('ChartContainer', () => {
- describe('when isLoading is true', () => {
- it('shows loading the indicator', () => {
- const component = render(
-
+ describe('loading indicator', () => {
+ it('shows loading when status equals to Loading or Pending and has no data', () => {
+ [FETCH_STATUS.PENDING, FETCH_STATUS.LOADING].map((status) => {
+ const { queryAllByTestId } = render(
+
+ My amazing component
+
+ );
+
+ expect(queryAllByTestId('loading')[0]).toBeInTheDocument();
+ });
+ });
+ it('does not show loading when status equals to Loading or Pending and has data', () => {
+ [FETCH_STATUS.PENDING, FETCH_STATUS.LOADING].map((status) => {
+ const { queryAllByText } = render(
+
+ My amazing component
+
+ );
+ expect(queryAllByText('My amazing component')[0]).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('failure indicator', () => {
+ it('shows failure message when status equals to Failure and has data', () => {
+ const { getByText } = render(
+
My amazing component
);
-
- expect(component.getByTestId('loading')).toBeInTheDocument();
+ expect(
+ getByText(
+ 'An error happened when trying to fetch data. Please try again'
+ )
+ ).toBeInTheDocument();
+ });
+ it('shows failure message when status equals to Failure and has no data', () => {
+ const { getByText } = render(
+
+ My amazing component
+
+ );
+ expect(
+ getByText(
+ 'An error happened when trying to fetch data. Please try again'
+ )
+ ).toBeInTheDocument();
});
});
- describe('when isLoading is false', () => {
- it('does not show the loading indicator', () => {
- const component = render(
-
+ describe('render component', () => {
+ it('shows children component when status Success and has data', () => {
+ const { getByText } = render(
+
My amazing component
);
-
- expect(component.queryByTestId('loading')).not.toBeInTheDocument();
+ expect(getByText('My amazing component')).toBeInTheDocument();
+ });
+ it('shows children component when status Success and has no data', () => {
+ const { getByText } = render(
+
+ My amazing component
+
+ );
+ expect(getByText('My amazing component')).toBeInTheDocument();
});
});
});
diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx
index a6f579308597f..b4486f1e9b94a 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx
@@ -3,27 +3,56 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiLoadingChart } from '@elastic/eui';
+
+import { EuiLoadingChart, EuiText } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import React from 'react';
+import { FETCH_STATUS } from '../../../hooks/useFetcher';
interface Props {
- isLoading: boolean;
+ hasData: boolean;
+ status: FETCH_STATUS;
height: number;
children: React.ReactNode;
}
-export function ChartContainer({ isLoading, children, height }: Props) {
+export function ChartContainer({ children, height, status, hasData }: Props) {
+ if (
+ !hasData &&
+ (status === FETCH_STATUS.LOADING || status === FETCH_STATUS.PENDING)
+ ) {
+ return ;
+ }
+
+ if (status === FETCH_STATUS.FAILURE) {
+ return ;
+ }
+
+ return {children}
;
+}
+
+function LoadingChartPlaceholder({ height }: { height: number }) {
return (
- {isLoading && }
- {children}
+
);
}
+
+function FailedChartPlaceholder({ height }: { height: number }) {
+ return (
+
+ {i18n.translate('xpack.apm.chart.error', {
+ defaultMessage:
+ 'An error happened when trying to fetch data. Please try again',
+ })}
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/legacy.tsx b/x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/legacy.tsx
deleted file mode 100644
index 29102f606414f..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/legacy.tsx
+++ /dev/null
@@ -1,112 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
-import theme from '@elastic/eui/dist/eui_theme_light.json';
-import { i18n } from '@kbn/i18n';
-import { max } from 'lodash';
-import React, { useCallback } from 'react';
-import { useParams } from 'react-router-dom';
-import { asPercent } from '../../../../../common/utils/formatters';
-import { useLegacyChartsSync as useChartsSync } from '../../../../hooks/use_charts_sync';
-import { useFetcher } from '../../../../hooks/useFetcher';
-import { useUrlParams } from '../../../../hooks/useUrlParams';
-import { callApmApi } from '../../../../services/rest/createCallApmApi';
-// @ts-expect-error
-import CustomPlot from '../CustomPlot';
-
-const tickFormatY = (y?: number | null) => {
- return asPercent(y || 0, 1);
-};
-
-/**
- * "Legacy" version of this chart using react-vis charts. See index.tsx for the
- * Elastic Charts version.
- *
- * This will be removed with #70290.
- */
-export function ErroneousTransactionsRateChart() {
- const { serviceName } = useParams<{ serviceName?: string }>();
- const { urlParams, uiFilters } = useUrlParams();
- const syncedChartsProps = useChartsSync();
-
- const { start, end, transactionType, transactionName } = urlParams;
-
- const { data } = useFetcher(() => {
- if (serviceName && start && end) {
- return callApmApi({
- pathname:
- '/api/apm/services/{serviceName}/transaction_groups/error_rate',
- params: {
- path: {
- serviceName,
- },
- query: {
- start,
- end,
- transactionType,
- transactionName,
- uiFilters: JSON.stringify(uiFilters),
- },
- },
- });
- }
- }, [serviceName, start, end, uiFilters, transactionType, transactionName]);
-
- const combinedOnHover = useCallback(
- (hoverX: number) => {
- return syncedChartsProps.onHover(hoverX);
- },
- [syncedChartsProps]
- );
-
- const errorRates = data?.transactionErrorRate || [];
- const maxRate = max(errorRates.map((errorRate) => errorRate.y));
-
- return (
-
-
-
- {i18n.translate('xpack.apm.errorRateChart.title', {
- defaultMessage: 'Transaction error rate',
- })}
-
-
-
-
- Number.isFinite(y) ? tickFormatY(y) : 'N/A'
- }
- />
-
- );
-}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx
index 3f2a08ecb7641..507acc49d89db 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx
@@ -20,15 +20,17 @@ import moment from 'moment';
import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { TimeSeries } from '../../../../../typings/timeseries';
+import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { useChartsSync } from '../../../../hooks/use_charts_sync';
import { unit } from '../../../../style/variables';
+import { Annotations } from '../annotations';
import { ChartContainer } from '../chart_container';
import { onBrushEnd } from '../helper/helper';
interface Props {
id: string;
- isLoading: boolean;
+ fetchStatus: FETCH_STATUS;
onToggleLegend?: LegendItemListener;
timeseries: TimeSeries[];
/**
@@ -38,18 +40,20 @@ interface Props {
/**
* Formatter for legend and tooltip values
*/
- yTickFormat: (y: number) => string;
+ yTickFormat?: (y: number) => string;
+ showAnnotations?: boolean;
}
const XY_HEIGHT = unit * 16;
export function LineChart({
id,
- isLoading,
+ fetchStatus,
onToggleLegend,
timeseries,
yLabelFormat,
yTickFormat,
+ showAnnotations = true,
}: Props) {
const history = useHistory();
const chartRef = React.createRef();
@@ -84,7 +88,7 @@ export function LineChart({
);
return (
-
+
onBrushEnd({ x, history })}
@@ -115,11 +119,13 @@ export function LineChart({
id="y-axis"
ticks={3}
position={Position.Left}
- tickFormat={yTickFormat}
+ tickFormat={yTickFormat ? yTickFormat : yLabelFormat}
labelFormat={yLabelFormat}
showGridLines
/>
+ {showAnnotations && }
+
{timeseries.map((serie) => {
return (
();
const { urlParams, uiFilters } = useUrlParams();
@@ -56,25 +61,32 @@ export function ErroneousTransactionsRateChart() {
const errorRates = data?.transactionErrorRate || [];
return (
-
+
+
+
+ {i18n.translate('xpack.apm.errorRate', {
+ defaultMessage: 'Error rate',
+ })}
+
+
+
+
);
}
diff --git a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts
index 08d2300c3254a..0705383ecb0ca 100644
--- a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts
+++ b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts
@@ -15,7 +15,7 @@ export function useTransactionBreakdown() {
uiFilters,
} = useUrlParams();
- const { data = { timeseries: [] }, error, status } = useFetcher(
+ const { data = { timeseries: undefined }, error, status } = useFetcher(
(callApmApi) => {
if (serviceName && start && end && transactionType) {
return callApmApi({
diff --git a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts
index a5096a314388c..8c76225d03486 100644
--- a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts
+++ b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts
@@ -10,7 +10,7 @@ import { IUrlParams } from '../context/UrlParamsContext/types';
import { useFetcher } from './useFetcher';
import { useUiFilters } from '../context/UrlParamsContext';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { TransactionDistributionAPIResponse } from '../../server/lib/transactions/distribution';
+import type { TransactionDistributionAPIResponse } from '../../server/lib/transactions/distribution';
import { toQuery, fromQuery } from '../components/shared/Links/url_helpers';
import { maybe } from '../../common/utils/maybe';
diff --git a/x-pack/plugins/apm/public/hooks/useTransactionList.ts b/x-pack/plugins/apm/public/hooks/useTransactionList.ts
index 9c3a18b9c0d0d..b2c2cc30f78ec 100644
--- a/x-pack/plugins/apm/public/hooks/useTransactionList.ts
+++ b/x-pack/plugins/apm/public/hooks/useTransactionList.ts
@@ -14,8 +14,8 @@ type TransactionsAPIResponse = APIReturnType<
'/api/apm/services/{serviceName}/transaction_groups'
>;
-const DEFAULT_RESPONSE: TransactionsAPIResponse = {
- items: [],
+const DEFAULT_RESPONSE: Partial = {
+ items: undefined,
isAggregationAccurate: true,
bucketSize: 0,
};
diff --git a/x-pack/plugins/apm/public/hooks/use_annotations.ts b/x-pack/plugins/apm/public/hooks/use_annotations.ts
new file mode 100644
index 0000000000000..2b1c2bec52b3d
--- /dev/null
+++ b/x-pack/plugins/apm/public/hooks/use_annotations.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { useParams } from 'react-router-dom';
+import { callApmApi } from '../services/rest/createCallApmApi';
+import { useFetcher } from './useFetcher';
+import { useUrlParams } from './useUrlParams';
+
+const INITIAL_STATE = { annotations: [] };
+
+export function useAnnotations() {
+ const { serviceName } = useParams<{ serviceName?: string }>();
+ const { urlParams, uiFilters } = useUrlParams();
+ const { start, end } = urlParams;
+ const { environment } = uiFilters;
+
+ const { data = INITIAL_STATE } = useFetcher(() => {
+ if (start && end && serviceName) {
+ return callApmApi({
+ pathname: '/api/apm/services/{serviceName}/annotation/search',
+ params: {
+ path: {
+ serviceName,
+ },
+ query: {
+ start,
+ end,
+ environment,
+ },
+ },
+ });
+ }
+ }, [start, end, environment, serviceName]);
+
+ return data;
+}
diff --git a/x-pack/plugins/apm/public/selectors/chartSelectors.ts b/x-pack/plugins/apm/public/selectors/chartSelectors.ts
index 8c6093859f969..450f02f70c6a4 100644
--- a/x-pack/plugins/apm/public/selectors/chartSelectors.ts
+++ b/x-pack/plugins/apm/public/selectors/chartSelectors.ts
@@ -31,40 +31,37 @@ export interface ITpmBucket {
}
export interface ITransactionChartData {
- tpmSeries: ITpmBucket[];
- responseTimeSeries: TimeSeries[];
+ tpmSeries?: ITpmBucket[];
+ responseTimeSeries?: TimeSeries[];
mlJobId: string | undefined;
}
-const INITIAL_DATA = {
- apmTimeseries: {
- responseTimes: {
- avg: [],
- p95: [],
- p99: [],
- },
- tpmBuckets: [],
- overallAvgDuration: null,
- },
+const INITIAL_DATA: Partial = {
+ apmTimeseries: undefined,
anomalyTimeseries: undefined,
};
export function getTransactionCharts(
{ transactionType }: IUrlParams,
- { apmTimeseries, anomalyTimeseries }: TimeSeriesAPIResponse = INITIAL_DATA
+ charts = INITIAL_DATA
): ITransactionChartData {
- const tpmSeries = getTpmSeries(apmTimeseries, transactionType);
-
- const responseTimeSeries = getResponseTimeSeries({
- apmTimeseries,
- anomalyTimeseries,
- });
+ const { apmTimeseries, anomalyTimeseries } = charts;
- return {
- tpmSeries,
- responseTimeSeries,
+ const transactionCharts: ITransactionChartData = {
+ tpmSeries: undefined,
+ responseTimeSeries: undefined,
mlJobId: anomalyTimeseries?.jobId,
};
+
+ if (apmTimeseries) {
+ transactionCharts.tpmSeries = getTpmSeries(apmTimeseries, transactionType);
+
+ transactionCharts.responseTimeSeries = getResponseTimeSeries({
+ apmTimeseries,
+ anomalyTimeseries,
+ });
+ }
+ return transactionCharts;
}
export function getResponseTimeSeries({
diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts
index 090110b0454c0..29a0d1fdf4249 100644
--- a/x-pack/plugins/apm/server/index.ts
+++ b/x-pack/plugins/apm/server/index.ts
@@ -43,6 +43,7 @@ export const config = {
),
telemetryCollectionEnabled: schema.boolean({ defaultValue: true }),
metricsInterval: schema.number({ defaultValue: 30 }),
+ maxServiceEnvironments: schema.number({ defaultValue: 100 }),
}),
};
@@ -74,6 +75,7 @@ export function mergeConfigs(
'xpack.apm.serviceMapMaxTracesPerRequest':
apmConfig.serviceMapMaxTracesPerRequest,
'xpack.apm.ui.enabled': apmConfig.ui.enabled,
+ 'xpack.apm.maxServiceEnvironments': apmConfig.maxServiceEnvironments,
'xpack.apm.ui.maxTraceItems': apmConfig.ui.maxTraceItems,
'xpack.apm.ui.transactionGroupBucketSize':
apmConfig.ui.transactionGroupBucketSize,
diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts
index 7b63f2c354916..ecda5b0e8504b 100644
--- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts
+++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts
@@ -66,6 +66,7 @@ export function registerErrorCountAlertType({
config,
savedObjectsClient: services.savedObjectsClient,
});
+ const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments'];
const searchParams = {
index: indices['apm_oss.errorIndices'],
@@ -100,6 +101,7 @@ export function registerErrorCountAlertType({
environments: {
terms: {
field: SERVICE_ENVIRONMENT,
+ size: maxServiceEnvironments,
},
},
},
diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts
index 1d8b664751ba2..d9e69c8f3b7d7 100644
--- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts
+++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts
@@ -75,6 +75,7 @@ export function registerTransactionDurationAlertType({
config,
savedObjectsClient: services.savedObjectsClient,
});
+ const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments'];
const searchParams = {
index: indices['apm_oss.transactionIndices'],
@@ -112,6 +113,7 @@ export function registerTransactionDurationAlertType({
environments: {
terms: {
field: SERVICE_ENVIRONMENT,
+ size: maxServiceEnvironments,
},
},
},
diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts
index 969f4ceaca93a..06b296db5a485 100644
--- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts
+++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts
@@ -71,6 +71,7 @@ export function registerTransactionErrorRateAlertType({
config,
savedObjectsClient: services.savedObjectsClient,
});
+ const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments'];
const searchParams = {
index: indices['apm_oss.transactionIndices'],
@@ -120,6 +121,7 @@ export function registerTransactionErrorRateAlertType({
environments: {
terms: {
field: SERVICE_ENVIRONMENT,
+ size: maxServiceEnvironments,
},
},
},
diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts
index 95ff357937d47..39b4f7a7fe81b 100644
--- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts
+++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts
@@ -24,7 +24,8 @@ export async function getAllEnvironments({
searchAggregatedTransactions: boolean;
includeMissing?: boolean;
}) {
- const { apmEventClient } = setup;
+ const { apmEventClient, config } = setup;
+ const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments'];
// omit filter for service.name if "All" option is selected
const serviceNameFilter = serviceName
@@ -55,7 +56,7 @@ export async function getAllEnvironments({
environments: {
terms: {
field: SERVICE_ENVIRONMENT,
- size: 100,
+ size: maxServiceEnvironments,
...(!serviceName ? { min_doc_count: 0 } : {}),
missing: includeMissing ? ENVIRONMENT_NOT_DEFINED.value : undefined,
},
diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts
index a42710947a792..b12dd73a20986 100644
--- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts
+++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts
@@ -73,6 +73,6 @@ export async function getBuckets({
return {
noHits: resp.hits.total.value === 0,
- buckets,
+ buckets: resp.hits.total.value > 0 ? buckets : [],
};
}
diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts
index 2bfd3c94ed34c..9020cb1b9953a 100644
--- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts
@@ -7,14 +7,16 @@
import { ValuesType } from 'utility-types';
import { APMBaseDoc } from '../../../../../typings/es_schemas/raw/apm_base_doc';
import { APMError } from '../../../../../typings/es_schemas/ui/apm_error';
-import { KibanaRequest } from '../../../../../../../../src/core/server';
+import {
+ KibanaRequest,
+ LegacyScopedClusterClient,
+} from '../../../../../../../../src/core/server';
import { ProcessorEvent } from '../../../../../common/processor_event';
import {
ESSearchRequest,
ESSearchResponse,
} from '../../../../../typings/elasticsearch';
import { ApmIndicesConfig } from '../../../settings/apm_indices/get_apm_indices';
-import { APMRequestHandlerContext } from '../../../../routes/typings';
import { addFilterToExcludeLegacyData } from './add_filter_to_exclude_legacy_data';
import { callClientWithDebug } from '../call_client_with_debug';
import { Transaction } from '../../../../../typings/es_schemas/ui/transaction';
@@ -51,20 +53,23 @@ type TypedSearchResponse<
export type APMEventClient = ReturnType;
export function createApmEventClient({
- context,
+ esClient,
+ debug,
request,
indices,
options: { includeFrozen } = { includeFrozen: false },
}: {
- context: APMRequestHandlerContext;
+ esClient: Pick<
+ LegacyScopedClusterClient,
+ 'callAsInternalUser' | 'callAsCurrentUser'
+ >;
+ debug: boolean;
request: KibanaRequest;
indices: ApmIndicesConfig;
options: {
includeFrozen: boolean;
};
}) {
- const client = context.core.elasticsearch.legacy.client;
-
return {
search(
params: TParams,
@@ -77,14 +82,14 @@ export function createApmEventClient({
: withProcessorEventFilter;
return callClientWithDebug({
- apiCaller: client.callAsCurrentUser,
+ apiCaller: esClient.callAsCurrentUser,
operationName: 'search',
params: {
...withPossibleLegacyDataFilter,
ignore_throttled: !includeFrozen,
},
request,
- debug: context.params.query._debug,
+ debug,
});
},
};
diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
index 5e75535c678b3..363c4128137e0 100644
--- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
@@ -88,7 +88,8 @@ export async function setupRequest(
const coreSetupRequest = {
indices,
apmEventClient: createApmEventClient({
- context,
+ esClient: context.core.elasticsearch.legacy.client,
+ debug: context.params.query._debug,
request,
indices,
options: { includeFrozen },
diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap
index 3a38f80c87b35..a6818f96c728e 100644
--- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap
+++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap
@@ -366,6 +366,7 @@ Array [
"environments": Object {
"terms": Object {
"field": "service.environment",
+ "size": 100,
},
},
},
diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts
index 7d190c5b66450..fac80cf22c310 100644
--- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts
@@ -337,7 +337,8 @@ export const getEnvironments = async ({
setup,
projection,
}: AggregationParams) => {
- const { apmEventClient } = setup;
+ const { apmEventClient, config } = setup;
+ const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments'];
const response = await apmEventClient.search(
mergeProjection(projection, {
body: {
@@ -352,6 +353,7 @@ export const getEnvironments = async ({
environments: {
terms: {
field: SERVICE_ENVIRONMENT,
+ size: maxServiceEnvironments,
},
},
},
diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap
index 8db97a4929eb0..18ef3f44331d9 100644
--- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap
+++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap
@@ -127,7 +127,7 @@ Object {
"terms": Object {
"field": "service.environment",
"missing": "ALL_OPTION_VALUE",
- "size": 50,
+ "size": 100,
},
},
},
diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts
index 8327ac59a95b2..5e19f8f211cf7 100644
--- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts
+++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts
@@ -18,7 +18,8 @@ export async function getExistingEnvironmentsForService({
serviceName: string | undefined;
setup: Setup;
}) {
- const { internalClient, indices } = setup;
+ const { internalClient, indices, config } = setup;
+ const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments'];
const bool = serviceName
? { filter: [{ term: { [SERVICE_NAME]: serviceName } }] }
@@ -34,7 +35,7 @@ export async function getExistingEnvironmentsForService({
terms: {
field: SERVICE_ENVIRONMENT,
missing: ALL_OPTION_VALUE,
- size: 50,
+ size: maxServiceEnvironments,
},
},
},
diff --git a/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap
index d94b766aee6a8..3baaefe203ce7 100644
--- a/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap
+++ b/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap
@@ -15,6 +15,7 @@ Object {
"terms": Object {
"field": "service.environment",
"missing": "ENVIRONMENT_NOT_DEFINED",
+ "size": 100,
},
},
},
@@ -58,6 +59,7 @@ Object {
"terms": Object {
"field": "service.environment",
"missing": "ENVIRONMENT_NOT_DEFINED",
+ "size": 100,
},
},
},
diff --git a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts
index e72cc7e2483ad..b9f25e20f9f73 100644
--- a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts
+++ b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts
@@ -24,7 +24,7 @@ export async function getEnvironments({
serviceName?: string;
searchAggregatedTransactions: boolean;
}) {
- const { start, end, apmEventClient } = setup;
+ const { start, end, apmEventClient, config } = setup;
const filter: ESFilter[] = [{ range: rangeFilter(start, end) }];
@@ -34,6 +34,8 @@ export async function getEnvironments({
});
}
+ const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments'];
+
const params = {
apm: {
events: [
@@ -56,6 +58,7 @@ export async function getEnvironments({
terms: {
field: SERVICE_ENVIRONMENT,
missing: ENVIRONMENT_NOT_DEFINED.value,
+ size: maxServiceEnvironments,
},
},
},
diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts
index d3341b6c1b163..44269b1775953 100644
--- a/x-pack/plugins/apm/server/plugin.ts
+++ b/x-pack/plugins/apm/server/plugin.ts
@@ -10,14 +10,17 @@ import { map, take } from 'rxjs/operators';
import {
CoreSetup,
CoreStart,
+ KibanaRequest,
Logger,
Plugin,
PluginInitializerContext,
+ RequestHandlerContext,
} from 'src/core/server';
import { APMConfig, APMXPackConfig, mergeConfigs } from '.';
import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server';
import { HomeServerPluginSetup } from '../../../../src/plugins/home/server';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
+import { UI_SETTINGS } from '../../../../src/plugins/data/common';
import { ActionsPlugin } from '../../actions/server';
import { AlertingPlugin } from '../../alerts/server';
import { CloudSetup } from '../../cloud/server';
@@ -30,6 +33,7 @@ import { TaskManagerSetupContract } from '../../task_manager/server';
import { APM_FEATURE, registerFeaturesUsage } from './feature';
import { registerApmAlerts } from './lib/alerts/register_apm_alerts';
import { createApmTelemetry } from './lib/apm_telemetry';
+import { createApmEventClient } from './lib/helpers/create_es_client/create_apm_event_client';
import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_objects_client';
import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index';
import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices';
@@ -42,6 +46,11 @@ import { uiSettings } from './ui_settings';
export interface APMPluginSetup {
config$: Observable;
getApmIndices: () => ReturnType;
+ createApmEventClient: (params: {
+ debug?: boolean;
+ request: KibanaRequest;
+ context: RequestHandlerContext;
+ }) => Promise>;
}
export class APMPlugin implements Plugin {
@@ -141,13 +150,41 @@ export class APMPlugin implements Plugin {
},
});
+ const boundGetApmIndices = async () =>
+ getApmIndices({
+ savedObjectsClient: await getInternalSavedObjectsClient(core),
+ config: await mergedConfig$.pipe(take(1)).toPromise(),
+ });
+
return {
config$: mergedConfig$,
- getApmIndices: async () =>
- getApmIndices({
- savedObjectsClient: await getInternalSavedObjectsClient(core),
- config: await mergedConfig$.pipe(take(1)).toPromise(),
- }),
+ getApmIndices: boundGetApmIndices,
+ createApmEventClient: async ({
+ request,
+ context,
+ debug,
+ }: {
+ debug?: boolean;
+ request: KibanaRequest;
+ context: RequestHandlerContext;
+ }) => {
+ const [indices, includeFrozen] = await Promise.all([
+ boundGetApmIndices(),
+ context.core.uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN),
+ ]);
+
+ const esClient = context.core.elasticsearch.legacy.client;
+
+ return createApmEventClient({
+ debug: debug ?? false,
+ esClient,
+ request,
+ indices,
+ options: {
+ includeFrozen,
+ },
+ });
+ },
};
}
diff --git a/x-pack/plugins/apm/server/utils/test_helpers.tsx b/x-pack/plugins/apm/server/utils/test_helpers.tsx
index 18b990b35b5a5..21b59dc516d06 100644
--- a/x-pack/plugins/apm/server/utils/test_helpers.tsx
+++ b/x-pack/plugins/apm/server/utils/test_helpers.tsx
@@ -76,6 +76,9 @@ export async function inspectSearchParams(
case 'xpack.apm.metricsInterval':
return 30;
+
+ case 'xpack.apm.maxServiceEnvironments':
+ return 100;
}
},
}
diff --git a/x-pack/plugins/canvas/shareable_runtime/api/index.ts b/x-pack/plugins/canvas/shareable_runtime/api/index.ts
index 0780ab46cd873..dc7445eb7bc5a 100644
--- a/x-pack/plugins/canvas/shareable_runtime/api/index.ts
+++ b/x-pack/plugins/canvas/shareable_runtime/api/index.ts
@@ -7,5 +7,7 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import 'whatwg-fetch';
+import 'jquery';
+import '@kbn/ui-shared-deps/flot_charts';
export * from './shareable';
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 0b9f47e188d15..646978dd68153 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
@@ -221,6 +221,11 @@ export const setup = async () => {
setFreeze,
setIndexPriority: setIndexPriority('cold'),
},
+ delete: {
+ enable: enable('delete'),
+ setMinAgeValue: setMinAgeValue('delete'),
+ setMinAgeUnits: setMinAgeUnits('delete'),
+ },
},
};
};
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 11fadf51f27f8..4ee67d1ed8a19 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
@@ -367,7 +367,6 @@ describe(' ', () => {
expect(testBed.find('snapshotPolicyCombobox').prop('data-currentvalue')).toEqual([
{
label: DELETE_PHASE_POLICY.policy.phases.delete?.actions.wait_for_snapshot?.policy,
- value: DELETE_PHASE_POLICY.policy.phases.delete?.actions.wait_for_snapshot?.policy,
},
]);
});
@@ -412,7 +411,7 @@ describe(' ', () => {
test('wait for snapshot field should delete action if field is empty', async () => {
const { actions } = testBed;
- actions.setWaitForSnapshotPolicy('');
+ await actions.setWaitForSnapshotPolicy('');
await actions.savePolicy();
const expected = {
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx
index 4a3fedfb264ac..43910583ceec9 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx
@@ -20,27 +20,27 @@ import {
notificationServiceMock,
fatalErrorsServiceMock,
} from '../../../../../src/core/public/mocks';
+
import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks';
+
import { CloudSetup } from '../../../cloud/public';
import { EditPolicy } from '../../public/application/sections/edit_policy/edit_policy';
+import {
+ EditPolicyContextProvider,
+ EditPolicyContextValue,
+} from '../../public/application/sections/edit_policy/edit_policy_context';
+
import { KibanaContextProvider } from '../../public/shared_imports';
+
import { init as initHttp } from '../../public/application/services/http';
import { init as initUiMetric } from '../../public/application/services/ui_metric';
import { init as initNotification } from '../../public/application/services/notification';
import { PolicyFromES } from '../../common/types';
-import {
- positiveNumberRequiredMessage,
- policyNameRequiredMessage,
- policyNameStartsWithUnderscoreErrorMessage,
- policyNameContainsCommaErrorMessage,
- policyNameContainsSpaceErrorMessage,
- policyNameMustBeDifferentErrorMessage,
- policyNameAlreadyUsedErrorMessage,
-} from '../../public/application/services/policies/policy_validation';
import { i18nTexts } from '../../public/application/sections/edit_policy/i18n_texts';
import { editPolicyHelpers } from './helpers';
+import { defaultPolicy } from '../../public/application/constants';
// @ts-ignore
initHttp(axios.create({ adapter: axiosXhrAdapter }));
@@ -122,14 +122,11 @@ const noRollover = async (rendered: ReactWrapper) => {
const getNodeAttributeSelect = (rendered: ReactWrapper, phase: string) => {
return findTestSubject(rendered, `${phase}-selectedNodeAttrs`);
};
-const setPolicyName = (rendered: ReactWrapper, policyName: string) => {
+const setPolicyName = async (rendered: ReactWrapper, policyName: string) => {
const policyNameField = findTestSubject(rendered, 'policyNameField');
- policyNameField.simulate('change', { target: { value: policyName } });
- rendered.update();
-};
-const setPhaseAfterLegacy = (rendered: ReactWrapper, phase: string, after: string | number) => {
- const afterInput = rendered.find(`input#${phase}-selectedMinimumAge`);
- afterInput.simulate('change', { target: { value: after } });
+ await act(async () => {
+ policyNameField.simulate('change', { target: { value: policyName } });
+ });
rendered.update();
};
const setPhaseAfter = async (rendered: ReactWrapper, phase: string, after: string | number) => {
@@ -157,6 +154,32 @@ const save = async (rendered: ReactWrapper) => {
});
rendered.update();
};
+
+const MyComponent = ({
+ isCloudEnabled,
+ isNewPolicy,
+ policy: _policy,
+ existingPolicies,
+ getUrlForApp,
+ policyName,
+}: EditPolicyContextValue & { isCloudEnabled: boolean }) => {
+ return (
+
+
+
+
+
+ );
+};
+
describe('edit policy', () => {
beforeAll(() => {
jest.useFakeTimers();
@@ -179,14 +202,14 @@ describe('edit policy', () => {
beforeEach(() => {
component = (
-
-
-
+
);
({ http } = editPolicyHelpers.setup());
@@ -198,62 +221,78 @@ describe('edit policy', () => {
test('should show error when trying to save empty form', async () => {
const rendered = mountWithIntl(component);
await save(rendered);
- expectedErrorMessages(rendered, [policyNameRequiredMessage]);
+ expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.policyNameRequiredMessage]);
});
test('should show error when trying to save policy name with space', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'my policy');
- await save(rendered);
- expectedErrorMessages(rendered, [policyNameContainsSpaceErrorMessage]);
+ await setPolicyName(rendered, 'my policy');
+ waitForFormLibValidation(rendered);
+ expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]);
});
test('should show error when trying to save policy name that is already used', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'testy0');
- rendered.update();
- await save(rendered);
- expectedErrorMessages(rendered, [policyNameAlreadyUsedErrorMessage]);
+ await setPolicyName(rendered, 'testy0');
+ waitForFormLibValidation(rendered);
+ expectedErrorMessages(rendered, [
+ i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage,
+ ]);
});
test('should show error when trying to save as new policy but using the same name', async () => {
component = (
-
);
const rendered = mountWithIntl(component);
findTestSubject(rendered, 'saveAsNewSwitch').simulate('click');
rendered.update();
- setPolicyName(rendered, 'testy0');
- await save(rendered);
- expectedErrorMessages(rendered, [policyNameMustBeDifferentErrorMessage]);
+ await setPolicyName(rendered, 'testy0');
+ waitForFormLibValidation(rendered);
+ expectedErrorMessages(rendered, [
+ i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage,
+ ]);
});
test('should show error when trying to save policy name with comma', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'my,policy');
- await save(rendered);
- expectedErrorMessages(rendered, [policyNameContainsCommaErrorMessage]);
+ await setPolicyName(rendered, 'my,policy');
+ waitForFormLibValidation(rendered);
+ expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]);
});
test('should show error when trying to save policy name starting with underscore', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, '_mypolicy');
- await save(rendered);
- expectedErrorMessages(rendered, [policyNameStartsWithUnderscoreErrorMessage]);
+ await setPolicyName(rendered, '_mypolicy');
+ waitForFormLibValidation(rendered);
+ expectedErrorMessages(rendered, [
+ i18nTexts.editPolicy.errors.policyNameStartsWithUnderscoreErrorMessage,
+ ]);
});
test('should show correct json in policy flyout', async () => {
- const rendered = mountWithIntl(component);
+ const rendered = mountWithIntl(
+
+ );
await act(async () => {
findTestSubject(rendered, 'requestButton').simulate('click');
});
rendered.update();
+
const json = rendered.find(`code`).text();
- const expected = `PUT _ilm/policy/\n${JSON.stringify(
+ const expected = `PUT _ilm/policy/my-policy\n${JSON.stringify(
{
policy: {
phases: {
@@ -282,7 +321,7 @@ describe('edit policy', () => {
test('should show errors when trying to save with no max size and no max age', async () => {
const rendered = mountWithIntl(component);
expect(findTestSubject(rendered, 'rolloverSettingsRequired').exists()).toBeFalsy();
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored');
await act(async () => {
maxSizeInput.simulate('change', { target: { value: '' } });
@@ -298,7 +337,7 @@ describe('edit policy', () => {
});
test('should show number above 0 required error when trying to save with -1 for max size', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored');
await act(async () => {
maxSizeInput.simulate('change', { target: { value: '-1' } });
@@ -309,7 +348,7 @@ describe('edit policy', () => {
});
test('should show number above 0 required error when trying to save with 0 for max size', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored');
await act(async () => {
maxSizeInput.simulate('change', { target: { value: '-1' } });
@@ -319,7 +358,7 @@ describe('edit policy', () => {
});
test('should show number above 0 required error when trying to save with -1 for max age', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge');
await act(async () => {
maxAgeInput.simulate('change', { target: { value: '-1' } });
@@ -329,7 +368,7 @@ describe('edit policy', () => {
});
test('should show number above 0 required error when trying to save with 0 for max age', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge');
await act(async () => {
maxAgeInput.simulate('change', { target: { value: '0' } });
@@ -337,21 +376,21 @@ describe('edit policy', () => {
waitForFormLibValidation(rendered);
expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]);
});
- test('should show forcemerge input when rollover enabled', () => {
+ test('should show forcemerge input when rollover enabled', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeTruthy();
});
test('should hide forcemerge input when rollover is disabled', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await noRollover(rendered);
waitForFormLibValidation(rendered);
expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeFalsy();
});
test('should show positive number required above zero error when trying to save hot phase with 0 for force merge', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
act(() => {
findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click');
});
@@ -365,7 +404,7 @@ describe('edit policy', () => {
});
test('should show positive number above 0 required error when trying to save hot phase with -1 for force merge', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click');
rendered.update();
const forcemergeInput = findTestSubject(rendered, 'hot-selectedForceMergeSegments');
@@ -379,7 +418,7 @@ describe('edit policy', () => {
test('should show positive number required error when trying to save with -1 for index priority', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await setPhaseIndexPriority(rendered, 'hot', '-1');
waitForFormLibValidation(rendered);
expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]);
@@ -397,7 +436,7 @@ describe('edit policy', () => {
test('should show number required error when trying to save empty warm phase', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '');
waitForFormLibValidation(rendered);
@@ -406,7 +445,7 @@ describe('edit policy', () => {
test('should allow 0 for phase timing', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '0');
waitForFormLibValidation(rendered);
@@ -415,7 +454,7 @@ describe('edit policy', () => {
test('should show positive number required error when trying to save warm phase with -1 for after', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '-1');
waitForFormLibValidation(rendered);
@@ -424,7 +463,7 @@ describe('edit policy', () => {
test('should show positive number required error when trying to save warm phase with -1 for index priority', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '1');
await setPhaseAfter(rendered, 'warm', '-1');
@@ -434,7 +473,7 @@ describe('edit policy', () => {
test('should show positive number required above zero error when trying to save warm phase with 0 for shrink', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
act(() => {
findTestSubject(rendered, 'shrinkSwitch').simulate('click');
@@ -451,7 +490,7 @@ describe('edit policy', () => {
test('should show positive number above 0 required error when trying to save warm phase with -1 for shrink', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '1');
act(() => {
@@ -468,7 +507,7 @@ describe('edit policy', () => {
test('should show positive number required above zero error when trying to save warm phase with 0 for force merge', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '1');
act(() => {
@@ -485,7 +524,7 @@ describe('edit policy', () => {
test('should show positive number above 0 required error when trying to save warm phase with -1 for force merge', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '1');
await act(async () => {
@@ -503,7 +542,7 @@ describe('edit policy', () => {
server.respondImmediately = false;
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
@@ -517,7 +556,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
await openNodeAttributesSection(rendered, 'warm');
@@ -527,7 +566,7 @@ describe('edit policy', () => {
test('should show node attributes input when attributes exist', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
await openNodeAttributesSection(rendered, 'warm');
@@ -539,7 +578,7 @@ describe('edit policy', () => {
test('should show view node attributes link when attribute selected and show flyout when clicked', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
await openNodeAttributesSection(rendered, 'warm');
@@ -568,7 +607,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy();
@@ -581,7 +620,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeTruthy();
@@ -594,7 +633,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy();
@@ -611,7 +650,7 @@ describe('edit policy', () => {
test('should allow 0 for phase timing', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
await setPhaseAfter(rendered, 'cold', '0');
waitForFormLibValidation(rendered);
@@ -621,7 +660,7 @@ describe('edit policy', () => {
test('should show positive number required error when trying to save cold phase with -1 for after', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
await setPhaseAfter(rendered, 'cold', '-1');
waitForFormLibValidation(rendered);
@@ -631,7 +670,7 @@ describe('edit policy', () => {
server.respondImmediately = false;
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
@@ -645,7 +684,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
await openNodeAttributesSection(rendered, 'cold');
@@ -655,7 +694,7 @@ describe('edit policy', () => {
test('should show node attributes input when attributes exist', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
await openNodeAttributesSection(rendered, 'cold');
@@ -667,7 +706,7 @@ describe('edit policy', () => {
test('should show view node attributes link when attribute selected and show flyout when clicked', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
await openNodeAttributesSection(rendered, 'cold');
@@ -689,7 +728,7 @@ describe('edit policy', () => {
test('should show positive number required error when trying to save with -1 for index priority', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
await setPhaseAfter(rendered, 'cold', '1');
await setPhaseIndexPriority(rendered, 'cold', '-1');
@@ -704,7 +743,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy();
@@ -717,7 +756,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeTruthy();
@@ -730,7 +769,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy();
@@ -740,20 +779,20 @@ describe('edit policy', () => {
test('should allow 0 for phase timing', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'delete');
- setPhaseAfterLegacy(rendered, 'delete', '0');
- await save(rendered);
+ await setPhaseAfter(rendered, 'delete', '0');
+ waitForFormLibValidation(rendered);
expectedErrorMessages(rendered, []);
});
test('should show positive number required error when trying to save delete phase with -1 for after', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'delete');
- setPhaseAfterLegacy(rendered, 'delete', '-1');
- await save(rendered);
- expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
+ await setPhaseAfter(rendered, 'delete', '-1');
+ waitForFormLibValidation(rendered);
+ expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]);
});
});
describe('not on cloud', () => {
@@ -768,7 +807,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@@ -782,14 +821,13 @@ describe('edit policy', () => {
describe('on cloud', () => {
beforeEach(() => {
component = (
-
-
-
+
);
({ http } = editPolicyHelpers.setup());
({ server, httpRequestsMockHelpers } = http);
@@ -808,7 +846,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@@ -829,7 +867,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@@ -849,7 +887,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'cloudDataTierCallout').exists()).toBeTruthy();
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/policies.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/policies.ts
new file mode 100644
index 0000000000000..c4a91978a3765
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/policies.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { PolicyFromES } from '../../../common/types';
+
+export const splitSizeAndUnits = (field: string): { size: string; units: string } => {
+ let size = '';
+ let units = '';
+
+ const result = /(\d+)(\w+)/.exec(field);
+ if (result) {
+ size = result[1];
+ units = result[2];
+ }
+
+ return {
+ size,
+ units,
+ };
+};
+
+export const getPolicyByName = (
+ policies: PolicyFromES[] | null | undefined,
+ policyName: string = ''
+): PolicyFromES | undefined => {
+ if (policies && policies.length > 0) {
+ return policies.find((policy: PolicyFromES) => policy.name === policyName);
+ }
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts
index a04608338718e..326f6ff87dc3b 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts
@@ -7,11 +7,8 @@
export { ActiveBadge } from './active_badge';
export { ErrableFormRow } from './form_errors';
export { LearnMoreLink } from './learn_more_link';
-export { MinAgeInput } from './min_age_input_legacy';
export { OptionalLabel } from './optional_label';
-export { PhaseErrorMessage } from './phase_error_message';
export { PolicyJsonFlyout } from './policy_json_flyout';
-export { SnapshotPolicies } from './snapshot_policies';
export { DescribedFormField } from './described_form_field';
export * from './phases';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input_legacy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input_legacy.tsx
deleted file mode 100644
index 6fcf35b799289..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input_legacy.tsx
+++ /dev/null
@@ -1,263 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { i18n } from '@kbn/i18n';
-
-import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui';
-
-import { LearnMoreLink } from './learn_more_link';
-import { ErrableFormRow } from './form_errors';
-import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation';
-import { PhaseWithMinAge, Phases } from '../../../../../common/types';
-
-function getTimingLabelForPhase(phase: keyof Phases) {
- // NOTE: Hot phase isn't necessary, because indices begin in the hot phase.
- switch (phase) {
- case 'warm':
- return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeLabel', {
- defaultMessage: 'Timing for warm phase',
- });
-
- case 'cold':
- return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel', {
- defaultMessage: 'Timing for cold phase',
- });
-
- case 'delete':
- return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeLabel', {
- defaultMessage: 'Timing for delete phase',
- });
- }
-}
-
-function getUnitsAriaLabelForPhase(phase: keyof Phases) {
- // NOTE: Hot phase isn't necessary, because indices begin in the hot phase.
- switch (phase) {
- case 'warm':
- return i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeUnitsAriaLabel',
- {
- defaultMessage: 'Units for timing of warm phase',
- }
- );
-
- case 'cold':
- return i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel',
- {
- defaultMessage: 'Units for timing of cold phase',
- }
- );
-
- case 'delete':
- return i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeUnitsAriaLabel',
- {
- defaultMessage: 'Units for timing of delete phase',
- }
- );
- }
-}
-
-interface Props {
- rolloverEnabled: boolean;
- errors?: PhaseValidationErrors;
- phase: keyof Phases & string;
- phaseData: T;
- setPhaseData: (dataKey: keyof T & string, value: string) => void;
- isShowingErrors: boolean;
-}
-
-export const MinAgeInput = ({
- rolloverEnabled,
- errors,
- phaseData,
- phase,
- setPhaseData,
- isShowingErrors,
-}: React.PropsWithChildren>): React.ReactElement => {
- let daysOptionLabel;
- let hoursOptionLabel;
- let minutesOptionLabel;
- let secondsOptionLabel;
- let millisecondsOptionLabel;
- let microsecondsOptionLabel;
- let nanosecondsOptionLabel;
-
- if (rolloverEnabled) {
- daysOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverDaysOptionLabel',
- {
- defaultMessage: 'days from rollover',
- }
- );
-
- hoursOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverHoursOptionLabel',
- {
- defaultMessage: 'hours from rollover',
- }
- );
- minutesOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverMinutesOptionLabel',
- {
- defaultMessage: 'minutes from rollover',
- }
- );
-
- secondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverSecondsOptionLabel',
- {
- defaultMessage: 'seconds from rollover',
- }
- );
- millisecondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverMilliSecondsOptionLabel',
- {
- defaultMessage: 'milliseconds from rollover',
- }
- );
-
- microsecondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverMicroSecondsOptionLabel',
- {
- defaultMessage: 'microseconds from rollover',
- }
- );
-
- nanosecondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverNanoSecondsOptionLabel',
- {
- defaultMessage: 'nanoseconds from rollover',
- }
- );
- } else {
- daysOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationDaysOptionLabel',
- {
- defaultMessage: 'days from index creation',
- }
- );
-
- hoursOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationHoursOptionLabel',
- {
- defaultMessage: 'hours from index creation',
- }
- );
-
- minutesOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationMinutesOptionLabel',
- {
- defaultMessage: 'minutes from index creation',
- }
- );
-
- secondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationSecondsOptionLabel',
- {
- defaultMessage: 'seconds from index creation',
- }
- );
-
- millisecondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationMilliSecondsOptionLabel',
- {
- defaultMessage: 'milliseconds from index creation',
- }
- );
-
- microsecondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationMicroSecondsOptionLabel',
- {
- defaultMessage: 'microseconds from index creation',
- }
- );
-
- nanosecondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationNanoSecondsOptionLabel',
- {
- defaultMessage: 'nanoseconds from index creation',
- }
- );
- }
-
- // check that these strings are valid properties
- const selectedMinimumAgeProperty = propertyof('selectedMinimumAge');
- const selectedMinimumAgeUnitsProperty = propertyof('selectedMinimumAgeUnits');
- return (
-
-
-
- }
- />
- }
- >
- {
- setPhaseData(selectedMinimumAgeProperty, e.target.value);
- }}
- min={0}
- />
-
-
-
-
- setPhaseData(selectedMinimumAgeUnitsProperty, e.target.value)}
- options={[
- {
- value: 'd',
- text: daysOptionLabel,
- },
- {
- value: 'h',
- text: hoursOptionLabel,
- },
- {
- value: 'm',
- text: minutesOptionLabel,
- },
- {
- value: 's',
- text: secondsOptionLabel,
- },
- {
- value: 'ms',
- text: millisecondsOptionLabel,
- },
- {
- value: 'micros',
- text: microsecondsOptionLabel,
- },
- {
- value: 'nanos',
- text: nanosecondsOptionLabel,
- },
- ]}
- />
-
-
-
- );
-};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_error_message.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_error_message.tsx
deleted file mode 100644
index 750f68543f221..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_error_message.tsx
+++ /dev/null
@@ -1,19 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React from 'react';
-import { EuiBadge } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-
-export const PhaseErrorMessage = ({ isShowingErrors }: { isShowingErrors: boolean }) => {
- return isShowingErrors ? (
-
-
-
- ) : null;
-};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx
index 84e955a91ad7c..b87243bd1a9a1 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx
@@ -13,19 +13,13 @@ import { EuiDescribedFormGroup, EuiTextColor } from '@elastic/eui';
import { Phases } from '../../../../../../../common/types';
-import {
- useFormData,
- useFormContext,
- UseField,
- ToggleField,
- NumericField,
-} from '../../../../../../shared_imports';
+import { useFormData, UseField, ToggleField, NumericField } from '../../../../../../shared_imports';
import { useEditPolicyContext } from '../../../edit_policy_context';
-import { LearnMoreLink, ActiveBadge, PhaseErrorMessage, DescribedFormField } from '../../';
+import { LearnMoreLink, ActiveBadge, DescribedFormField } from '../../';
-import { MinAgeInputField, DataTierAllocationField, SetPriorityInput } from '../shared';
+import { MinAgeInputField, DataTierAllocationField, SetPriorityInput } from '../shared_fields';
const i18nTexts = {
dataTierAllocation: {
@@ -43,15 +37,13 @@ const formFieldPaths = {
};
export const ColdPhase: FunctionComponent = () => {
- const { originalPolicy } = useEditPolicyContext();
- const form = useFormContext();
+ const { policy } = useEditPolicyContext();
const [formData] = useFormData({
watch: [formFieldPaths.enabled],
});
const enabled = get(formData, formFieldPaths.enabled);
- const isShowingErrors = form.isValid === false;
return (
@@ -66,8 +58,7 @@ export const ColdPhase: FunctionComponent = () => {
defaultMessage="Cold phase"
/>
{' '}
- {enabled && !isShowingErrors ?
: null}
-
+ {enabled &&
}
}
titleSize="s"
@@ -128,9 +119,7 @@ export const ColdPhase: FunctionComponent = () => {
'xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel',
{ defaultMessage: 'Set replicas' }
),
- initialValue: Boolean(
- originalPolicy.phases.cold?.actions?.allocate?.number_of_replicas
- ),
+ initialValue: Boolean(policy.phases.cold?.actions?.allocate?.number_of_replicas),
}}
fullWidth
>
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx
similarity index 50%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx
index 78ae66327654c..37323b97edc92 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx
@@ -7,53 +7,24 @@
import React, { FunctionComponent, Fragment } from 'react';
import { get } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui';
+import { EuiDescribedFormGroup, EuiTextColor, EuiFormRow } from '@elastic/eui';
-import { DeletePhase as DeletePhaseInterface, Phases } from '../../../../../../common/types';
+import { useFormData, UseField, ToggleField } from '../../../../../../shared_imports';
-import { useFormData } from '../../../../../shared_imports';
+import { ActiveBadge, LearnMoreLink, OptionalLabel } from '../../index';
-import { PhaseValidationErrors } from '../../../../services/policies/policy_validation';
+import { MinAgeInputField, SnapshotPoliciesField } from '../shared_fields';
-import {
- ActiveBadge,
- LearnMoreLink,
- OptionalLabel,
- PhaseErrorMessage,
- MinAgeInput,
- SnapshotPolicies,
-} from '../';
-import { useRolloverPath } from './shared';
-
-const deleteProperty: keyof Phases = 'delete';
-const phaseProperty = (propertyName: keyof DeletePhaseInterface) => propertyName;
-
-interface Props {
- setPhaseData: (key: keyof DeletePhaseInterface & string, value: string | boolean) => void;
- phaseData: DeletePhaseInterface;
- isShowingErrors: boolean;
- errors?: PhaseValidationErrors;
- getUrlForApp: (
- appId: string,
- options?: {
- path?: string;
- absolute?: boolean;
- }
- ) => string;
-}
+const formFieldPaths = {
+ enabled: '_meta.delete.enabled',
+};
-export const DeletePhase: FunctionComponent = ({
- setPhaseData,
- phaseData,
- errors,
- isShowingErrors,
- getUrlForApp,
-}) => {
+export const DeletePhase: FunctionComponent = () => {
const [formData] = useFormData({
- watch: useRolloverPath,
+ watch: formFieldPaths.enabled,
});
- const hotPhaseRolloverEnabled = get(formData, useRolloverPath);
+ const enabled = get(formData, formFieldPaths.enabled);
return (
@@ -66,8 +37,7 @@ export const DeletePhase: FunctionComponent
= ({
defaultMessage="Delete phase"
/>
{' '}
- {phaseData.phaseEnabled && !isShowingErrors ? : null}
-
+ {enabled && }
}
titleSize="s"
@@ -79,39 +49,23 @@ export const DeletePhase: FunctionComponent = ({
defaultMessage="You no longer need your index. You can define when it is safe to delete it."
/>
-
- }
- id={`${deleteProperty}-${phaseProperty('phaseEnabled')}`}
- checked={phaseData.phaseEnabled}
- onChange={(e) => {
- setPhaseData(phaseProperty('phaseEnabled'), e.target.checked);
+
}
fullWidth
>
- {phaseData.phaseEnabled ? (
-
- errors={errors}
- phaseData={phaseData}
- phase={deleteProperty}
- isShowingErrors={isShowingErrors}
- setPhaseData={setPhaseData}
- rolloverEnabled={hotPhaseRolloverEnabled}
- />
- ) : (
-
- )}
+ {enabled && }
- {phaseData.phaseEnabled ? (
+ {enabled ? (
@@ -145,11 +99,7 @@ export const DeletePhase: FunctionComponent = ({
}
>
- setPhaseData(phaseProperty('waitForSnapshotPolicy'), value)}
- getUrlForApp={getUrlForApp}
- />
+
) : null}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/index.ts
new file mode 100644
index 0000000000000..488e4e26cfce0
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { DeletePhase } from './delete_phase';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
index a184ddf5148b9..629c1388f61fb 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
@@ -19,7 +19,6 @@ import {
import { Phases } from '../../../../../../../common/types';
import {
- useFormContext,
useFormData,
UseField,
SelectField,
@@ -29,26 +28,24 @@ import {
import { i18nTexts } from '../../../i18n_texts';
-import { ROLLOVER_EMPTY_VALIDATION } from '../../../form_validations';
+import { ROLLOVER_EMPTY_VALIDATION } from '../../../form';
import { ROLLOVER_FORM_PATHS } from '../../../constants';
-import { LearnMoreLink, ActiveBadge, PhaseErrorMessage } from '../../';
+import { LearnMoreLink, ActiveBadge } from '../../';
-import { Forcemerge, SetPriorityInput, useRolloverPath } from '../shared';
+import { Forcemerge, SetPriorityInput, useRolloverPath } from '../shared_fields';
import { maxSizeStoredUnits, maxAgeUnits } from './constants';
const hotProperty: keyof Phases = 'hot';
export const HotPhase: FunctionComponent = () => {
- const form = useFormContext();
const [formData] = useFormData({
watch: useRolloverPath,
});
const isRolloverEnabled = get(formData, useRolloverPath);
- const isShowingErrors = form.isValid === false;
const [showEmptyRolloverFieldsError, setShowEmptyRolloverFieldsError] = useState(false);
return (
@@ -62,8 +59,7 @@ export const HotPhase: FunctionComponent = () => {
defaultMessage="Hot phase"
/>
{' '}
- {isShowingErrors ? null : }
-
+
}
titleSize="s"
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/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
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/cloud_data_tier_callout.tsx
rename to 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
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/data_tier_allocation.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.scss
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/data_tier_allocation.scss
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.scss
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/data_tier_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/data_tier_allocation.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/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
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/default_allocation_notice.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/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
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/index.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/no_node_attributes_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_node_attributes_warning.tsx
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/no_node_attributes_warning.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_node_attributes_warning.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx
similarity index 90%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_allocation.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx
index 407bb9ea92e85..c1676d7074dbc 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_allocation.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx
@@ -10,12 +10,8 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiText, EuiSpacer } from '@elastic/eui';
-import { PhaseWithAllocationAction } from '../../../../../../../../../common/types';
-
import { UseField, SelectField, useFormData } from '../../../../../../../../shared_imports';
-import { propertyof } from '../../../../../../../services/policies/policy_validation';
-
import { LearnMoreLink } from '../../../../learn_more_link';
import { NodeAttrsDetails } from './node_attrs_details';
@@ -61,9 +57,6 @@ export const NodeAllocation: FunctionComponent = ({ phase, nodes })
nodeOptions.sort((a, b) => a.value.localeCompare(b.value));
- // check that this string is a valid property
- const nodeAttrsProperty = propertyof('selectedNodeAttrs');
-
return (
<>
@@ -100,7 +93,7 @@ export const NodeAllocation: FunctionComponent = ({ phase, nodes })
) : undefined,
euiFieldProps: {
- 'data-test-subj': `${phase}-${nodeAttrsProperty}`,
+ 'data-test-subj': `${phase}-selectedNodeAttrs`,
options: [{ text: i18nTexts.doNotModifyAllocationOption, value: '' }].concat(
nodeOptions
),
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_attrs_details.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_attrs_details.tsx
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_attrs_details.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_attrs_details.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_data_provider.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_data_provider.tsx
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_data_provider.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_data_provider.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/types.ts
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/types.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/types.ts
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/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
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/data_tier_allocation_field.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/index.ts
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/index.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/index.ts
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx
similarity index 94%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx
index b410bd0e6b3b0..b05d49be497cd 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx
@@ -21,11 +21,11 @@ interface Props {
}
export const Forcemerge: React.FunctionComponent = ({ phase }) => {
- const { originalPolicy } = useEditPolicyContext();
+ const { policy } = useEditPolicyContext();
const initialToggleValue = useMemo(() => {
- return Boolean(originalPolicy.phases[phase]?.actions?.forcemerge);
- }, [originalPolicy, phase]);
+ return Boolean(policy.phases[phase]?.actions?.forcemerge);
+ }, [policy, phase]);
return (
= ({ phase }) => {
- const phaseIndexPriorityProperty = propertyof('phaseIndexPriority');
return (
= ({ phase }) => {
componentProps={{
fullWidth: false,
euiFieldProps: {
- 'data-test-subj': `${phase}-${phaseIndexPriorityProperty}`,
- min: 1,
+ 'data-test-subj': `${phase}-phaseIndexPriority`,
+ min: 0,
},
}}
/>
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx
similarity index 68%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx
index cc2849b5c8e9c..e9f9f331e410a 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx
@@ -4,52 +4,39 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment } from 'react';
-
+import React from 'react';
+import { get } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-import { ApplicationStart } from 'kibana/public';
import {
EuiButtonIcon,
EuiCallOut,
- EuiComboBox,
EuiComboBoxOptionOption,
EuiLink,
EuiSpacer,
} from '@elastic/eui';
-import { useLoadSnapshotPolicies } from '../../../services/api';
+import { UseField, ComboBoxField, useFormData } from '../../../../../../shared_imports';
+import { useLoadSnapshotPolicies } from '../../../../../services/api';
+import { useEditPolicyContext } from '../../../edit_policy_context';
+
+const waitForSnapshotFormField = 'phases.delete.actions.wait_for_snapshot.policy';
-interface Props {
- value: string;
- onChange: (value: string) => void;
- getUrlForApp: ApplicationStart['getUrlForApp'];
-}
-export const SnapshotPolicies: React.FunctionComponent = ({
- value,
- onChange,
- getUrlForApp,
-}) => {
+export const SnapshotPoliciesField: React.FunctionComponent = () => {
+ const { getUrlForApp } = useEditPolicyContext();
const { error, isLoading, data, resendRequest } = useLoadSnapshotPolicies();
+ const [formData] = useFormData({
+ watch: waitForSnapshotFormField,
+ });
+
+ const selectedSnapshotPolicy = get(formData, waitForSnapshotFormField);
const policies = data.map((name: string) => ({
label: name,
value: name,
}));
- const onComboChange = (options: EuiComboBoxOptionOption[]) => {
- if (options.length > 0) {
- onChange(options[0].label);
- } else {
- onChange('');
- }
- };
-
- const onCreateOption = (newValue: string) => {
- onChange(newValue);
- };
-
const getUrlForSnapshotPolicyWizard = () => {
return getUrlForApp('management', {
path: `data/snapshot_restore/add_policy`,
@@ -59,14 +46,14 @@ export const SnapshotPolicies: React.FunctionComponent = ({
let calloutContent;
if (error) {
calloutContent = (
-
+ <>
+ <>
= ({
}
)}
/>
-
+ >
}
>
= ({
defaultMessage="Refresh this field and enter the name of an existing snapshot policy."
/>
-
+ >
);
} else if (data.length === 0) {
calloutContent = (
-
+ <>
= ({
}}
/>
-
+ >
);
- } else if (value && !data.includes(value)) {
+ } else if (selectedSnapshotPolicy && !data.includes(selectedSnapshotPolicy)) {
calloutContent = (
-
+ <>
= ({
}}
/>
-
+ >
);
}
return (
-
-
+ path={waitForSnapshotFormField}>
+ {(field) => {
+ const singleSelectionArray: [selectedSnapshot?: string] = field.value
+ ? [field.value]
+ : [];
+
+ return (
+ {
+ field.setValue(newOption);
},
- ]
- : []
- }
- onChange={onComboChange}
- noSuggestions={!!(error || data.length === 0)}
- />
+ onChange: (options: EuiComboBoxOptionOption[]) => {
+ if (options.length > 0) {
+ field.setValue(options[0].label);
+ } else {
+ field.setValue('');
+ }
+ },
+ }}
+ />
+ );
+ }}
+
{calloutContent}
-
+ >
);
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx
index 06c16e8bdd5ab..94fd2ee9edaca 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx
@@ -17,23 +17,17 @@ import {
EuiDescribedFormGroup,
} from '@elastic/eui';
-import {
- useFormData,
- UseField,
- ToggleField,
- useFormContext,
- NumericField,
-} from '../../../../../../shared_imports';
+import { useFormData, UseField, ToggleField, NumericField } from '../../../../../../shared_imports';
import { Phases } from '../../../../../../../common/types';
-import { useRolloverPath, MinAgeInputField, Forcemerge, SetPriorityInput } from '../shared';
+import { useRolloverPath, MinAgeInputField, Forcemerge, SetPriorityInput } from '../shared_fields';
import { useEditPolicyContext } from '../../../edit_policy_context';
-import { LearnMoreLink, ActiveBadge, PhaseErrorMessage, DescribedFormField } from '../../';
+import { LearnMoreLink, ActiveBadge, DescribedFormField } from '../../';
-import { DataTierAllocationField } from '../shared';
+import { DataTierAllocationField } from '../shared_fields';
const i18nTexts = {
shrinkLabel: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', {
@@ -54,8 +48,7 @@ const formFieldPaths = {
};
export const WarmPhase: FunctionComponent = () => {
- const { originalPolicy } = useEditPolicyContext();
- const form = useFormContext();
+ const { policy } = useEditPolicyContext();
const [formData] = useFormData({
watch: [useRolloverPath, formFieldPaths.enabled, formFieldPaths.warmPhaseOnRollover],
});
@@ -63,7 +56,6 @@ export const WarmPhase: FunctionComponent = () => {
const enabled = get(formData, formFieldPaths.enabled);
const hotPhaseRolloverEnabled = get(formData, useRolloverPath);
const warmPhaseOnRollover = get(formData, formFieldPaths.warmPhaseOnRollover);
- const isShowingErrors = form.isValid === false;
return (
@@ -77,8 +69,7 @@ export const WarmPhase: FunctionComponent = () => {
defaultMessage="Warm phase"
/>
{' '}
- {enabled && !isShowingErrors ?
: null}
-
+ {enabled &&
}
}
titleSize="s"
@@ -161,9 +152,7 @@ export const WarmPhase: FunctionComponent = () => {
'xpack.indexLifecycleMgmt.editPolicy.warmPhase.numberOfReplicas.switchLabel',
{ defaultMessage: 'Set replicas' }
),
- initialValue: Boolean(
- originalPolicy.phases.warm?.actions?.allocate?.number_of_replicas
- ),
+ initialValue: Boolean(policy.phases.warm?.actions?.allocate?.number_of_replicas),
}}
fullWidth
>
@@ -203,7 +192,7 @@ export const WarmPhase: FunctionComponent = () => {
'data-test-subj': 'shrinkSwitch',
label: i18nTexts.shrinkLabel,
'aria-label': i18nTexts.shrinkLabel,
- initialValue: Boolean(originalPolicy.phases.warm?.actions?.shrink),
+ initialValue: Boolean(policy.phases.warm?.actions?.shrink),
}}
fullWidth
>
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx
index 7098b018d6dfd..a8b1680ebde07 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx
@@ -7,7 +7,6 @@
import React, { useCallback, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-
import {
EuiButtonEmpty,
EuiCodeBlock,
@@ -25,19 +24,15 @@ import {
import { SerializedPolicy } from '../../../../../common/types';
import { useFormContext, useFormData } from '../../../../shared_imports';
+
import { FormInternal } from '../types';
interface Props {
- legacyPolicy: SerializedPolicy;
close: () => void;
policyName: string;
}
-export const PolicyJsonFlyout: React.FunctionComponent = ({
- policyName,
- close,
- legacyPolicy,
-}) => {
+export const PolicyJsonFlyout: React.FunctionComponent = ({ policyName, close }) => {
/**
* policy === undefined: we are checking validity
* policy === null: we have determined the policy is invalid
@@ -51,20 +46,11 @@ export const PolicyJsonFlyout: React.FunctionComponent = ({
const updatePolicy = useCallback(async () => {
setPolicy(undefined);
if (await validateForm()) {
- const p = getFormData() as SerializedPolicy;
- setPolicy({
- ...legacyPolicy,
- phases: {
- ...legacyPolicy.phases,
- hot: p.phases.hot,
- warm: p.phases.warm,
- cold: p.phases.cold,
- },
- });
+ setPolicy(getFormData() as SerializedPolicy);
} else {
setPolicy(null);
}
- }, [setPolicy, getFormData, legacyPolicy, validateForm]);
+ }, [setPolicy, getFormData, validateForm]);
useEffect(() => {
updatePolicy();
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx
index c82a420b74857..ebef80871b83d 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx
@@ -12,8 +12,11 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { useKibana } from '../../../shared_imports';
import { useLoadPoliciesList } from '../../services/api';
+import { getPolicyByName } from '../../lib/policies';
+import { defaultPolicy } from '../../constants';
import { EditPolicy as PresentationComponent } from './edit_policy';
+import { EditPolicyContextProvider } from './edit_policy_context';
interface RouterProps {
policyName: string;
@@ -44,6 +47,7 @@ export const EditPolicy: React.FunctionComponent {
breadcrumbService.setBreadcrumbs('editPolicy');
}, [breadcrumbService]);
+
if (isLoading) {
return (
+
+
+
);
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx
index 5397f5da2d6bb..1abbe884c2dc2 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx
@@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment, useEffect, useState, useCallback, useMemo } from 'react';
+import React, { Fragment, useEffect, useState, useMemo } from 'react';
+import { get } from 'lodash';
import { RouteComponentProps } from 'react-router-dom';
@@ -16,7 +17,6 @@ import {
EuiButton,
EuiButtonEmpty,
EuiDescribedFormGroup,
- EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
@@ -30,31 +30,13 @@ import {
EuiTitle,
} from '@elastic/eui';
-import { useForm, Form } from '../../../shared_imports';
+import { useForm, Form, UseField, TextField, useFormData } from '../../../shared_imports';
import { toasts } from '../../services/notification';
-import { LegacyPolicy, PolicyFromES, SerializedPolicy } from '../../../../common/types';
-
-import { defaultPolicy } from '../../constants';
-
-import {
- validatePolicy,
- ValidationErrors,
- findFirstError,
-} from '../../services/policies/policy_validation';
-
-import { savePolicy } from '../../services/policies/policy_save';
+import { savePolicy } from './save_policy';
import {
- deserializePolicy,
- getPolicyByName,
- initializeNewPolicy,
- legacySerializePolicy,
-} from '../../services/policies/policy_serialization';
-
-import {
- ErrableFormRow,
LearnMoreLink,
PolicyJsonFlyout,
ColdPhase,
@@ -63,93 +45,66 @@ import {
WarmPhase,
} from './components';
-import { schema } from './form_schema';
-import { deserializer } from './deserializer';
-import { createSerializer } from './serializer';
+import { schema, deserializer, createSerializer, createPolicyNameValidations } from './form';
-import { EditPolicyContextProvider } from './edit_policy_context';
+import { useEditPolicyContext } from './edit_policy_context';
+import { FormInternal } from './types';
export interface Props {
- policies: PolicyFromES[];
- policyName: string;
- getUrlForApp: (
- appId: string,
- options?: {
- path?: string;
- absolute?: boolean;
- }
- ) => string;
history: RouteComponentProps['history'];
}
-const mergeAllSerializedPolicies = (
- serializedPolicy: SerializedPolicy,
- legacySerializedPolicy: SerializedPolicy
-): SerializedPolicy => {
- return {
- ...legacySerializedPolicy,
- phases: {
- ...legacySerializedPolicy.phases,
- hot: serializedPolicy.phases.hot,
- warm: serializedPolicy.phases.warm,
- cold: serializedPolicy.phases.cold,
- },
- };
-};
+const policyNamePath = 'name';
-export const EditPolicy: React.FunctionComponent = ({
- policies,
- policyName,
- history,
- getUrlForApp,
-}) => {
+export const EditPolicy: React.FunctionComponent = ({ history }) => {
useEffect(() => {
window.scrollTo(0, 0);
}, []);
- const [isShowingErrors, setIsShowingErrors] = useState(false);
- const [errors, setErrors] = useState();
const [isShowingPolicyJsonFlyout, setIsShowingPolicyJsonFlyout] = useState(false);
-
- const existingPolicy = getPolicyByName(policies, policyName);
+ const {
+ isNewPolicy,
+ policy: currentPolicy,
+ existingPolicies,
+ policyName,
+ } = useEditPolicyContext();
const serializer = useMemo(() => {
- return createSerializer(existingPolicy?.policy);
- }, [existingPolicy?.policy]);
+ return createSerializer(isNewPolicy ? undefined : currentPolicy);
+ }, [isNewPolicy, currentPolicy]);
- const originalPolicy = existingPolicy?.policy ?? defaultPolicy;
+ const [saveAsNew, setSaveAsNew] = useState(isNewPolicy);
+ const originalPolicyName: string = isNewPolicy ? '' : policyName!;
const { form } = useForm({
schema,
- defaultValue: originalPolicy,
+ defaultValue: {
+ ...currentPolicy,
+ name: originalPolicyName,
+ },
deserializer,
serializer,
});
- const [policy, setPolicy] = useState(() =>
- existingPolicy ? deserializePolicy(existingPolicy) : initializeNewPolicy(policyName)
+ const [formData] = useFormData({ form, watch: policyNamePath });
+ const currentPolicyName = get(formData, policyNamePath);
+
+ const policyNameValidations = useMemo(
+ () =>
+ createPolicyNameValidations({
+ originalPolicyName,
+ policies: existingPolicies,
+ saveAsNewPolicy: saveAsNew,
+ }),
+ [originalPolicyName, existingPolicies, saveAsNew]
);
- const isNewPolicy: boolean = !Boolean(existingPolicy);
- const [saveAsNew, setSaveAsNew] = useState(isNewPolicy);
- const originalPolicyName: string = existingPolicy ? existingPolicy.name : '';
-
const backToPolicyList = () => {
history.push('/policies');
};
const submit = async () => {
- setIsShowingErrors(true);
- const { data: formLibPolicy, isValid: newIsValid } = await form.submit();
- const [legacyIsValid, validationErrors] = validatePolicy(
- saveAsNew,
- policy,
- policies,
- originalPolicyName
- );
- setErrors(validationErrors);
-
- const isValid = legacyIsValid && newIsValid;
+ const { data: policy, isValid } = await form.submit();
if (!isValid) {
toasts.addDanger(
@@ -157,22 +112,11 @@ export const EditPolicy: React.FunctionComponent = ({
defaultMessage: 'Please fix the errors on this page.',
})
);
- // This functionality will not be required for once form lib is fully adopted for this form
- // because errors are reported as fields are edited.
- if (!legacyIsValid) {
- const firstError = findFirstError(validationErrors);
- const errorRowId = `${firstError ? firstError.replace('.', '-') : ''}-row`;
- const element = document.getElementById(errorRowId);
- if (element) {
- element.scrollIntoView({ block: 'center', inline: 'nearest' });
- }
- }
} else {
- const readSerializedPolicy = () => {
- const legacySerializedPolicy = legacySerializePolicy(policy, existingPolicy?.policy);
- return mergeAllSerializedPolicies(formLibPolicy, legacySerializedPolicy);
- };
- const success = await savePolicy(readSerializedPolicy, isNewPolicy || saveAsNew);
+ const success = await savePolicy(
+ { ...policy, name: saveAsNew ? currentPolicyName : originalPolicyName },
+ isNewPolicy || saveAsNew
+ );
if (success) {
backToPolicyList();
}
@@ -183,248 +127,217 @@ export const EditPolicy: React.FunctionComponent = ({
setIsShowingPolicyJsonFlyout(!isShowingPolicyJsonFlyout);
};
- const setPhaseData = useCallback(
- (phase: keyof LegacyPolicy['phases'], key: string, value: any) => {
- setPolicy((nextPolicy) => ({
- ...nextPolicy,
- phases: {
- ...nextPolicy.phases,
- [phase]: { ...nextPolicy.phases[phase], [key]: value },
- },
- }));
- },
- [setPolicy]
- );
-
- const setDeletePhaseData = useCallback(
- (key: string, value: any) => setPhaseData('delete', key, value),
- [setPhaseData]
- );
-
return (
-
-
-
-
-
-
- {isNewPolicy
- ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage', {
- defaultMessage: 'Create an index lifecycle policy',
- })
- : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage', {
- defaultMessage: 'Edit index lifecycle policy {originalPolicyName}',
- values: { originalPolicyName },
- })}
-
-
-
-
+ }
+ titleSize="s"
+ fullWidth
+ >
+
+ path={policyNamePath}
+ config={{
+ label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.policyNameLabel', {
+ defaultMessage: 'Policy name',
+ }),
+ helpText: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.validPolicyNameMessage',
+ {
+ defaultMessage:
+ 'A policy name cannot start with an underscore and cannot contain a question mark or a space.',
+ }
+ ),
+ validations: policyNameValidations,
+ }}
+ component={TextField}
+ componentProps={{
+ fullWidth: false,
+ euiFieldProps: {
+ 'data-test-subj': 'policyNameField',
+ },
+ }}
+ />
+
+ ) : null}
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- 0
- }
- getUrlForApp={getUrlForApp}
- setPhaseData={setDeletePhaseData}
- phaseData={policy.phases.delete}
- />
+
-
-
-
-
-
-
-
- {saveAsNew ? (
-
- ) : (
-
- )}
-
-
-
-
-
+
+
+
+
+
+
+
+ {saveAsNew ? (
-
-
-
-
-
-
-
- {isShowingPolicyJsonFlyout ? (
-
- ) : (
+ ) : (
+
+ )}
+
+
+
+
+
- )}
-
-
-
-
- {isShowingPolicyJsonFlyout ? (
- setIsShowingPolicyJsonFlyout(false)}
- />
- ) : null}
-
-
-
-
-
-
+
+
+
+
+
+
+
+ {isShowingPolicyJsonFlyout ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {isShowingPolicyJsonFlyout ? (
+ setIsShowingPolicyJsonFlyout(false)}
+ />
+ ) : null}
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx
index 4748c26d6cec1..da5f940b1b6c8 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx
@@ -5,10 +5,16 @@
*/
import React, { createContext, ReactChild, useContext } from 'react';
-import { SerializedPolicy } from '../../../../common/types';
+import { ApplicationStart } from 'kibana/public';
-interface EditPolicyContextValue {
- originalPolicy: SerializedPolicy;
+import { PolicyFromES, SerializedPolicy } from '../../../../common/types';
+
+export interface EditPolicyContextValue {
+ isNewPolicy: boolean;
+ policy: SerializedPolicy;
+ existingPolicies: PolicyFromES[];
+ getUrlForApp: ApplicationStart['getUrlForApp'];
+ policyName?: string;
}
const EditPolicyContext = createContext(null as any);
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts
similarity index 82%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts
index f0294a5391d21..5af8807f2dec8 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts
@@ -6,17 +6,17 @@
import { produce } from 'immer';
-import { SerializedPolicy } from '../../../../common/types';
+import { SerializedPolicy } from '../../../../../common/types';
-import { splitSizeAndUnits } from '../../services/policies/policy_serialization';
+import { splitSizeAndUnits } from '../../../lib/policies';
-import { determineDataTierAllocationType } from '../../lib';
+import { determineDataTierAllocationType } from '../../../lib';
-import { FormInternal } from './types';
+import { FormInternal } from '../types';
export const deserializer = (policy: SerializedPolicy): FormInternal => {
const {
- phases: { hot, warm, cold },
+ phases: { hot, warm, cold, delete: deletePhase },
} = policy;
const _meta: FormInternal['_meta'] = {
@@ -37,6 +37,9 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => {
dataTierAllocationType: determineDataTierAllocationType(cold?.actions),
freezeEnabled: Boolean(cold?.actions?.freeze),
},
+ delete: {
+ enabled: Boolean(deletePhase),
+ },
};
return produce(
@@ -86,6 +89,14 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => {
draft._meta.cold.minAgeUnit = minAge.units;
}
}
+
+ if (draft.phases.delete) {
+ if (draft.phases.delete.min_age) {
+ const minAge = splitSizeAndUnits(draft.phases.delete.min_age);
+ draft.phases.delete.min_age = minAge.size;
+ draft._meta.delete.minAgeUnit = minAge.units;
+ }
+ }
}
);
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts
new file mode 100644
index 0000000000000..82fa478832582
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { deserializer } from './deserializer';
+
+export { createSerializer } from './serializer';
+
+export { schema } from './schema';
+
+export * from './validations';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts
similarity index 90%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts
index 070f03f74b954..4d20db4018740 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts
@@ -6,18 +6,19 @@
import { i18n } from '@kbn/i18n';
-import { FormSchema, fieldValidators } from '../../../shared_imports';
-import { defaultSetPriority, defaultPhaseIndexPriority } from '../../constants';
+import { FormSchema, fieldValidators } from '../../../../shared_imports';
+import { defaultSetPriority, defaultPhaseIndexPriority } from '../../../constants';
-import { FormInternal } from './types';
+import { FormInternal } from '../types';
import {
ifExistsNumberGreaterThanZero,
ifExistsNumberNonNegative,
rolloverThresholdsValidator,
-} from './form_validations';
+ minAgeValidator,
+} from './validations';
-import { i18nTexts } from './i18n_texts';
+import { i18nTexts } from '../i18n_texts';
const { emptyField, numberGreaterThanField } = fieldValidators;
@@ -97,6 +98,18 @@ export const schema: FormSchema = {
label: i18nTexts.editPolicy.allocationNodeAttributeFieldLabel,
},
},
+ delete: {
+ enabled: {
+ defaultValue: false,
+ label: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.deletePhase.activateWarmPhaseSwitchLabel',
+ { defaultMessage: 'Activate delete phase' }
+ ),
+ },
+ minAgeUnit: {
+ defaultValue: 'd',
+ },
+ },
},
phases: {
hot: {
@@ -177,15 +190,7 @@ export const schema: FormSchema = {
defaultValue: '0',
validations: [
{
- validator: (arg) =>
- numberGreaterThanField({
- than: 0,
- allowEquality: true,
- message: i18nTexts.editPolicy.errors.nonNegativeNumberRequired,
- })({
- ...arg,
- value: arg.value === '' ? -Infinity : parseInt(arg.value, 10),
- }),
+ validator: minAgeValidator,
},
],
},
@@ -256,15 +261,7 @@ export const schema: FormSchema = {
defaultValue: '0',
validations: [
{
- validator: (arg) =>
- numberGreaterThanField({
- than: 0,
- allowEquality: true,
- message: i18nTexts.editPolicy.errors.nonNegativeNumberRequired,
- })({
- ...arg,
- value: arg.value === '' ? -Infinity : parseInt(arg.value, 10),
- }),
+ validator: minAgeValidator,
},
],
},
@@ -292,5 +289,15 @@ export const schema: FormSchema = {
},
},
},
+ delete: {
+ min_age: {
+ defaultValue: '0',
+ validations: [
+ {
+ validator: minAgeValidator,
+ },
+ ],
+ },
+ },
},
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer.ts
similarity index 90%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer.ts
index 564b5a2c4e397..2274efda426ad 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer.ts
@@ -4,12 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { isEmpty } from 'lodash';
+import { isEmpty, isNumber } from 'lodash';
-import { SerializedPolicy, SerializedActionWithAllocation } from '../../../../common/types';
+import { SerializedPolicy, SerializedActionWithAllocation } from '../../../../../common/types';
-import { FormInternal, DataAllocationMetaFields } from './types';
-import { isNumber } from '../../services/policies/policy_serialization';
+import { FormInternal, DataAllocationMetaFields } from '../types';
const serializeAllocateAction = (
{ dataTierAllocationType, allocationNodeAttribute }: DataAllocationMetaFields,
@@ -165,5 +164,22 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => (
}
}
+ /**
+ * DELETE PHASE SERIALIZATION
+ */
+ if (policy.phases.delete) {
+ if (policy.phases.delete.min_age) {
+ policy.phases.delete.min_age = `${policy.phases.delete.min_age}${_meta.delete.minAgeUnit}`;
+ }
+
+ if (originalPolicy?.phases.delete?.actions) {
+ const { wait_for_snapshot: __, ...rest } = originalPolicy.phases.delete.actions;
+ policy.phases.delete.actions = {
+ ...policy.phases.delete.actions,
+ ...rest,
+ };
+ }
+ }
+
return policy;
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts
similarity index 50%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts
index 9c855ccb41624..f2e26a552efc9 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts
@@ -4,13 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { fieldValidators, ValidationFunc } from '../../../shared_imports';
+import { fieldValidators, ValidationFunc, ValidationConfig } from '../../../../shared_imports';
-import { ROLLOVER_FORM_PATHS } from './constants';
+import { ROLLOVER_FORM_PATHS } from '../constants';
-import { i18nTexts } from './i18n_texts';
+import { i18nTexts } from '../i18n_texts';
+import { PolicyFromES } from '../../../../../common/types';
+import { FormInternal } from '../types';
-const { numberGreaterThanField } = fieldValidators;
+const { numberGreaterThanField, containsCharsField, emptyField, startsWithField } = fieldValidators;
const createIfNumberExistsValidator = ({
than,
@@ -46,7 +48,7 @@ export const ifExistsNumberNonNegative = createIfNumberExistsValidator({
* A special validation type used to keep track of validation errors for
* the rollover threshold values not being set (e.g., age and doc count)
*/
-export const ROLLOVER_EMPTY_VALIDATION = 'EMPTY';
+export const ROLLOVER_EMPTY_VALIDATION = 'ROLLOVER_EMPTY_VALIDATION';
/**
* An ILM policy requires that for rollover a value must be set for one of the threshold values.
@@ -87,3 +89,68 @@ export const rolloverThresholdsValidator: ValidationFunc = ({ form }) => {
fields[ROLLOVER_FORM_PATHS.maxSize].clearErrors(ROLLOVER_EMPTY_VALIDATION);
}
};
+
+export const minAgeValidator: ValidationFunc = (arg) =>
+ numberGreaterThanField({
+ than: 0,
+ allowEquality: true,
+ message: i18nTexts.editPolicy.errors.nonNegativeNumberRequired,
+ })({
+ ...arg,
+ value: arg.value === '' ? -Infinity : parseInt(arg.value, 10),
+ });
+
+export const createPolicyNameValidations = ({
+ policies,
+ saveAsNewPolicy,
+ originalPolicyName,
+}: {
+ policies: PolicyFromES[];
+ saveAsNewPolicy: boolean;
+ originalPolicyName?: string;
+}): Array> => {
+ return [
+ {
+ validator: emptyField(i18nTexts.editPolicy.errors.policyNameRequiredMessage),
+ },
+ {
+ validator: startsWithField({
+ message: i18nTexts.editPolicy.errors.policyNameStartsWithUnderscoreErrorMessage,
+ char: '_',
+ }),
+ },
+ {
+ validator: containsCharsField({
+ message: i18nTexts.editPolicy.errors.policyNameContainsInvalidChars,
+ chars: [',', ' '],
+ }),
+ },
+ {
+ validator: (arg) => {
+ const policyName = arg.value;
+ if (window.TextEncoder && new window.TextEncoder().encode(policyName).length > 255) {
+ return {
+ message: i18nTexts.editPolicy.errors.policyNameTooLongErrorMessage,
+ };
+ }
+ },
+ },
+ {
+ validator: (arg) => {
+ const policyName = arg.value;
+ if (saveAsNewPolicy && policyName === originalPolicyName) {
+ return {
+ message: i18nTexts.editPolicy.errors.policyNameMustBeDifferentErrorMessage,
+ };
+ } else if (policyName !== originalPolicyName) {
+ const policyNames = policies.map((existingPolicy) => existingPolicy.name);
+ if (policyNames.includes(policyName)) {
+ return {
+ message: i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage,
+ };
+ }
+ }
+ },
+ },
+ ];
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts
index 1fba69b7634ae..ccd5d3a568fe3 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts
@@ -98,6 +98,42 @@ export const i18nTexts = {
defaultMessage: 'Only non-negative numbers are allowed.',
}
),
+ policyNameContainsInvalidChars: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.errors.policyNameContainsInvalidCharsError',
+ {
+ defaultMessage: 'A policy name cannot contain spaces or commas.',
+ }
+ ),
+ policyNameAlreadyUsedErrorMessage: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError',
+ {
+ defaultMessage: 'That policy name is already used.',
+ }
+ ),
+ policyNameMustBeDifferentErrorMessage: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.differentPolicyNameRequiredError',
+ {
+ defaultMessage: 'The policy name must be different.',
+ }
+ ),
+ policyNameRequiredMessage: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.policyNameRequiredError',
+ {
+ defaultMessage: 'A policy name is required.',
+ }
+ ),
+ policyNameStartsWithUnderscoreErrorMessage: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.policyNameStartsWithUnderscoreError',
+ {
+ defaultMessage: 'A policy name cannot start with an underscore.',
+ }
+ ),
+ policyNameTooLongErrorMessage: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.policyNameTooLongError',
+ {
+ defaultMessage: 'A policy name cannot be longer than 255 bytes.',
+ }
+ ),
},
},
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/save_policy.ts
similarity index 84%
rename from x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/save_policy.ts
index 9cf622e830cb2..e2ab6a8817ef6 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/save_policy.ts
@@ -3,23 +3,22 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
import { i18n } from '@kbn/i18n';
import { METRIC_TYPE } from '@kbn/analytics';
import { SerializedPolicy } from '../../../../common/types';
-import { savePolicy as savePolicyApi } from '../api';
-import { showApiError } from '../api_errors';
-import { getUiMetricsForPhases, trackUiMetric } from '../ui_metric';
+
import { UIM_POLICY_CREATE, UIM_POLICY_UPDATE } from '../../constants';
-import { toasts } from '../notification';
+
+import { toasts } from '../../services/notification';
+import { savePolicy as savePolicyApi } from '../../services/api';
+import { getUiMetricsForPhases, trackUiMetric } from '../../services/ui_metric';
+import { showApiError } from '../../services/api_errors';
export const savePolicy = async (
- readSerializedPolicy: () => SerializedPolicy,
+ serializedPolicy: SerializedPolicy,
isNew: boolean
): Promise => {
- const serializedPolicy = readSerializedPolicy();
-
try {
await savePolicyApi(serializedPolicy);
} catch (err) {
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts
index 1884f8dbc0619..dc3d8a640e682 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts
@@ -38,6 +38,10 @@ interface ColdPhaseMetaFields extends DataAllocationMetaFields, MinAgeField {
freezeEnabled: boolean;
}
+interface DeletePhaseMetaFields extends MinAgeField {
+ enabled: boolean;
+}
+
/**
* Describes the shape of data after deserialization.
*/
@@ -50,5 +54,6 @@ export interface FormInternal extends SerializedPolicy {
hot: HotPhaseMetaFields;
warm: WarmPhaseMetaFields;
cold: ColdPhaseMetaFields;
+ delete: DeletePhaseMetaFields;
};
}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts
deleted file mode 100644
index 6ada039d45cd9..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts
+++ /dev/null
@@ -1,88 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { DeletePhase, SerializedDeletePhase } from '../../../../common/types';
-import { serializedPhaseInitialization } from '../../constants';
-import { isNumber, splitSizeAndUnits } from './policy_serialization';
-import {
- numberRequiredMessage,
- PhaseValidationErrors,
- positiveNumberRequiredMessage,
-} from './policy_validation';
-
-const deletePhaseInitialization: DeletePhase = {
- phaseEnabled: false,
- selectedMinimumAge: '0',
- selectedMinimumAgeUnits: 'd',
- waitForSnapshotPolicy: '',
-};
-
-export const deletePhaseFromES = (phaseSerialized?: SerializedDeletePhase): DeletePhase => {
- const phase = { ...deletePhaseInitialization };
- if (phaseSerialized === undefined || phaseSerialized === null) {
- return phase;
- }
-
- phase.phaseEnabled = true;
- if (phaseSerialized.min_age) {
- const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age);
- phase.selectedMinimumAge = minAge;
- phase.selectedMinimumAgeUnits = minAgeUnits;
- }
-
- if (phaseSerialized.actions) {
- const actions = phaseSerialized.actions;
-
- if (actions.wait_for_snapshot) {
- phase.waitForSnapshotPolicy = actions.wait_for_snapshot.policy;
- }
- }
-
- return phase;
-};
-
-export const deletePhaseToES = (
- phase: DeletePhase,
- originalEsPhase?: SerializedDeletePhase
-): SerializedDeletePhase => {
- if (!originalEsPhase) {
- originalEsPhase = { ...serializedPhaseInitialization };
- }
- const esPhase = { ...originalEsPhase };
-
- if (isNumber(phase.selectedMinimumAge)) {
- esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`;
- }
-
- esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {};
-
- if (phase.waitForSnapshotPolicy) {
- esPhase.actions.wait_for_snapshot = {
- policy: phase.waitForSnapshotPolicy,
- };
- } else {
- delete esPhase.actions.wait_for_snapshot;
- }
-
- return esPhase;
-};
-
-export const validateDeletePhase = (phase: DeletePhase): PhaseValidationErrors => {
- if (!phase.phaseEnabled) {
- return {};
- }
-
- const phaseErrors = {} as PhaseValidationErrors;
-
- // min age needs to be a positive number
- if (!isNumber(phase.selectedMinimumAge)) {
- phaseErrors.selectedMinimumAge = [numberRequiredMessage];
- } else if (parseInt(phase.selectedMinimumAge, 10) < 0) {
- phaseErrors.selectedMinimumAge = [positiveNumberRequiredMessage];
- }
-
- return { ...phaseErrors };
-};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts
deleted file mode 100644
index 19481b39a2c80..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts
+++ /dev/null
@@ -1,198 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-// Prefer importing entire lodash library, e.g. import { get } from "lodash"
-// eslint-disable-next-line no-restricted-imports
-import cloneDeep from 'lodash/cloneDeep';
-import { deserializePolicy, legacySerializePolicy } from './policy_serialization';
-import { defaultNewDeletePhase } from '../../constants';
-
-describe('Policy serialization', () => {
- test('serialize a policy using "default" data allocation', () => {
- expect(
- legacySerializePolicy(
- {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- },
- {
- name: 'test',
- phases: {
- hot: { actions: {} },
- },
- }
- )
- ).toEqual({
- name: 'test',
- phases: {},
- });
- });
-
- test('serialize a policy using "custom" data allocation', () => {
- expect(
- legacySerializePolicy(
- {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- },
- {
- name: 'test',
- phases: {
- hot: { actions: {} },
- },
- }
- )
- ).toEqual({
- name: 'test',
- phases: {},
- });
- });
-
- test('serialize a policy using "custom" data allocation with no node attributes', () => {
- expect(
- legacySerializePolicy(
- {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- },
- {
- name: 'test',
- phases: {
- hot: { actions: {} },
- },
- }
- )
- ).toEqual({
- // There should be no allocation action in any phases...
- name: 'test',
- phases: {},
- });
- });
-
- test('serialize a policy using "none" data allocation with no node attributes', () => {
- expect(
- legacySerializePolicy(
- {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- },
- {
- name: 'test',
- phases: {
- hot: { actions: {} },
- },
- }
- )
- ).toEqual({
- // There should be no allocation action in any phases...
- name: 'test',
- phases: {},
- });
- });
-
- test('serialization does not alter the original policy', () => {
- const originalPolicy = {
- name: 'test',
- phases: {},
- };
-
- const originalClone = cloneDeep(originalPolicy);
-
- const deserializedPolicy = {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- };
-
- legacySerializePolicy(deserializedPolicy, originalPolicy);
- expect(originalPolicy).toEqual(originalClone);
- });
-
- test('serialize a policy using "best_compression" codec for forcemerge', () => {
- expect(
- legacySerializePolicy(
- {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- },
- {
- name: 'test',
- phases: {
- hot: { actions: {} },
- },
- }
- )
- ).toEqual({
- name: 'test',
- phases: {},
- });
- });
-
- test('de-serialize a policy using "best_compression" codec for forcemerge', () => {
- expect(
- deserializePolicy({
- modified_date: Date.now().toString(),
- name: 'test',
- version: 1,
- policy: {
- name: 'test',
- phases: {
- hot: {
- actions: {
- rollover: {
- max_age: '30d',
- max_size: '50gb',
- },
- forcemerge: {
- max_num_segments: 1,
- index_codec: 'best_compression',
- },
- set_priority: {
- priority: 100,
- },
- },
- },
- },
- },
- })
- ).toEqual({
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- });
- });
-
- test('delete "best_compression" codec for forcemerge if disabled in UI', () => {
- expect(
- legacySerializePolicy(
- {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- },
- {
- name: 'test',
- phases: {},
- }
- )
- ).toEqual({
- name: 'test',
- phases: {},
- });
- });
-});
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts
deleted file mode 100644
index 55e9d88dcd383..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts
+++ /dev/null
@@ -1,82 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { LegacyPolicy, PolicyFromES, SerializedPolicy } from '../../../../common/types';
-
-import { defaultNewDeletePhase, serializedPhaseInitialization } from '../../constants';
-
-import { deletePhaseFromES, deletePhaseToES } from './delete_phase';
-
-export const splitSizeAndUnits = (field: string): { size: string; units: string } => {
- let size = '';
- let units = '';
-
- const result = /(\d+)(\w+)/.exec(field);
- if (result) {
- size = result[1];
- units = result[2];
- }
-
- return {
- size,
- units,
- };
-};
-
-export const isNumber = (value: any): boolean => value !== '' && value !== null && isFinite(value);
-
-export const getPolicyByName = (
- policies: PolicyFromES[] | null | undefined,
- policyName: string = ''
-): PolicyFromES | undefined => {
- if (policies && policies.length > 0) {
- return policies.find((policy: PolicyFromES) => policy.name === policyName);
- }
-};
-
-export const initializeNewPolicy = (newPolicyName: string = ''): LegacyPolicy => {
- return {
- name: newPolicyName,
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- };
-};
-
-export const deserializePolicy = (policy: PolicyFromES): LegacyPolicy => {
- const {
- name,
- policy: { phases },
- } = policy;
-
- return {
- name,
- phases: {
- delete: deletePhaseFromES(phases.delete),
- },
- };
-};
-
-export const legacySerializePolicy = (
- policy: LegacyPolicy,
- originalEsPolicy: SerializedPolicy = {
- name: policy.name,
- phases: { hot: { ...serializedPhaseInitialization } },
- }
-): SerializedPolicy => {
- const serializedPolicy = {
- name: policy.name,
- phases: {},
- } as SerializedPolicy;
-
- if (policy.phases.delete.phaseEnabled) {
- serializedPolicy.phases.delete = deletePhaseToES(
- policy.phases.delete,
- originalEsPolicy.phases.delete
- );
- }
- return serializedPolicy;
-};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts
deleted file mode 100644
index 79c909c433f33..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts
+++ /dev/null
@@ -1,144 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { i18n } from '@kbn/i18n';
-import { DeletePhase, LegacyPolicy, PolicyFromES } from '../../../../common/types';
-import { validateDeletePhase } from './delete_phase';
-
-export const propertyof = (propertyName: keyof T & string) => propertyName;
-
-export const numberRequiredMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.numberRequiredError',
- {
- defaultMessage: 'A number is required.',
- }
-);
-
-// TODO validation includes 0 -> should be non-negative number?
-export const positiveNumberRequiredMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.positiveNumberRequiredError',
- {
- defaultMessage: 'Only positive numbers are allowed.',
- }
-);
-
-export const positiveNumbersAboveZeroErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.positiveNumberAboveZeroRequiredError',
- {
- defaultMessage: 'Only numbers above 0 are allowed.',
- }
-);
-
-export const policyNameRequiredMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.policyNameRequiredError',
- {
- defaultMessage: 'A policy name is required.',
- }
-);
-
-export const policyNameStartsWithUnderscoreErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.policyNameStartsWithUnderscoreError',
- {
- defaultMessage: 'A policy name cannot start with an underscore.',
- }
-);
-export const policyNameContainsCommaErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.policyNameContainsCommaError',
- {
- defaultMessage: 'A policy name cannot include a comma.',
- }
-);
-export const policyNameContainsSpaceErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.policyNameContainsSpaceError',
- {
- defaultMessage: 'A policy name cannot include a space.',
- }
-);
-
-export const policyNameTooLongErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.policyNameTooLongError',
- {
- defaultMessage: 'A policy name cannot be longer than 255 bytes.',
- }
-);
-export const policyNameMustBeDifferentErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.differentPolicyNameRequiredError',
- {
- defaultMessage: 'The policy name must be different.',
- }
-);
-export const policyNameAlreadyUsedErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError',
- {
- defaultMessage: 'That policy name is already used.',
- }
-);
-export type PhaseValidationErrors = {
- [P in keyof Partial]: string[];
-};
-
-export interface ValidationErrors {
- delete: PhaseValidationErrors;
- policyName: string[];
-}
-
-export const validatePolicy = (
- saveAsNew: boolean,
- policy: LegacyPolicy,
- policies: PolicyFromES[],
- originalPolicyName: string
-): [boolean, ValidationErrors] => {
- const policyNameErrors: string[] = [];
- if (!policy.name) {
- policyNameErrors.push(policyNameRequiredMessage);
- } else {
- if (policy.name.startsWith('_')) {
- policyNameErrors.push(policyNameStartsWithUnderscoreErrorMessage);
- }
- if (policy.name.includes(',')) {
- policyNameErrors.push(policyNameContainsCommaErrorMessage);
- }
- if (policy.name.includes(' ')) {
- policyNameErrors.push(policyNameContainsSpaceErrorMessage);
- }
- if (window.TextEncoder && new window.TextEncoder().encode(policy.name).length > 255) {
- policyNameErrors.push(policyNameTooLongErrorMessage);
- }
-
- if (saveAsNew && policy.name === originalPolicyName) {
- policyNameErrors.push(policyNameMustBeDifferentErrorMessage);
- } else if (policy.name !== originalPolicyName) {
- const policyNames = policies.map((existingPolicy) => existingPolicy.name);
- if (policyNames.includes(policy.name)) {
- policyNameErrors.push(policyNameAlreadyUsedErrorMessage);
- }
- }
- }
-
- const deletePhaseErrors = validateDeletePhase(policy.phases.delete);
- const isValid = policyNameErrors.length === 0 && Object.keys(deletePhaseErrors).length === 0;
- return [
- isValid,
- {
- policyName: [...policyNameErrors],
- delete: deletePhaseErrors,
- },
- ];
-};
-
-export const findFirstError = (errors?: ValidationErrors): string | undefined => {
- if (!errors) {
- return;
- }
-
- if (errors.policyName.length > 0) {
- return propertyof('policyName');
- }
-
- if (Object.keys(errors.delete).length > 0) {
- return `${propertyof('delete')}.${Object.keys(errors.delete)[0]}`;
- }
-};
diff --git a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts
index 023aeba57aa7a..a127574d5bad0 100644
--- a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts
@@ -18,6 +18,7 @@ export {
getFieldValidityAndErrorMessage,
useFormContext,
FormSchema,
+ ValidationConfig,
} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
export { fieldValidators } from '../../../../src/plugins/es_ui_shared/static/forms/helpers';
@@ -27,6 +28,8 @@ export {
NumericField,
SelectField,
SuperSelectField,
+ ComboBoxField,
+ TextField,
} from '../../../../src/plugins/es_ui_shared/static/forms/components';
export { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public';
diff --git a/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts b/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts
index 4c8c610794b2e..214bb16b24283 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts
+++ b/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { useEffect, useState, useReducer, useCallback } from 'react';
+import { useMountedState } from 'react-use';
import createContainer from 'constate';
import { pick, throttle } from 'lodash';
import { TimeKey, timeKeyIsBetween } from '../../../../common/time';
@@ -146,15 +147,20 @@ const useFetchEntriesEffect = (
props: LogEntriesProps
) => {
const { services } = useKibanaContextForPlugin();
+ const isMounted = useMountedState();
const [prevParams, cachePrevParams] = useState();
const [startedStreaming, setStartedStreaming] = useState(false);
+ const dispatchIfMounted = useCallback((action) => (isMounted() ? dispatch(action) : undefined), [
+ dispatch,
+ isMounted,
+ ]);
const runFetchNewEntriesRequest = async (overrides: Partial = {}) => {
if (!props.startTimestamp || !props.endTimestamp) {
return;
}
- dispatch({ type: Action.FetchingNewEntries });
+ dispatchIfMounted({ type: Action.FetchingNewEntries });
try {
const commonFetchArgs: LogEntriesBaseRequest = {
@@ -175,13 +181,15 @@ const useFetchEntriesEffect = (
};
const { data: payload } = await fetchLogEntries(fetchArgs, services.http.fetch);
- dispatch({ type: Action.ReceiveNewEntries, payload });
+ dispatchIfMounted({ type: Action.ReceiveNewEntries, payload });
// Move position to the bottom if it's the first load.
// Do it in the next tick to allow the `dispatch` to fire
if (!props.timeKey && payload.bottomCursor) {
setTimeout(() => {
- props.jumpToTargetPosition(payload.bottomCursor!);
+ if (isMounted()) {
+ props.jumpToTargetPosition(payload.bottomCursor!);
+ }
});
} else if (
props.timeKey &&
@@ -192,7 +200,7 @@ const useFetchEntriesEffect = (
props.jumpToTargetPosition(payload.topCursor);
}
} catch (e) {
- dispatch({ type: Action.ErrorOnNewEntries });
+ dispatchIfMounted({ type: Action.ErrorOnNewEntries });
}
};
@@ -210,7 +218,7 @@ const useFetchEntriesEffect = (
return;
}
- dispatch({ type: Action.FetchingMoreEntries });
+ dispatchIfMounted({ type: Action.FetchingMoreEntries });
try {
const commonFetchArgs: LogEntriesBaseRequest = {
@@ -232,14 +240,14 @@ const useFetchEntriesEffect = (
const { data: payload } = await fetchLogEntries(fetchArgs, services.http.fetch);
- dispatch({
+ dispatchIfMounted({
type: getEntriesBefore ? Action.ReceiveEntriesBefore : Action.ReceiveEntriesAfter,
payload,
});
return payload.bottomCursor;
} catch (e) {
- dispatch({ type: Action.ErrorOnMoreEntries });
+ dispatchIfMounted({ type: Action.ErrorOnMoreEntries });
}
};
@@ -322,7 +330,7 @@ const useFetchEntriesEffect = (
after: props.endTimestamp > prevParams.endTimestamp,
};
- dispatch({ type: Action.ExpandRange, payload: shouldExpand });
+ dispatchIfMounted({ type: Action.ExpandRange, payload: shouldExpand });
};
const expandRangeEffectDependencies = [
diff --git a/x-pack/plugins/infra/public/utils/use_tracked_promise.ts b/x-pack/plugins/infra/public/utils/use_tracked_promise.ts
index 9951b62fa64a3..42518127f68bf 100644
--- a/x-pack/plugins/infra/public/utils/use_tracked_promise.ts
+++ b/x-pack/plugins/infra/public/utils/use_tracked_promise.ts
@@ -6,13 +6,15 @@
/* eslint-disable max-classes-per-file */
-import { DependencyList, useEffect, useMemo, useRef, useState } from 'react';
+import { DependencyList, useEffect, useMemo, useRef, useState, useCallback } from 'react';
+import { useMountedState } from 'react-use';
interface UseTrackedPromiseArgs {
createPromise: (...args: Arguments) => Promise;
onResolve?: (result: Result) => void;
onReject?: (value: unknown) => void;
cancelPreviousOn?: 'creation' | 'settlement' | 'resolution' | 'rejection' | 'never';
+ triggerOrThrow?: 'always' | 'whenMounted';
}
/**
@@ -64,6 +66,16 @@ interface UseTrackedPromiseArgs {
* The last argument is a normal React hook dependency list that indicates
* under which conditions a new reference to the configuration object should be
* used.
+ *
+ * The `onResolve`, `onReject` and possible uncatched errors are only triggered
+ * if the underlying component is mounted. To ensure they always trigger (i.e.
+ * if the promise is called in a `useLayoutEffect`) use the `triggerOrThrow`
+ * attribute:
+ *
+ * 'whenMounted': (default) they are called only if the component is mounted.
+ *
+ * 'always': they always call. The consumer is then responsible of ensuring no
+ * side effects happen if the underlying component is not mounted.
*/
export const useTrackedPromise = (
{
@@ -71,9 +83,20 @@ export const useTrackedPromise = (
onResolve = noOp,
onReject = noOp,
cancelPreviousOn = 'never',
+ triggerOrThrow = 'whenMounted',
}: UseTrackedPromiseArgs,
dependencies: DependencyList
) => {
+ const isComponentMounted = useMountedState();
+ const shouldTriggerOrThrow = useCallback(() => {
+ switch (triggerOrThrow) {
+ case 'always':
+ return true;
+ case 'whenMounted':
+ return isComponentMounted();
+ }
+ }, [isComponentMounted, triggerOrThrow]);
+
/**
* If a promise is currently pending, this holds a reference to it and its
* cancellation function.
@@ -144,7 +167,7 @@ export const useTrackedPromise = (
(pendingPromise) => pendingPromise.promise !== newPendingPromise.promise
);
- if (onResolve) {
+ if (onResolve && shouldTriggerOrThrow()) {
onResolve(value);
}
@@ -173,11 +196,13 @@ export const useTrackedPromise = (
(pendingPromise) => pendingPromise.promise !== newPendingPromise.promise
);
- if (onReject) {
- onReject(value);
- }
+ if (shouldTriggerOrThrow()) {
+ if (onReject) {
+ onReject(value);
+ }
- throw value;
+ throw value;
+ }
}
),
};
diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts b/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts
index 91396bce359b0..e81207300a5f3 100644
--- a/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts
+++ b/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts
@@ -84,16 +84,14 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
{
type: 'foo',
enabled: true,
- streams: [
- { id: 'foo-foo', enabled: true, data_stream: { dataset: 'foo', type: 'logs' } },
- ],
+ streams: [{ enabled: true, data_stream: { dataset: 'foo', type: 'logs' } }],
},
{
type: 'bar',
enabled: true,
streams: [
- { id: 'bar-bar', enabled: true, data_stream: { dataset: 'bar', type: 'logs' } },
- { id: 'bar-bar2', enabled: true, data_stream: { dataset: 'bar2', type: 'logs' } },
+ { enabled: true, data_stream: { dataset: 'bar', type: 'logs' } },
+ { enabled: true, data_stream: { dataset: 'bar2', type: 'logs' } },
],
},
]);
@@ -142,7 +140,6 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
enabled: true,
streams: [
{
- id: 'foo-foo',
enabled: true,
data_stream: { dataset: 'foo', type: 'logs' },
vars: { 'var-name': { value: 'foo-var-value' } },
@@ -154,13 +151,11 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
enabled: true,
streams: [
{
- id: 'bar-bar',
enabled: true,
data_stream: { dataset: 'bar', type: 'logs' },
vars: { 'var-name': { type: 'text', value: 'bar-var-value' } },
},
{
- id: 'bar-bar2',
enabled: true,
data_stream: { dataset: 'bar2', type: 'logs' },
vars: { 'var-name': { type: 'yaml', value: 'bar2-var-value' } },
@@ -258,7 +253,6 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
},
streams: [
{
- id: 'foo-foo',
enabled: true,
data_stream: { dataset: 'foo', type: 'logs' },
vars: {
@@ -276,7 +270,6 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
},
streams: [
{
- id: 'bar-bar',
enabled: true,
data_stream: { dataset: 'bar', type: 'logs' },
vars: {
@@ -284,7 +277,6 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
},
},
{
- id: 'bar-bar2',
enabled: true,
data_stream: { dataset: 'bar2', type: 'logs' },
vars: {
@@ -298,7 +290,6 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
enabled: false,
streams: [
{
- id: 'with-disabled-streams-disabled',
enabled: false,
data_stream: { dataset: 'disabled', type: 'logs' },
vars: {
@@ -306,7 +297,6 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
},
},
{
- id: 'with-disabled-streams-disabled2',
enabled: false,
data_stream: { dataset: 'disabled2', type: 'logs' },
},
diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.ts b/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.ts
index 822747916ebc5..cbdfa25ed7f7e 100644
--- a/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.ts
+++ b/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.ts
@@ -8,11 +8,10 @@ import {
RegistryPolicyTemplate,
RegistryVarsEntry,
RegistryStream,
- PackagePolicy,
PackagePolicyConfigRecord,
PackagePolicyConfigRecordEntry,
- PackagePolicyInput,
- PackagePolicyInputStream,
+ NewPackagePolicyInput,
+ NewPackagePolicyInputStream,
NewPackagePolicy,
} from '../types';
@@ -42,8 +41,10 @@ const getStreamsForInputType = (
/*
* This service creates a package policy inputs definition from defaults provided in package info
*/
-export const packageToPackagePolicyInputs = (packageInfo: PackageInfo): PackagePolicy['inputs'] => {
- const inputs: PackagePolicy['inputs'] = [];
+export const packageToPackagePolicyInputs = (
+ packageInfo: PackageInfo
+): NewPackagePolicy['inputs'] => {
+ const inputs: NewPackagePolicy['inputs'] = [];
// Assume package will only ever ship one package policy template for now
const packagePolicyTemplate: RegistryPolicyTemplate | null =
@@ -71,12 +72,11 @@ export const packageToPackagePolicyInputs = (packageInfo: PackageInfo): PackageP
};
// Map each package input stream into package policy input stream
- const streams: PackagePolicyInputStream[] = getStreamsForInputType(
+ const streams: NewPackagePolicyInputStream[] = getStreamsForInputType(
packageInput.type,
packageInfo
).map((packageStream) => {
- const stream: PackagePolicyInputStream = {
- id: `${packageInput.type}-${packageStream.data_stream.dataset}`,
+ const stream: NewPackagePolicyInputStream = {
enabled: packageStream.enabled === false ? false : true,
data_stream: packageStream.data_stream,
};
@@ -86,7 +86,7 @@ export const packageToPackagePolicyInputs = (packageInfo: PackageInfo): PackageP
return stream;
});
- const input: PackagePolicyInput = {
+ const input: NewPackagePolicyInput = {
type: packageInput.type,
enabled: streams.length ? !!streams.find((stream) => stream.enabled) : true,
streams,
diff --git a/x-pack/plugins/ingest_manager/common/types/models/package_policy.ts b/x-pack/plugins/ingest_manager/common/types/models/package_policy.ts
index 724dbae5dac85..ae16899a4b6f9 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/package_policy.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/package_policy.ts
@@ -18,7 +18,6 @@ export interface PackagePolicyConfigRecordEntry {
export type PackagePolicyConfigRecord = Record;
export interface NewPackagePolicyInputStream {
- id: string;
enabled: boolean;
data_stream: {
dataset: string;
@@ -29,6 +28,7 @@ export interface NewPackagePolicyInputStream {
}
export interface PackagePolicyInputStream extends NewPackagePolicyInputStream {
+ id: string;
compiled_stream?: any;
}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_config.tsx
index 175bfb1469902..177354dad14dc 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_config.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_config.tsx
@@ -14,7 +14,7 @@ import {
EuiSpacer,
EuiButtonEmpty,
} from '@elastic/eui';
-import { PackagePolicyInput, RegistryVarsEntry } from '../../../../types';
+import { NewPackagePolicyInput, RegistryVarsEntry } from '../../../../types';
import {
isAdvancedVar,
PackagePolicyConfigValidationResults,
@@ -28,8 +28,8 @@ const FlexItemWithMaxWidth = styled(EuiFlexItem)`
export const PackagePolicyInputConfig: React.FunctionComponent<{
packageInputVars?: RegistryVarsEntry[];
- packagePolicyInput: PackagePolicyInput;
- updatePackagePolicyInput: (updatedInput: Partial) => void;
+ packagePolicyInput: NewPackagePolicyInput;
+ updatePackagePolicyInput: (updatedInput: Partial) => void;
inputVarsValidationResults: PackagePolicyConfigValidationResults;
forceShowErrors?: boolean;
}> = memo(
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_panel.tsx
index 1e43cc0d5938e..79ff0cc29850c 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_panel.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_panel.tsx
@@ -17,7 +17,7 @@ import {
EuiSpacer,
} from '@elastic/eui';
import {
- PackagePolicyInput,
+ NewPackagePolicyInput,
PackagePolicyInputStream,
RegistryInput,
RegistryStream,
@@ -40,7 +40,7 @@ const ShortenedHorizontalRule = styled(EuiHorizontalRule)`
const shouldShowStreamsByDefault = (
packageInput: RegistryInput,
packageInputStreams: Array,
- packagePolicyInput: PackagePolicyInput
+ packagePolicyInput: NewPackagePolicyInput
): boolean => {
return (
packagePolicyInput.enabled &&
@@ -63,8 +63,8 @@ const shouldShowStreamsByDefault = (
export const PackagePolicyInputPanel: React.FunctionComponent<{
packageInput: RegistryInput;
packageInputStreams: Array;
- packagePolicyInput: PackagePolicyInput;
- updatePackagePolicyInput: (updatedInput: Partial) => void;
+ packagePolicyInput: NewPackagePolicyInput;
+ updatePackagePolicyInput: (updatedInput: Partial) => void;
inputValidationResults: PackagePolicyInputValidationResults;
forceShowErrors?: boolean;
}> = memo(
@@ -210,7 +210,7 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{
...updatedStream,
};
- const updatedInput: Partial = {
+ const updatedInput: Partial = {
streams: newStreams,
};
@@ -227,7 +227,7 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{
updatePackagePolicyInput(updatedInput);
}}
inputStreamValidationResults={
- inputValidationResults.streams![packagePolicyInputStream!.id]
+ inputValidationResults.streams![packagePolicyInputStream!.data_stream!.dataset]
}
forceShowErrors={forceShowErrors}
/>
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx
index 3d33edd468151..963d0da50ce7f 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx
@@ -16,7 +16,7 @@ import {
EuiSpacer,
EuiButtonEmpty,
} from '@elastic/eui';
-import { PackagePolicyInputStream, RegistryStream, RegistryVarsEntry } from '../../../../types';
+import { NewPackagePolicyInputStream, RegistryStream, RegistryVarsEntry } from '../../../../types';
import {
isAdvancedVar,
PackagePolicyConfigValidationResults,
@@ -30,8 +30,8 @@ const FlexItemWithMaxWidth = styled(EuiFlexItem)`
export const PackagePolicyInputStreamConfig: React.FunctionComponent<{
packageInputStream: RegistryStream;
- packagePolicyInputStream: PackagePolicyInputStream;
- updatePackagePolicyInputStream: (updatedStream: Partial) => void;
+ packagePolicyInputStream: NewPackagePolicyInputStream;
+ updatePackagePolicyInputStream: (updatedStream: Partial) => void;
inputStreamValidationResults: PackagePolicyConfigValidationResults;
forceShowErrors?: boolean;
}> = memo(
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test..ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test.ts
similarity index 91%
rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test..ts
rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test.ts
index 9022e312ece79..8d46fed1ff14e 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test..ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test.ts
@@ -154,7 +154,6 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
},
streams: [
{
- id: 'foo-foo',
data_stream: { dataset: 'foo', type: 'logs' },
enabled: true,
vars: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } },
@@ -170,13 +169,11 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
},
streams: [
{
- id: 'bar-bar',
data_stream: { dataset: 'bar', type: 'logs' },
enabled: true,
vars: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } },
},
{
- id: 'bar-bar2',
data_stream: { dataset: 'bar2', type: 'logs' },
enabled: true,
vars: { 'var-name': { value: undefined, type: 'text' } },
@@ -193,13 +190,11 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
enabled: true,
streams: [
{
- id: 'with-disabled-streams-disabled',
data_stream: { dataset: 'disabled', type: 'logs' },
enabled: false,
vars: { 'var-name': { value: undefined, type: 'text' } },
},
{
- id: 'with-disabled-streams-disabled-without-vars',
data_stream: { dataset: 'disabled2', type: 'logs' },
enabled: false,
},
@@ -213,8 +208,7 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
},
streams: [
{
- id: 'with-no-stream-vars-bar',
- data_stream: { dataset: 'bar', type: 'logs' },
+ data_stream: { dataset: 'with-no-stream-vars-bar', type: 'logs' },
enabled: true,
},
],
@@ -236,7 +230,6 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
},
streams: [
{
- id: 'foo-foo',
data_stream: { dataset: 'foo', type: 'logs' },
enabled: true,
vars: { 'var-name': { value: 'invalidyaml: test\n foo bar:', type: 'yaml' } },
@@ -252,13 +245,11 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
},
streams: [
{
- id: 'bar-bar',
data_stream: { dataset: 'bar', type: 'logs' },
enabled: true,
vars: { 'var-name': { value: ' \n\n', type: 'yaml' } },
},
{
- id: 'bar-bar2',
data_stream: { dataset: 'bar2', type: 'logs' },
enabled: true,
vars: { 'var-name': { value: undefined, type: 'text' } },
@@ -275,7 +266,6 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
enabled: true,
streams: [
{
- id: 'with-disabled-streams-disabled',
data_stream: { dataset: 'disabled', type: 'logs' },
enabled: false,
vars: {
@@ -286,7 +276,6 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
},
},
{
- id: 'with-disabled-streams-disabled-without-vars',
data_stream: { dataset: 'disabled2', type: 'logs' },
enabled: false,
},
@@ -300,8 +289,7 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
},
streams: [
{
- id: 'with-no-stream-vars-bar',
- data_stream: { dataset: 'bar', type: 'logs' },
+ data_stream: { dataset: 'with-no-stream-vars-bar', type: 'logs' },
enabled: true,
},
],
@@ -320,21 +308,21 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
'foo-input2-var-name': null,
'foo-input3-var-name': null,
},
- streams: { 'foo-foo': { vars: { 'var-name': null } } },
+ streams: { foo: { vars: { 'var-name': null } } },
},
bar: {
vars: { 'bar-input-var-name': null, 'bar-input2-var-name': null },
streams: {
- 'bar-bar': { vars: { 'var-name': null } },
- 'bar-bar2': { vars: { 'var-name': null } },
+ bar: { vars: { 'var-name': null } },
+ bar2: { vars: { 'var-name': null } },
},
},
'with-disabled-streams': {
streams: {
- 'with-disabled-streams-disabled': {
+ disabled: {
vars: { 'var-name': null },
},
- 'with-disabled-streams-disabled-without-vars': {},
+ disabled2: {},
},
},
'with-no-stream-vars': {
@@ -364,7 +352,7 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
'foo-input2-var-name': ['foo-input2-var-name is required'],
'foo-input3-var-name': ['foo-input3-var-name is required'],
},
- streams: { 'foo-foo': { vars: { 'var-name': ['Invalid YAML format'] } } },
+ streams: { foo: { vars: { 'var-name': ['Invalid YAML format'] } } },
},
bar: {
vars: {
@@ -372,14 +360,14 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
'bar-input2-var-name': ['bar-input2-var-name is required'],
},
streams: {
- 'bar-bar': { vars: { 'var-name': ['var-name is required'] } },
- 'bar-bar2': { vars: { 'var-name': null } },
+ bar: { vars: { 'var-name': ['var-name is required'] } },
+ bar2: { vars: { 'var-name': null } },
},
},
'with-disabled-streams': {
streams: {
- 'with-disabled-streams-disabled': { vars: { 'var-name': null } },
- 'with-disabled-streams-disabled-without-vars': {},
+ disabled: { vars: { 'var-name': null } },
+ disabled2: {},
},
},
'with-no-stream-vars': {
@@ -427,7 +415,7 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
'foo-input2-var-name': ['foo-input2-var-name is required'],
'foo-input3-var-name': ['foo-input3-var-name is required'],
},
- streams: { 'foo-foo': { vars: { 'var-name': null } } },
+ streams: { foo: { vars: { 'var-name': null } } },
},
bar: {
vars: {
@@ -435,16 +423,16 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
'bar-input2-var-name': ['bar-input2-var-name is required'],
},
streams: {
- 'bar-bar': { vars: { 'var-name': null } },
- 'bar-bar2': { vars: { 'var-name': null } },
+ bar: { vars: { 'var-name': null } },
+ bar2: { vars: { 'var-name': null } },
},
},
'with-disabled-streams': {
streams: {
- 'with-disabled-streams-disabled': {
+ disabled: {
vars: { 'var-name': null },
},
- 'with-disabled-streams-disabled-without-vars': {},
+ disabled2: {},
},
},
'with-no-stream-vars': {
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts
index 9ce73c0690ccb..1126cd7e58e18 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts
@@ -151,7 +151,7 @@ export const validatePackagePolicy = (
);
}
- inputValidationResults.streams![stream.id] = streamValidationResults;
+ inputValidationResults.streams![stream.data_stream.dataset] = streamValidationResults;
});
} else {
delete inputValidationResults.streams;
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_configure_package.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_configure_package.tsx
index d3d5e60c34e58..b335ff439684b 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_configure_package.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_configure_package.tsx
@@ -5,7 +5,12 @@
*/
import React from 'react';
import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { PackageInfo, RegistryStream, NewPackagePolicy, PackagePolicyInput } from '../../../types';
+import {
+ PackageInfo,
+ RegistryStream,
+ NewPackagePolicy,
+ NewPackagePolicyInput,
+} from '../../../types';
import { Loading } from '../../../components';
import { PackagePolicyValidationResults } from './services';
import { PackagePolicyInputPanel, CustomPackagePolicy } from './components';
@@ -71,7 +76,7 @@ export const StepConfigurePackagePolicy: React.FunctionComponent<{
packageInput={packageInput}
packageInputStreams={packageInputStreams}
packagePolicyInput={packagePolicyInput}
- updatePackagePolicyInput={(updatedInput: Partial) => {
+ updatePackagePolicyInput={(updatedInput: Partial) => {
const indexOfUpdatedInput = packagePolicy.inputs.findIndex(
(input) => input.type === packageInput.type
);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts
index 386ffa5649cc2..1cf8077aeda40 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts
@@ -17,7 +17,9 @@ export {
NewPackagePolicy,
UpdatePackagePolicy,
PackagePolicyInput,
+ NewPackagePolicyInput,
PackagePolicyInputStream,
+ NewPackagePolicyInputStream,
PackagePolicyConfigRecord,
PackagePolicyConfigRecordEntry,
Output,
diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
index 198a54ca84125..1d221b8b1eead 100644
--- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
@@ -35,8 +35,7 @@ import {
getPackageInfo,
handleInstallPackageFailure,
isBulkInstallError,
- installPackageFromRegistry,
- installPackageByUpload,
+ installPackage,
removeInstallation,
getLimitedPackages,
getInstallationObject,
@@ -149,7 +148,8 @@ export const installPackageFromRegistryHandler: RequestHandler<
const { pkgName, pkgVersion } = splitPkgKey(pkgkey);
const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName });
try {
- const res = await installPackageFromRegistry({
+ const res = await installPackage({
+ installSource: 'registry',
savedObjectsClient,
pkgkey,
callCluster,
@@ -224,7 +224,8 @@ export const installPackageByUploadHandler: RequestHandler<
const contentType = request.headers['content-type'] as string; // from types it could also be string[] or undefined but this is checked later
const archiveBuffer = Buffer.from(request.body);
try {
- const res = await installPackageByUpload({
+ const res = await installPackage({
+ installSource: 'upload',
savedObjectsClient,
callCluster,
archiveBuffer,
diff --git a/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.test.ts
index 44c2ccda3bd2a..f47b8499a1b69 100644
--- a/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.test.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.test.ts
@@ -28,6 +28,13 @@ jest.mock('../../services/package_policy', (): {
create: jest.fn((soClient, callCluster, newData) =>
Promise.resolve({
...newData,
+ inputs: newData.inputs.map((input) => ({
+ ...input,
+ streams: input.streams.map((stream) => ({
+ id: stream.data_stream.dataset,
+ ...stream,
+ })),
+ })),
id: '1',
revision: 1,
updated_at: new Date().toISOString(),
diff --git a/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.ts
index d9baeca4deb47..3a2b9ba7a744f 100644
--- a/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.ts
@@ -7,7 +7,6 @@ import { TypeOf } from '@kbn/config-schema';
import Boom from '@hapi/boom';
import { RequestHandler, SavedObjectsErrorHelpers } from '../../../../../../src/core/server';
import { appContextService, packagePolicyService } from '../../services';
-import { getPackageInfo } from '../../services/epm/packages';
import {
GetPackagePoliciesRequestSchema,
GetOnePackagePolicyRequestSchema,
@@ -134,21 +133,11 @@ export const updatePackagePolicyHandler: RequestHandler<
const newData = { ...request.body };
const pkg = newData.package || packagePolicy.package;
const inputs = newData.inputs || packagePolicy.inputs;
- if (pkg && (newData.inputs || newData.package)) {
- const pkgInfo = await getPackageInfo({
- savedObjectsClient: soClient,
- pkgName: pkg.name,
- pkgVersion: pkg.version,
- });
- newData.inputs = (await packagePolicyService.assignPackageStream(pkgInfo, inputs)) as TypeOf<
- typeof CreatePackagePolicyRequestSchema.body
- >['inputs'];
- }
const updatedPackagePolicy = await packagePolicyService.update(
soClient,
request.params.packagePolicyId,
- newData,
+ { ...newData, package: pkg, inputs },
{ user }
);
return response.ok({
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts b/x-pack/plugins/ingest_manager/server/services/epm/archive/cache.ts
similarity index 95%
rename from x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts
rename to x-pack/plugins/ingest_manager/server/services/epm/archive/cache.ts
index 695db9db73fa2..102324c18bd43 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/archive/cache.ts
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { pkgToPkgKey } from './index';
+import { pkgToPkgKey } from '../registry/index';
const cache: Map = new Map();
export const cacheGet = (key: string) => cache.get(key);
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/archive/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/archive/index.ts
index ee505b205fc84..27451ed6b5e60 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/archive/index.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/archive/index.ts
@@ -6,10 +6,18 @@
import { ArchivePackage } from '../../../../common/types';
import { PackageInvalidArchiveError, PackageUnsupportedMediaTypeError } from '../../../errors';
-import { cacheSet, setArchiveFilelist } from '../registry/cache';
+import {
+ cacheSet,
+ cacheDelete,
+ getArchiveFilelist,
+ setArchiveFilelist,
+ deleteArchiveFilelist,
+} from './cache';
import { ArchiveEntry, getBufferExtractor } from '../registry/extract';
import { parseAndVerifyArchive } from './validation';
+export * from './cache';
+
export async function loadArchivePackage({
archiveBuffer,
contentType,
@@ -64,3 +72,15 @@ export async function unpackArchiveToCache(
}
return paths;
}
+
+export const deletePackageCache = (name: string, version: string) => {
+ // get cached archive filelist
+ const paths = getArchiveFilelist(name, version);
+
+ // delete cached archive filelist
+ deleteArchiveFilelist(name, version);
+
+ // delete cached archive files
+ // this has been populated in unpackArchiveToCache()
+ paths?.forEach((path) => cacheDelete(path));
+};
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/archive/validation.ts b/x-pack/plugins/ingest_manager/server/services/epm/archive/validation.ts
index e83340124a2d0..90941aaf80cdd 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/archive/validation.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/archive/validation.ts
@@ -16,7 +16,7 @@ import {
} from '../../../../common/types';
import { PackageInvalidArchiveError } from '../../../errors';
import { pkgToPkgKey } from '../registry';
-import { cacheGet } from '../registry/cache';
+import { cacheGet } from './cache';
// TODO: everything below performs verification of manifest.yml files, and hence duplicates functionality already implemented in the
// package registry. At some point this should probably be replaced (or enhanced) with verification based on
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.test.ts
index 5d3e8e9ce87d1..b7650d10b6b25 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.test.ts
@@ -61,11 +61,7 @@ describe('_installPackage', () => {
const installationPromise = _installPackage({
savedObjectsClient: soClient,
callCluster,
- pkgName: 'abc',
- pkgVersion: '1.2.3',
paths: [],
- removable: false,
- internal: false,
packageInfo: {
name: 'xyz',
version: '4.5.6',
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.ts
index f570984cc61aa..a83d9428b7c93 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.ts
@@ -21,6 +21,7 @@ import { installPipelines, deletePreviousPipelines } from '../elasticsearch/inge
import { installILMPolicy } from '../elasticsearch/ilm/install';
import { installKibanaAssets, getKibanaAssets } from '../kibana/assets/install';
import { updateCurrentWriteIndices } from '../elasticsearch/template/template';
+import { isRequiredPackage } from './index';
import { deleteKibanaSavedObjectsAssets } from './remove';
import { installTransform } from '../elasticsearch/transform/install';
import { createInstallation, saveKibanaAssetsRefs, updateVersion } from './install';
@@ -32,28 +33,22 @@ import { createInstallation, saveKibanaAssetsRefs, updateVersion } from './insta
export async function _installPackage({
savedObjectsClient,
callCluster,
- pkgName,
- pkgVersion,
installedPkg,
paths,
- removable,
- internal,
packageInfo,
installType,
installSource,
}: {
savedObjectsClient: SavedObjectsClientContract;
callCluster: CallESAsCurrentUser;
- pkgName: string;
- pkgVersion: string;
installedPkg?: SavedObject;
paths: string[];
- removable: boolean;
- internal: boolean;
packageInfo: InstallablePackage;
installType: InstallType;
installSource: InstallSource;
}): Promise {
+ const { internal = false, name: pkgName, version: pkgVersion } = packageInfo;
+ const removable = !isRequiredPackage(pkgName);
const toSaveESIndexPatterns = generateESIndexPatterns(packageInfo.data_streams);
// add the package installation to the saved object.
// if some installation already exists, just update install info
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts
index eb43bef72db70..ab93a73a55f39 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts
@@ -6,9 +6,9 @@
import { InstallablePackage } from '../../../types';
import { getAssets } from './assets';
-import { getArchiveFilelist } from '../registry/cache';
+import { getArchiveFilelist } from '../archive/cache';
-jest.mock('../registry/cache', () => {
+jest.mock('../archive/cache', () => {
return {
getArchiveFilelist: jest.fn(),
};
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts
index 856f04c0c9b67..2e2090312c9ae 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts
@@ -6,7 +6,7 @@
import { InstallablePackage } from '../../../types';
import * as Registry from '../registry';
-import { getArchiveFilelist } from '../registry/cache';
+import { getArchiveFilelist } from '../archive/cache';
// paths from RegistryPackage are routes to the assets on EPR
// e.g. `/package/nginx/1.2.0/data_stream/access/fields/fields.yml`
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts
index 2021b353f1a27..893df1733c58b 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts
@@ -116,7 +116,7 @@ export async function getPackageInfo(options: {
] = await Promise.all([
getInstallationObject({ savedObjectsClient, pkgName }),
Registry.fetchFindLatestPackage(pkgName),
- Registry.loadRegistryPackage(pkgName, pkgVersion),
+ Registry.getRegistryPackage(pkgName, pkgVersion),
]);
// add properties that aren't (or aren't yet) on Registry response
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts
index 410a9c0b22537..a1128011d81e6 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts
@@ -29,8 +29,7 @@ export {
BulkInstallResponse,
IBulkInstallPackageError,
handleInstallPackageFailure,
- installPackageFromRegistry,
- installPackageByUpload,
+ installPackage,
ensureInstalledPackage,
} from './install';
export { removeInstallation } from './remove';
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
index 0496a6e9aeef1..00a5c689e906d 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
@@ -24,7 +24,6 @@ import * as Registry from '../registry';
import {
getInstallation,
getInstallationObject,
- isRequiredPackage,
bulkInstallPackages,
isBulkInstallError,
} from './index';
@@ -52,7 +51,7 @@ export async function installLatestPackage(options: {
name: latestPackage.name,
version: latestPackage.version,
});
- return installPackageFromRegistry({ savedObjectsClient, pkgkey, callCluster });
+ return installPackage({ installSource: 'registry', savedObjectsClient, pkgkey, callCluster });
} catch (err) {
throw err;
}
@@ -148,7 +147,8 @@ export async function handleInstallPackageFailure({
}
const prevVersion = `${pkgName}-${installedPkg.attributes.version}`;
logger.error(`rolling back to ${prevVersion} after error installing ${pkgkey}`);
- await installPackageFromRegistry({
+ await installPackage({
+ installSource: 'registry',
savedObjectsClient,
pkgkey: prevVersion,
callCluster,
@@ -186,7 +186,12 @@ export async function upgradePackage({
});
try {
- const assets = await installPackageFromRegistry({ savedObjectsClient, pkgkey, callCluster });
+ const assets = await installPackage({
+ installSource: 'registry',
+ savedObjectsClient,
+ pkgkey,
+ callCluster,
+ });
return {
name: pkgToUpgrade,
newVersion: latestPkg.version,
@@ -218,19 +223,19 @@ export async function upgradePackage({
}
}
-interface InstallPackageParams {
+interface InstallRegistryPackageParams {
savedObjectsClient: SavedObjectsClientContract;
pkgkey: string;
callCluster: CallESAsCurrentUser;
force?: boolean;
}
-export async function installPackageFromRegistry({
+async function installPackageFromRegistry({
savedObjectsClient,
pkgkey,
callCluster,
force = false,
-}: InstallPackageParams): Promise {
+}: InstallRegistryPackageParams): Promise {
// TODO: change epm API to /packageName/version so we don't need to do this
const { pkgName, pkgVersion } = Registry.splitPkgKey(pkgkey);
// TODO: calls to getInstallationObject, Registry.fetchInfo, and Registry.fetchFindLatestPackge
@@ -248,39 +253,38 @@ export async function installPackageFromRegistry({
throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`);
}
- const { paths, registryPackageInfo } = await Registry.loadRegistryPackage(pkgName, pkgVersion);
-
- const removable = !isRequiredPackage(pkgName);
- const { internal = false } = registryPackageInfo;
- const installSource = 'registry';
+ const { paths, registryPackageInfo } = await Registry.getRegistryPackage(pkgName, pkgVersion);
return _installPackage({
savedObjectsClient,
callCluster,
- pkgName,
- pkgVersion,
installedPkg,
paths,
- removable,
- internal,
packageInfo: registryPackageInfo,
installType,
- installSource,
+ installSource: 'registry',
});
}
-export async function installPackageByUpload({
- savedObjectsClient,
- callCluster,
- archiveBuffer,
- contentType,
-}: {
+interface InstallUploadedArchiveParams {
savedObjectsClient: SavedObjectsClientContract;
callCluster: CallESAsCurrentUser;
archiveBuffer: Buffer;
contentType: string;
-}): Promise {
+}
+
+export type InstallPackageParams =
+ | ({ installSource: Extract } & InstallRegistryPackageParams)
+ | ({ installSource: Extract } & InstallUploadedArchiveParams);
+
+async function installPackageByUpload({
+ savedObjectsClient,
+ callCluster,
+ archiveBuffer,
+ contentType,
+}: InstallUploadedArchiveParams): Promise {
const { paths, archivePackageInfo } = await loadArchivePackage({ archiveBuffer, contentType });
+
const installedPkg = await getInstallationObject({
savedObjectsClient,
pkgName: archivePackageInfo.name,
@@ -292,25 +296,45 @@ export async function installPackageByUpload({
);
}
- const removable = !isRequiredPackage(archivePackageInfo.name);
- const { internal = false } = archivePackageInfo;
- const installSource = 'upload';
-
return _installPackage({
savedObjectsClient,
callCluster,
- pkgName: archivePackageInfo.name,
- pkgVersion: archivePackageInfo.version,
installedPkg,
paths,
- removable,
- internal,
packageInfo: archivePackageInfo,
installType,
- installSource,
+ installSource: 'upload',
});
}
+export async function installPackage(args: InstallPackageParams) {
+ if (!('installSource' in args)) {
+ throw new Error('installSource is required');
+ }
+
+ if (args.installSource === 'registry') {
+ const { savedObjectsClient, pkgkey, callCluster, force } = args;
+
+ return installPackageFromRegistry({
+ savedObjectsClient,
+ pkgkey,
+ callCluster,
+ force,
+ });
+ } else if (args.installSource === 'upload') {
+ const { savedObjectsClient, callCluster, archiveBuffer, contentType } = args;
+
+ return installPackageByUpload({
+ savedObjectsClient,
+ callCluster,
+ archiveBuffer,
+ contentType,
+ });
+ }
+ // @ts-expect-error s/b impossibe b/c `never` by this point, but just in case
+ throw new Error(`Unknown installSource: ${args.installSource}`);
+}
+
export const updateVersion = async (
savedObjectsClient: SavedObjectsClientContract,
pkgName: string,
@@ -421,7 +445,9 @@ export async function ensurePackagesCompletedInstall(
const pkgkey = `${pkg.attributes.name}-${pkg.attributes.install_version}`;
// reinstall package
if (elapsedTime > MAX_TIME_COMPLETE_INSTALL) {
- acc.push(installPackageFromRegistry({ savedObjectsClient, pkgkey, callCluster }));
+ acc.push(
+ installPackage({ installSource: 'registry', savedObjectsClient, pkgkey, callCluster })
+ );
}
return acc;
}, []);
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
index 5db47adc983c2..9fabbaf72474e 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
@@ -21,7 +21,8 @@ import { deletePipeline } from '../elasticsearch/ingest_pipeline/';
import { installIndexPatterns } from '../kibana/index_pattern/install';
import { deleteTransforms } from '../elasticsearch/transform/remove';
import { packagePolicyService, appContextService } from '../..';
-import { splitPkgKey, deletePackageCache } from '../registry';
+import { splitPkgKey } from '../registry';
+import { deletePackageCache } from '../archive';
export async function removeInstallation(options: {
savedObjectsClient: SavedObjectsClientContract;
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts
index b1dd9a8c3c3f1..52a1894570b2a 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts
@@ -19,14 +19,7 @@ import {
RegistrySearchResult,
} from '../../../types';
import { unpackArchiveToCache } from '../archive';
-import {
- cacheGet,
- cacheDelete,
- getArchiveFilelist,
- setArchiveFilelist,
- deleteArchiveFilelist,
-} from './cache';
-import { ArchiveEntry } from './extract';
+import { cacheGet, getArchiveFilelist, setArchiveFilelist } from '../archive';
import { fetchUrl, getResponse, getResponseStream } from './requests';
import { streamToBuffer } from './streams';
import { getRegistryUrl } from './registry_url';
@@ -132,27 +125,18 @@ export async function fetchCategories(params?: CategoriesParams): Promise true
-): Promise {
- const { archiveBuffer, archivePath } = await fetchArchiveBuffer(pkgName, pkgVersion);
- const contentType = mime.lookup(archivePath);
- if (!contentType) {
- throw new Error(`Unknown compression format for '${archivePath}'. Please use .zip or .gz`);
- }
- const paths: string[] = await unpackArchiveToCache(archiveBuffer, contentType);
- return paths;
-}
-
-export async function loadRegistryPackage(
+export async function getRegistryPackage(
pkgName: string,
pkgVersion: string
): Promise<{ paths: string[]; registryPackageInfo: RegistryPackage }> {
let paths = getArchiveFilelist(pkgName, pkgVersion);
if (!paths || paths.length === 0) {
- paths = await unpackRegistryPackageToCache(pkgName, pkgVersion);
+ const { archiveBuffer, archivePath } = await fetchArchiveBuffer(pkgName, pkgVersion);
+ const contentType = mime.lookup(archivePath);
+ if (!contentType) {
+ throw new Error(`Unknown compression format for '${archivePath}'. Please use .zip or .gz`);
+ }
+ paths = await unpackArchiveToCache(archiveBuffer, contentType);
setArchiveFilelist(pkgName, pkgVersion, paths);
}
@@ -200,7 +184,7 @@ export async function ensureCachedArchiveInfo(
const paths = getArchiveFilelist(name, version);
if (!paths || paths.length === 0) {
if (installSource === 'registry') {
- await loadRegistryPackage(name, version);
+ await getRegistryPackage(name, version);
} else {
throw new PackageCacheError(
`Package ${name}-${version} not cached. If it was uploaded, try uninstalling and reinstalling manually.`
@@ -247,15 +231,3 @@ export function groupPathsByService(paths: string[]): AssetsGroupedByServiceByTy
// elasticsearch: assets.elasticsearch,
};
}
-
-export const deletePackageCache = (name: string, version: string) => {
- // get cached archive filelist
- const paths = getArchiveFilelist(name, version);
-
- // delete cached archive filelist
- deleteArchiveFilelist(name, version);
-
- // delete cached archive files
- // this has been populated in unpackRegistryPackageToCache()
- paths?.forEach((path) => cacheDelete(path));
-};
diff --git a/x-pack/plugins/ingest_manager/server/services/package_policy.test.ts b/x-pack/plugins/ingest_manager/server/services/package_policy.test.ts
index 6064e5bae0634..6ae76c56436d5 100644
--- a/x-pack/plugins/ingest_manager/server/services/package_policy.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/package_policy.test.ts
@@ -34,6 +34,12 @@ jest.mock('./epm/packages/assets', () => {
};
});
+jest.mock('./epm/packages', () => {
+ return {
+ getPackageInfo: () => ({}),
+ };
+});
+
jest.mock('./epm/registry', () => {
return {
fetchInfo: () => ({}),
diff --git a/x-pack/plugins/ingest_manager/server/services/package_policy.ts b/x-pack/plugins/ingest_manager/server/services/package_policy.ts
index dc3a4495191c9..0f78c97a6f2bd 100644
--- a/x-pack/plugins/ingest_manager/server/services/package_policy.ts
+++ b/x-pack/plugins/ingest_manager/server/services/package_policy.ts
@@ -4,10 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObjectsClientContract } from 'src/core/server';
+import uuid from 'uuid';
import { AuthenticatedUser } from '../../../security/server';
import {
DeletePackagePoliciesResponse,
PackagePolicyInput,
+ NewPackagePolicyInput,
PackagePolicyInputStream,
PackageInfo,
ListWithKuery,
@@ -58,6 +60,11 @@ class PackagePolicyService {
throw new Error('There is already a package with the same name on this agent policy');
}
}
+ // Add ids to stream
+ const packagePolicyId = options?.id || uuid.v4();
+ let inputs: PackagePolicyInput[] = packagePolicy.inputs.map((input) =>
+ assignStreamIdToInput(packagePolicyId, input)
+ );
// Make sure the associated package is installed
if (packagePolicy.package?.name) {
@@ -85,7 +92,7 @@ class PackagePolicyService {
}
}
- packagePolicy.inputs = await this.assignPackageStream(pkgInfo, packagePolicy.inputs);
+ inputs = await this.assignPackageStream(pkgInfo, inputs);
}
const isoDate = new Date().toISOString();
@@ -93,13 +100,15 @@ class PackagePolicyService {
SAVED_OBJECT_TYPE,
{
...packagePolicy,
+ inputs,
revision: 1,
created_at: isoDate,
created_by: options?.user?.username ?? 'system',
updated_at: isoDate,
updated_by: options?.user?.username ?? 'system',
},
- options
+
+ { ...options, id: packagePolicyId }
);
// Assign it to the given agent policy
@@ -124,18 +133,28 @@ class PackagePolicyService {
const isoDate = new Date().toISOString();
// eslint-disable-next-line @typescript-eslint/naming-convention
const { saved_objects } = await soClient.bulkCreate(
- packagePolicies.map((packagePolicy) => ({
- type: SAVED_OBJECT_TYPE,
- attributes: {
- ...packagePolicy,
- policy_id: agentPolicyId,
- revision: 1,
- created_at: isoDate,
- created_by: options?.user?.username ?? 'system',
- updated_at: isoDate,
- updated_by: options?.user?.username ?? 'system',
- },
- }))
+ packagePolicies.map((packagePolicy) => {
+ const packagePolicyId = uuid.v4();
+
+ const inputs = packagePolicy.inputs.map((input) =>
+ assignStreamIdToInput(packagePolicyId, input)
+ );
+
+ return {
+ type: SAVED_OBJECT_TYPE,
+ id: packagePolicyId,
+ attributes: {
+ ...packagePolicy,
+ inputs,
+ policy_id: agentPolicyId,
+ revision: 1,
+ created_at: isoDate,
+ created_by: options?.user?.username ?? 'system',
+ updated_at: isoDate,
+ updated_by: options?.user?.username ?? 'system',
+ },
+ };
+ })
);
// Filter out invalid SOs
@@ -255,11 +274,26 @@ class PackagePolicyService {
}
}
+ let inputs = await restOfPackagePolicy.inputs.map((input) =>
+ assignStreamIdToInput(oldPackagePolicy.id, input)
+ );
+
+ if (packagePolicy.package?.name) {
+ const pkgInfo = await getPackageInfo({
+ savedObjectsClient: soClient,
+ pkgName: packagePolicy.package.name,
+ pkgVersion: packagePolicy.package.version,
+ });
+
+ inputs = await this.assignPackageStream(pkgInfo, inputs);
+ }
+
await soClient.update(
SAVED_OBJECT_TYPE,
id,
{
...restOfPackagePolicy,
+ inputs,
revision: oldPackagePolicy.revision + 1,
updated_at: new Date().toISOString(),
updated_by: options?.user?.username ?? 'system',
@@ -353,6 +387,15 @@ class PackagePolicyService {
}
}
+function assignStreamIdToInput(packagePolicyId: string, input: NewPackagePolicyInput) {
+ return {
+ ...input,
+ streams: input.streams.map((stream) => {
+ return { ...stream, id: `${input.type}-${stream.data_stream.dataset}-${packagePolicyId}` };
+ }),
+ };
+}
+
async function _assignPackageStreamToInput(
registryPkgInfo: RegistryPackage,
pkgInfo: PackageInfo,
diff --git a/x-pack/plugins/ingest_manager/server/types/models/package_policy.ts b/x-pack/plugins/ingest_manager/server/types/models/package_policy.ts
index 6673c12d51511..20d29c0aa18c9 100644
--- a/x-pack/plugins/ingest_manager/server/types/models/package_policy.ts
+++ b/x-pack/plugins/ingest_manager/server/types/models/package_policy.ts
@@ -54,7 +54,7 @@ const PackagePolicyBaseSchema = {
),
streams: schema.arrayOf(
schema.object({
- id: schema.string(),
+ id: schema.maybe(schema.string()), // BWC < 7.11
enabled: schema.boolean(),
data_stream: schema.object({ dataset: schema.string(), type: schema.string() }),
vars: schema.maybe(ConfigRecordSchema),
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
index c9d99bcfb6d8d..0332f11aa78b3 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
@@ -451,6 +451,7 @@ export function LayerPanel(
columnId: activeId,
filterOperations: activeGroup.filterOperations,
suggestedPriority: activeGroup?.suggestedPriority,
+ dimensionGroups: groups,
setState: (newState: unknown) => {
props.updateAll(
datasourceId,
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
index f7a6f0597bf9c..b3ea14efbae80 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
@@ -601,7 +601,8 @@ describe('editor_frame', () => {
setDatasourceState(updatedState);
});
- expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(2);
+ // validation requires to calls this getConfiguration API
+ expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(6);
expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith(
expect.objectContaining({
state: updatedState,
@@ -680,7 +681,8 @@ describe('editor_frame', () => {
setDatasourceState({});
});
- expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(2);
+ // validation requires to calls this getConfiguration API
+ expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(6);
expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith(
expect.objectContaining({
frame: expect.objectContaining({
@@ -1193,7 +1195,8 @@ describe('editor_frame', () => {
instance.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click');
});
- expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(1);
+ // validation requires to calls this getConfiguration API
+ expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(4);
expect(mockVisualization.getConfiguration).toHaveBeenCalledWith(
expect.objectContaining({
state: suggestionVisState,
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts
index 28ad6c531e255..647c0f3ac9cca 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts
@@ -6,7 +6,13 @@
import { SavedObjectReference } from 'kibana/public';
import { Ast } from '@kbn/interpreter/common';
-import { Datasource, DatasourcePublicAPI, FramePublicAPI, Visualization } from '../../types';
+import {
+ Datasource,
+ DatasourcePublicAPI,
+ FramePublicAPI,
+ Visualization,
+ VisualizationDimensionGroupConfig,
+} from '../../types';
import { buildExpression } from './expression_helpers';
import { Document } from '../../persistence/saved_object_store';
import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public';
@@ -104,8 +110,24 @@ export const validateDatasourceAndVisualization = (
longMessage: string;
}>
| undefined => {
+ const layersGroups =
+ currentVisualizationState &&
+ currentVisualization
+ ?.getLayerIds(currentVisualizationState)
+ .reduce>((memo, layerId) => {
+ const groups = currentVisualization?.getConfiguration({
+ frame: frameAPI,
+ layerId,
+ state: currentVisualizationState,
+ }).groups;
+ if (groups) {
+ memo[layerId] = groups;
+ }
+ return memo;
+ }, {});
+
const datasourceValidationErrors = currentDatasourceState
- ? currentDataSource?.getErrorMessages(currentDatasourceState)
+ ? currentDataSource?.getErrorMessages(currentDatasourceState, layersGroups)
: undefined;
const visualizationValidationErrors = currentVisualizationState
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
index 829bd333ce2cc..92a4dad14dd25 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
@@ -174,6 +174,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
} as unknown) as DataPublicPluginStart['fieldFormats'],
} as unknown) as DataPublicPluginStart,
core: {} as CoreSetup,
+ dimensionGroups: [],
};
jest.clearAllMocks();
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts
index bbd1d4e0ae3ab..dd696f8be357f 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts
@@ -146,6 +146,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
} as unknown) as DataPublicPluginStart['fieldFormats'],
} as unknown) as DataPublicPluginStart,
core: {} as CoreSetup,
+ dimensionGroups: [],
};
jest.clearAllMocks();
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
index ecca1b878e9a7..fa106e90d518a 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
@@ -343,7 +343,7 @@ export function getIndexPatternDatasource({
getDatasourceSuggestionsFromCurrentState,
getDatasourceSuggestionsForVisualizeField,
- getErrorMessages(state) {
+ getErrorMessages(state, layersGroups) {
if (!state) {
return;
}
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts
index 3c96579fdc943..4ad849c5d441e 100644
--- a/x-pack/plugins/lens/public/types.ts
+++ b/x-pack/plugins/lens/public/types.ts
@@ -184,7 +184,10 @@ export interface Datasource {
) => Array>;
getPublicAPI: (props: PublicAPIProps) => DatasourcePublicAPI;
- getErrorMessages: (state: T) => Array<{ shortMessage: string; longMessage: string }> | undefined;
+ getErrorMessages: (
+ state: T,
+ layersGroups?: Record
+ ) => Array<{ shortMessage: string; longMessage: string }> | undefined;
/**
* uniqueLabels of dimensions exposed for aria-labels of dragged dimensions
*/
@@ -242,6 +245,7 @@ export type DatasourceDimensionEditorProps = DatasourceDimensionPro
setState: StateSetter;
core: Pick;
dateRange: DateRange;
+ dimensionGroups: VisualizationDimensionGroupConfig[];
};
export type DatasourceDimensionTriggerProps = DatasourceDimensionProps & {
diff --git a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js
index 96dad0c01139e..dc3ace69e5a61 100644
--- a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js
+++ b/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js
@@ -11,7 +11,7 @@ import { isRetina } from '../../../meta';
import {
addSpriteSheetToMapFromImageData,
loadSpriteSheetImageData,
-} from '../../../connected_components/map/mb/utils'; //todo move this implementation
+} from '../../../connected_components/mb_map/utils'; //todo move this implementation
const MB_STYLE_TYPE_TO_OPACITY = {
fill: ['fill-opacity'],
diff --git a/x-pack/plugins/maps/public/connected_components/_index.scss b/x-pack/plugins/maps/public/connected_components/_index.scss
index a952b3b545922..19c11d3fde662 100644
--- a/x-pack/plugins/maps/public/connected_components/_index.scss
+++ b/x-pack/plugins/maps/public/connected_components/_index.scss
@@ -2,4 +2,4 @@
@import 'layer_panel/index';
@import 'widget_overlay/index';
@import 'toolbar_overlay/index';
-@import 'map/features_tooltip/index';
+@import 'mb_map/features_tooltip/index';
diff --git a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx
index 352aed4a8cc93..169875e63a536 100644
--- a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx
+++ b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx
@@ -13,7 +13,7 @@ import uuid from 'uuid/v4';
import { Filter } from 'src/plugins/data/public';
import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public';
// @ts-expect-error
-import { MBMap } from '../map/mb';
+import { MBMap } from '../mb_map';
// @ts-expect-error
import { WidgetOverlay } from '../widget_overlay';
// @ts-expect-error
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts
similarity index 100%
rename from x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts
rename to x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.js
similarity index 97%
rename from x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.js
index 0356a8267c18a..089d4be28dff7 100644
--- a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.js
@@ -6,7 +6,7 @@
import _ from 'lodash';
import React from 'react';
-import { DRAW_TYPE } from '../../../../../common/constants';
+import { DRAW_TYPE } from '../../../../common/constants';
import MapboxDraw from '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw-unminified';
import DrawRectangle from 'mapbox-gl-draw-rectangle-mode';
import { DrawCircle } from './draw_circle';
@@ -15,7 +15,7 @@ import {
createSpatialFilterWithGeometry,
getBoundingBoxGeometry,
roundCoordinates,
-} from '../../../../../common/elasticsearch_util';
+} from '../../../../common/elasticsearch_util';
import { DrawTooltip } from './draw_tooltip';
const DRAW_RECTANGLE = 'draw_rectangle';
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.js
similarity index 97%
rename from x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.js
index c8bde29b94fb6..dd93b038ff8a1 100644
--- a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_tooltip.js
@@ -8,7 +8,7 @@ import _ from 'lodash';
import React, { Component } from 'react';
import { EuiPopover, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { DRAW_TYPE } from '../../../../../common/constants';
+import { DRAW_TYPE } from '../../../../common/constants';
const noop = () => {};
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/index.js b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.js
similarity index 83%
rename from x-pack/plugins/maps/public/connected_components/map/mb/draw_control/index.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.js
index bc026c41fcf0a..230ad5b3f39d5 100644
--- a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/index.js
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.js
@@ -6,8 +6,8 @@
import { connect } from 'react-redux';
import { DrawControl } from './draw_control';
-import { updateDrawState } from '../../../../actions';
-import { getDrawState, isDrawingFilter } from '../../../../selectors/map_selectors';
+import { updateDrawState } from '../../../actions';
+import { getDrawState, isDrawingFilter } from '../../../selectors/map_selectors';
function mapStateToProps(state = {}) {
return {
diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/feature_properties.test.js.snap b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/__snapshots__/feature_properties.test.js.snap
similarity index 100%
rename from x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/feature_properties.test.js.snap
rename to x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/__snapshots__/feature_properties.test.js.snap
diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/tooltip_header.test.js.snap b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/__snapshots__/tooltip_header.test.js.snap
similarity index 100%
rename from x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/tooltip_header.test.js.snap
rename to x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/__snapshots__/tooltip_header.test.js.snap
diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/_index.scss b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/_index.scss
similarity index 100%
rename from x-pack/plugins/maps/public/connected_components/map/features_tooltip/_index.scss
rename to x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/_index.scss
diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_geometry_filter_form.js
similarity index 100%
rename from x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_geometry_filter_form.js
diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.js
similarity index 100%
rename from x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.js
diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.test.js b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.test.js
similarity index 100%
rename from x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.test.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.test.js
diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/features_tooltip.js
similarity index 100%
rename from x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/features_tooltip.js
diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.js b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/tooltip_header.js
similarity index 100%
rename from x-pack/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/tooltip_header.js
diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.test.js b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/tooltip_header.test.js
similarity index 100%
rename from x-pack/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.test.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/tooltip_header.test.js
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/get_initial_view.ts b/x-pack/plugins/maps/public/connected_components/mb_map/get_initial_view.ts
similarity index 87%
rename from x-pack/plugins/maps/public/connected_components/map/mb/get_initial_view.ts
rename to x-pack/plugins/maps/public/connected_components/mb_map/get_initial_view.ts
index 20fb8186f9870..853819eb289a3 100644
--- a/x-pack/plugins/maps/public/connected_components/map/mb/get_initial_view.ts
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/get_initial_view.ts
@@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { INITIAL_LOCATION } from '../../../../common/constants';
-import { Goto, MapCenterAndZoom } from '../../../../common/descriptor_types';
-import { MapSettings } from '../../../reducers/map';
+import { INITIAL_LOCATION } from '../../../common/constants';
+import { Goto, MapCenterAndZoom } from '../../../common/descriptor_types';
+import { MapSettings } from '../../reducers/map';
export async function getInitialView(
goto: Goto | null,
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/image_utils.js b/x-pack/plugins/maps/public/connected_components/mb_map/image_utils.js
similarity index 100%
rename from x-pack/plugins/maps/public/connected_components/map/mb/image_utils.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/image_utils.js
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/index.js b/x-pack/plugins/maps/public/connected_components/mb_map/index.js
similarity index 84%
rename from x-pack/plugins/maps/public/connected_components/map/mb/index.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/index.js
index 4b8df07bd1f39..cccd5e571d3e8 100644
--- a/x-pack/plugins/maps/public/connected_components/map/mb/index.js
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/index.js
@@ -5,7 +5,7 @@
*/
import { connect } from 'react-redux';
-import { MBMap } from './view';
+import { MBMap } from './mb_map';
import {
mapExtentChanged,
mapReady,
@@ -14,7 +14,7 @@ import {
clearMouseCoordinates,
clearGoto,
setMapInitError,
-} from '../../../actions';
+} from '../../actions';
import {
getLayerList,
getMapReady,
@@ -25,9 +25,9 @@ import {
isViewControlHidden,
getSpatialFiltersLayer,
getMapSettings,
-} from '../../../selectors/map_selectors';
+} from '../../selectors/map_selectors';
-import { getInspectorAdapters } from '../../../reducers/non_serializable_instances';
+import { getInspectorAdapters } from '../../reducers/non_serializable_instances';
function mapStateToProps(state = {}) {
return {
@@ -72,7 +72,5 @@ function mapDispatchToProps(dispatch) {
};
}
-const connectedMBMap = connect(mapStateToProps, mapDispatchToProps, null, {
- forwardRef: true,
-})(MBMap);
-export { connectedMBMap as MBMap };
+const connected = connect(mapStateToProps, mapDispatchToProps)(MBMap);
+export { connected as MBMap };
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/mb.utils.test.js b/x-pack/plugins/maps/public/connected_components/mb_map/mb.utils.test.js
similarity index 98%
rename from x-pack/plugins/maps/public/connected_components/map/mb/mb.utils.test.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/mb.utils.test.js
index e2050724ef684..a28cc75f6d89d 100644
--- a/x-pack/plugins/maps/public/connected_components/map/mb/mb.utils.test.js
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb.utils.test.js
@@ -5,7 +5,7 @@
*/
import { removeOrphanedSourcesAndLayers } from './utils';
-import { SPATIAL_FILTERS_LAYER_ID } from '../../../../common/constants';
+import { SPATIAL_FILTERS_LAYER_ID } from '../../../common/constants';
import _ from 'lodash';
class MockMbMap {
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.js
similarity index 97%
rename from x-pack/plugins/maps/public/connected_components/map/mb/view.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/mb_map.js
index ddc48cfc9c329..04c376a093623 100644
--- a/x-pack/plugins/maps/public/connected_components/map/mb/view.js
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.js
@@ -6,15 +6,15 @@
import _ from 'lodash';
import React from 'react';
-import { ResizeChecker } from '../../../../../../../src/plugins/kibana_utils/public';
+import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public';
import { removeOrphanedSourcesAndLayers, addSpritesheetToMap } from './utils';
import { syncLayerOrder } from './sort_layers';
-import { getGlyphUrl, isRetina } from '../../../meta';
+import { getGlyphUrl, isRetina } from '../../meta';
import {
DECIMAL_DEGREES_PRECISION,
KBN_TOO_MANY_FEATURES_IMAGE_ID,
ZOOM_PRECISION,
-} from '../../../../common/constants';
+} from '../../../common/constants';
import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp';
import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker';
import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js';
@@ -23,9 +23,9 @@ import sprites1 from '@elastic/maki/dist/sprite@1.png';
import sprites2 from '@elastic/maki/dist/sprite@2.png';
import { DrawControl } from './draw_control';
import { TooltipControl } from './tooltip_control';
-import { clampToLatBounds, clampToLonBounds } from '../../../../common/elasticsearch_util';
+import { clampToLatBounds, clampToLonBounds } from '../../../common/elasticsearch_util';
import { getInitialView } from './get_initial_view';
-import { getPreserveDrawingBuffer } from '../../../kibana_services';
+import { getPreserveDrawingBuffer } from '../../kibana_services';
mapboxgl.workerUrl = mbWorkerUrl;
mapboxgl.setRTLTextPlugin(mbRtlPlugin);
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.test.ts b/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.test.ts
similarity index 98%
rename from x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.test.ts
rename to x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.test.ts
index e26a1e43509c8..9e85c7b04b266 100644
--- a/x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.test.ts
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.test.ts
@@ -8,8 +8,8 @@
import _ from 'lodash';
import { Map as MbMap, Layer as MbLayer, Style as MbStyle } from 'mapbox-gl';
import { getIsTextLayer, syncLayerOrder } from './sort_layers';
-import { SPATIAL_FILTERS_LAYER_ID } from '../../../../common/constants';
-import { ILayer } from '../../../classes/layers/layer';
+import { SPATIAL_FILTERS_LAYER_ID } from '../../../common/constants';
+import { ILayer } from '../../classes/layers/layer';
let moveCounter = 0;
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.ts b/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.ts
similarity index 98%
rename from x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.ts
rename to x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.ts
index 0c970fe663557..dda43269e32d8 100644
--- a/x-pack/plugins/maps/public/connected_components/map/mb/sort_layers.ts
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.ts
@@ -5,7 +5,7 @@
*/
import { Map as MbMap, Layer as MbLayer } from 'mapbox-gl';
-import { ILayer } from '../../../classes/layers/layer';
+import { ILayer } from '../../classes/layers/layer';
// "Layer" is overloaded and can mean the following
// 1) Map layer (ILayer): A single map layer consists of one to many mapbox layers.
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/__snapshots__/tooltip_control.test.js.snap
similarity index 100%
rename from x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap
rename to x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/__snapshots__/tooltip_control.test.js.snap
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/__snapshots__/tooltip_popover.test.js.snap
similarity index 100%
rename from x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap
rename to x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/__snapshots__/tooltip_popover.test.js.snap
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/index.js
similarity index 94%
rename from x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/index.js
index 407dcf1997aeb..7d2f2b05d6f11 100644
--- a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/index.js
@@ -11,13 +11,13 @@ import {
openOnClickTooltip,
closeOnHoverTooltip,
openOnHoverTooltip,
-} from '../../../../actions';
+} from '../../../actions';
import {
getLayerList,
getOpenTooltips,
getHasLockedTooltips,
isDrawingFilter,
-} from '../../../../selectors/map_selectors';
+} from '../../../selectors/map_selectors';
function mapStateToProps(state = {}) {
return {
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.js
similarity index 98%
rename from x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.js
index edfeb3c76b104..b178eef6fa5d3 100644
--- a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.js
@@ -6,9 +6,9 @@
import _ from 'lodash';
import React from 'react';
-import { FEATURE_ID_PROPERTY_NAME, LON_INDEX } from '../../../../../common/constants';
+import { FEATURE_ID_PROPERTY_NAME, LON_INDEX } from '../../../../common/constants';
import { TooltipPopover } from './tooltip_popover';
-import { EXCLUDE_TOO_MANY_FEATURES_BOX } from '../../../../classes/util/mb_filter_expressions';
+import { EXCLUDE_TOO_MANY_FEATURES_BOX } from '../../../classes/util/mb_filter_expressions';
function justifyAnchorLocation(mbLngLat, targetFeature) {
let popupAnchorLocation = [mbLngLat.lng, mbLngLat.lat]; // default popup location to mouse location
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.js
similarity index 100%
rename from x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.js
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.js
similarity index 97%
rename from x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.js
index 4cfddf0034039..ca4864f79940e 100644
--- a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.js
@@ -5,8 +5,8 @@
*/
import React, { Component } from 'react';
-import { LAT_INDEX, LON_INDEX } from '../../../../../common/constants';
-import { FeaturesTooltip } from '../../features_tooltip/features_tooltip';
+import { LAT_INDEX, LON_INDEX } from '../../../../common/constants';
+import { FeaturesTooltip } from '../features_tooltip/features_tooltip';
import { EuiPopover, EuiText } from '@elastic/eui';
const noop = () => {};
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.test.js
similarity index 98%
rename from x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.test.js
index 205ca7337277d..b15c3fce6c0b7 100644
--- a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.test.js
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-jest.mock('../../features_tooltip/features_tooltip', () => ({
+jest.mock('../features_tooltip/features_tooltip', () => ({
FeaturesTooltip: () => {
return mockFeaturesTooltip
;
},
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/utils.js b/x-pack/plugins/maps/public/connected_components/mb_map/utils.js
similarity index 100%
rename from x-pack/plugins/maps/public/connected_components/map/mb/utils.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/utils.js
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts
index 785f3ac9cd4dc..d46adff3de2a3 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts
@@ -33,6 +33,7 @@ export const MAX_COLUMNS = 10;
export const DEFAULT_REGRESSION_COLUMNS = 8;
export const BASIC_NUMERICAL_TYPES = new Set([
+ ES_FIELD_TYPES.UNSIGNED_LONG,
ES_FIELD_TYPES.LONG,
ES_FIELD_TYPES.INTEGER,
ES_FIELD_TYPES.SHORT,
diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts
index c3b1de64c3eb5..fec60f221b4fc 100644
--- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts
+++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts
@@ -27,6 +27,7 @@ const supportedTypes: string[] = [
ES_FIELD_TYPES.INTEGER,
ES_FIELD_TYPES.FLOAT,
ES_FIELD_TYPES.LONG,
+ ES_FIELD_TYPES.UNSIGNED_LONG,
ES_FIELD_TYPES.BYTE,
ES_FIELD_TYPES.HALF_FLOAT,
ES_FIELD_TYPES.SCALED_FLOAT,
@@ -245,6 +246,7 @@ function getNumericalFields(fields: Field[]): Field[] {
return fields.filter(
(f) =>
f.type === ES_FIELD_TYPES.LONG ||
+ f.type === ES_FIELD_TYPES.UNSIGNED_LONG ||
f.type === ES_FIELD_TYPES.INTEGER ||
f.type === ES_FIELD_TYPES.SHORT ||
f.type === ES_FIELD_TYPES.BYTE ||
diff --git a/x-pack/plugins/observability/common/annotations.ts b/x-pack/plugins/observability/common/annotations.ts
index 6aea4d3d92f9b..f7ab243cf73f3 100644
--- a/x-pack/plugins/observability/common/annotations.ts
+++ b/x-pack/plugins/observability/common/annotations.ts
@@ -5,7 +5,24 @@
*/
import * as t from 'io-ts';
-import { dateAsStringRt } from '../../apm/common/runtime_types/date_as_string_rt';
+import { either } from 'fp-ts/lib/Either';
+
+/**
+ * Checks whether a string is a valid ISO timestamp,
+ * but doesn't convert it into a Date object when decoding.
+ *
+ * Copied from x-pack/plugins/apm/common/runtime_types/date_as_string_rt.ts.
+ */
+const dateAsStringRt = new t.Type(
+ 'DateAsString',
+ t.string.is,
+ (input, context) =>
+ either.chain(t.string.validate(input, context), (str) => {
+ const date = new Date(str);
+ return isNaN(date.getTime()) ? t.failure(input, context) : t.success(str);
+ }),
+ t.identity
+);
export const createAnnotationRt = t.intersection([
t.type({
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index 767a2616a4c7e..8c423c663a4e8 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -39,6 +39,9 @@ export const FILTERS_GLOBAL_HEIGHT = 109; // px
export const FULL_SCREEN_TOGGLED_CLASS_NAME = 'fullScreenToggled';
export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51';
export const ENDPOINT_METADATA_INDEX = 'metrics-endpoint.metadata-*';
+export const DEFAULT_RULE_REFRESH_INTERVAL_ON = true;
+export const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000; // ms
+export const DEFAULT_RULE_REFRESH_IDLE_VALUE = 2700000; // ms
export enum SecurityPageName {
detections = 'detections',
@@ -74,6 +77,9 @@ export const DEFAULT_INDEX_PATTERN = [
/** This Kibana Advanced Setting enables the `Security news` feed widget */
export const ENABLE_NEWS_FEED_SETTING = 'securitySolution:enableNewsFeed';
+/** This Kibana Advanced Setting sets the auto refresh interval for the detections all rules table */
+export const DEFAULT_RULES_TABLE_REFRESH_SETTING = 'securitySolution:rulesTableRefresh';
+
/** This Kibana Advanced Setting specifies the URL of the News feed widget */
export const NEWS_FEED_URL_SETTING = 'securitySolution:newsFeedUrl';
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts
index 6ffbf4e4c8d4c..1b0417cf59bc2 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts
@@ -48,6 +48,8 @@ import {
} from '../common/schemas';
import {
threat_index,
+ concurrent_searches,
+ items_per_search,
threat_query,
threat_filters,
threat_mapping,
@@ -130,6 +132,8 @@ export const addPrepackagedRulesSchema = t.intersection([
threat_query, // defaults to "undefined" if not set during decode
threat_index, // defaults to "undefined" if not set during decode
threat_language, // defaults "undefined" if not set during decode
+ concurrent_searches, // defaults to "undefined" if not set during decode
+ items_per_search, // defaults to "undefined" if not set during decode
})
),
]);
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts
index a4f002b589ef5..1b6a8d6f27762 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts
@@ -1702,5 +1702,23 @@ describe('create rules schema', () => {
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(expected);
});
+
+ test('You can set a threat query, index, mapping, filters, concurrent_searches, items_per_search with a when creating a rule', () => {
+ const payload: CreateRulesSchema = {
+ ...getCreateThreatMatchRulesSchemaMock(),
+ concurrent_searches: 10,
+ items_per_search: 10,
+ };
+ const decoded = createRulesSchema.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+ const expected: CreateRulesSchemaDecoded = {
+ ...getCreateThreatMatchRulesSchemaDecodedMock(),
+ concurrent_searches: 10,
+ items_per_search: 10,
+ };
+ expect(getPaths(left(message.errors))).toEqual([]);
+ expect(message.schema).toEqual(expected);
+ });
});
});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts
index d8e7614fcb840..2fe52bbe470a5 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts
@@ -49,6 +49,8 @@ import {
} from '../common/schemas';
import {
threat_index,
+ concurrent_searches,
+ items_per_search,
threat_query,
threat_filters,
threat_mapping,
@@ -126,6 +128,8 @@ export const createRulesSchema = t.intersection([
threat_filters, // defaults to "undefined" if not set during decode
threat_index, // defaults to "undefined" if not set during decode
threat_language, // defaults "undefined" if not set during decode
+ concurrent_searches, // defaults "undefined" if not set during decode
+ items_per_search, // defaults "undefined" if not set during decode
})
),
]);
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts
index 75ad92578318c..a78b41cd0da18 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts
@@ -125,4 +125,36 @@ describe('create_rules_type_dependents', () => {
const errors = createRuleValidateTypeDependents(schema);
expect(errors).toEqual([]);
});
+
+ test('validates that both "items_per_search" and "concurrent_searches" works when together', () => {
+ const schema: CreateRulesSchema = {
+ ...getCreateThreatMatchRulesSchemaMock(),
+ concurrent_searches: 10,
+ items_per_search: 10,
+ };
+ const errors = createRuleValidateTypeDependents(schema);
+ expect(errors).toEqual([]);
+ });
+
+ test('does NOT validate when only "items_per_search" is present', () => {
+ const schema: CreateRulesSchema = {
+ ...getCreateThreatMatchRulesSchemaMock(),
+ items_per_search: 10,
+ };
+ const errors = createRuleValidateTypeDependents(schema);
+ expect(errors).toEqual([
+ 'when "items_per_search" exists, "concurrent_searches" must also exist',
+ ]);
+ });
+
+ test('does NOT validate when only "concurrent_searches" is present', () => {
+ const schema: CreateRulesSchema = {
+ ...getCreateThreatMatchRulesSchemaMock(),
+ concurrent_searches: 10,
+ };
+ const errors = createRuleValidateTypeDependents(schema);
+ expect(errors).toEqual([
+ 'when "concurrent_searches" exists, "items_per_search" must also exist',
+ ]);
+ });
});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts
index c2a41005ebf4d..c93b0f0b14f6a 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts
@@ -110,17 +110,23 @@ export const validateThreshold = (rule: CreateRulesSchema): string[] => {
export const validateThreatMapping = (rule: CreateRulesSchema): string[] => {
let errors: string[] = [];
if (isThreatMatchRule(rule.type)) {
- if (!rule.threat_mapping) {
+ if (rule.threat_mapping == null) {
errors = ['when "type" is "threat_match", "threat_mapping" is required', ...errors];
} else if (rule.threat_mapping.length === 0) {
errors = ['threat_mapping" must have at least one element', ...errors];
}
- if (!rule.threat_query) {
+ if (rule.threat_query == null) {
errors = ['when "type" is "threat_match", "threat_query" is required', ...errors];
}
- if (!rule.threat_index) {
+ if (rule.threat_index == null) {
errors = ['when "type" is "threat_match", "threat_index" is required', ...errors];
}
+ if (rule.concurrent_searches == null && rule.items_per_search != null) {
+ errors = ['when "items_per_search" exists, "concurrent_searches" must also exist', ...errors];
+ }
+ if (rule.concurrent_searches != null && rule.items_per_search == null) {
+ errors = ['when "concurrent_searches" exists, "items_per_search" must also exist', ...errors];
+ }
}
return errors;
};
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts
index 852394b74767b..4f28c46923865 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts
@@ -55,6 +55,8 @@ import {
} from '../common/schemas';
import {
threat_index,
+ items_per_search,
+ concurrent_searches,
threat_query,
threat_filters,
threat_mapping,
@@ -149,6 +151,8 @@ export const importRulesSchema = t.intersection([
threat_query, // defaults to "undefined" if not set during decode
threat_index, // defaults to "undefined" if not set during decode
threat_language, // defaults "undefined" if not set during decode
+ concurrent_searches, // defaults to "undefined" if not set during decode
+ items_per_search, // defaults to "undefined" if not set during decode
})
),
]);
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts
index f4dce5c7ac05f..45fcfbaa3c76a 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts
@@ -50,6 +50,8 @@ import {
} from '../common/schemas';
import {
threat_index,
+ concurrent_searches,
+ items_per_search,
threat_query,
threat_filters,
threat_mapping,
@@ -109,6 +111,8 @@ export const patchRulesSchema = t.exact(
threat_filters,
threat_mapping,
threat_language,
+ concurrent_searches,
+ items_per_search,
})
);
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts
index b0cd8b1c53688..5d759fc12cd52 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts
@@ -51,6 +51,8 @@ import {
} from '../common/schemas';
import {
threat_index,
+ concurrent_searches,
+ items_per_search,
threat_query,
threat_filters,
threat_mapping,
@@ -134,6 +136,8 @@ export const updateRulesSchema = t.intersection([
threat_filters, // defaults to "undefined" if not set during decode
threat_index, // defaults to "undefined" if not set during decode
threat_language, // defaults "undefined" if not set during decode
+ concurrent_searches, // defaults to "undefined" if not set during decode
+ items_per_search, // defaults to "undefined" if not set during decode
})
),
]);
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts
index 82675768a11b7..3508526e182d7 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts
@@ -762,9 +762,9 @@ describe('rules_schema', () => {
expect(fields).toEqual(expected);
});
- test('should return 5 fields for a rule of type "threat_match"', () => {
+ test('should return 8 fields for a rule of type "threat_match"', () => {
const fields = addThreatMatchFields({ type: 'threat_match' });
- expect(fields.length).toEqual(6);
+ expect(fields.length).toEqual(8);
});
});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts
index e85beddf0e51e..0f7d04763a36f 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts
@@ -63,6 +63,8 @@ import {
} from '../common/schemas';
import {
threat_index,
+ concurrent_searches,
+ items_per_search,
threat_query,
threat_filters,
threat_mapping,
@@ -144,6 +146,8 @@ export const dependentRulesSchema = t.partial({
threat_filters,
threat_index,
threat_query,
+ concurrent_searches,
+ items_per_search,
threat_mapping,
threat_language,
});
@@ -282,6 +286,12 @@ export const addThreatMatchFields = (typeAndTimelineOnly: TypeAndTimelineOnly):
t.exact(t.partial({ threat_language: dependentRulesSchema.props.threat_language })),
t.exact(t.partial({ threat_filters: dependentRulesSchema.props.threat_filters })),
t.exact(t.partial({ saved_id: dependentRulesSchema.props.saved_id })),
+ t.exact(t.partial({ concurrent_searches: dependentRulesSchema.props.concurrent_searches })),
+ t.exact(
+ t.partial({
+ items_per_search: dependentRulesSchema.props.items_per_search,
+ })
+ ),
];
} else {
return [];
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts
index 63d593ea84e67..d8f61e4309b17 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts
@@ -5,6 +5,8 @@
*/
import {
+ concurrent_searches,
+ items_per_search,
ThreatMapping,
threatMappingEntries,
ThreatMappingEntries,
@@ -33,7 +35,7 @@ describe('threat_mapping', () => {
expect(message.schema).toEqual(payload);
});
- test('it should NOT validate an extra entry item', () => {
+ test('it should fail validation with an extra entry item', () => {
const payload: ThreatMappingEntries & Array<{ extra: string }> = [
{
field: 'field.one',
@@ -50,7 +52,7 @@ describe('threat_mapping', () => {
expect(message.schema).toEqual({});
});
- test('it should NOT validate a non string', () => {
+ test('it should fail validation with a non string', () => {
const payload = ([
{
field: 5,
@@ -66,7 +68,7 @@ describe('threat_mapping', () => {
expect(message.schema).toEqual({});
});
- test('it should NOT validate a wrong type', () => {
+ test('it should fail validation with a wrong type', () => {
const payload = ([
{
field: 'field.one',
@@ -107,7 +109,7 @@ describe('threat_mapping', () => {
});
});
- test('it should NOT validate an extra key', () => {
+ test('it should fail validate with an extra key', () => {
const payload: ThreatMapping & Array<{ extra: string }> = [
{
entries: [
@@ -129,7 +131,7 @@ describe('threat_mapping', () => {
expect(message.schema).toEqual({});
});
- test('it should NOT validate an extra inner entry', () => {
+ test('it should fail validate with an extra inner entry', () => {
const payload: ThreatMapping & Array<{ entries: Array<{ extra: string }> }> = [
{
entries: [
@@ -151,7 +153,7 @@ describe('threat_mapping', () => {
expect(message.schema).toEqual({});
});
- test('it should NOT validate an extra inner entry with the wrong data type', () => {
+ test('it should fail validate with an extra inner entry with the wrong data type', () => {
const payload = ([
{
entries: [
@@ -173,4 +175,48 @@ describe('threat_mapping', () => {
]);
expect(message.schema).toEqual({});
});
+
+ test('it should fail validation when concurrent_searches is < 0', () => {
+ const payload = -1;
+ const decoded = concurrent_searches.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+ expect(getPaths(left(message.errors))).toEqual([
+ 'Invalid value "-1" supplied to "PositiveIntegerGreaterThanZero"',
+ ]);
+ expect(message.schema).toEqual({});
+ });
+
+ test('it should fail validation when concurrent_searches is 0', () => {
+ const payload = 0;
+ const decoded = concurrent_searches.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+ expect(getPaths(left(message.errors))).toEqual([
+ 'Invalid value "0" supplied to "PositiveIntegerGreaterThanZero"',
+ ]);
+ expect(message.schema).toEqual({});
+ });
+
+ test('it should fail validation when items_per_search is 0', () => {
+ const payload = 0;
+ const decoded = items_per_search.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+ expect(getPaths(left(message.errors))).toEqual([
+ 'Invalid value "0" supplied to "PositiveIntegerGreaterThanZero"',
+ ]);
+ expect(message.schema).toEqual({});
+ });
+
+ test('it should fail validation when items_per_search is < 0', () => {
+ const payload = -1;
+ const decoded = items_per_search.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+ expect(getPaths(left(message.errors))).toEqual([
+ 'Invalid value "-1" supplied to "PositiveIntegerGreaterThanZero"',
+ ]);
+ expect(message.schema).toEqual({});
+ });
});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts
index a1be6485f596b..dec8ddd000132 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts
@@ -9,6 +9,7 @@
import * as t from 'io-ts';
import { language } from '../common/schemas';
import { NonEmptyString } from './non_empty_string';
+import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero';
export const threat_query = t.string;
export type ThreatQuery = t.TypeOf;
@@ -55,3 +56,13 @@ export const threat_language = t.union([language, t.undefined]);
export type ThreatLanguage = t.TypeOf;
export const threatLanguageOrUndefined = t.union([threat_language, t.undefined]);
export type ThreatLanguageOrUndefined = t.TypeOf;
+
+export const concurrent_searches = PositiveIntegerGreaterThanZero;
+export type ConcurrentSearches = t.TypeOf;
+export const concurrentSearchesOrUndefined = t.union([concurrent_searches, t.undefined]);
+export type ConcurrentSearchesOrUndefined = t.TypeOf;
+
+export const items_per_search = PositiveIntegerGreaterThanZero;
+export type ItemsPerSearch = t.TypeOf;
+export const itemsPerSearchOrUndefined = t.union([items_per_search, t.undefined]);
+export type ItemsPerSearchOrUndefined = t.TypeOf;
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts
index 3fa304ab7cf19..6a62caecfaa67 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts
@@ -10,6 +10,7 @@ import {
RULE_SWITCH,
SECOND_RULE,
SEVENTH_RULE,
+ RULE_AUTO_REFRESH_IDLE_MODAL,
} from '../screens/alerts_detection_rules';
import {
@@ -19,12 +20,17 @@ import {
} from '../tasks/alerts';
import {
activateRule,
+ checkAllRulesIdleModal,
+ checkAutoRefresh,
+ dismissAllRulesIdleModal,
+ resetAllRulesIdleModalTimeout,
sortByActivatedRules,
waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded,
waitForRuleToBeActivated,
} from '../tasks/alerts_detection_rules';
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
+import { DEFAULT_RULE_REFRESH_INTERVAL_VALUE } from '../../common/constants';
import { DETECTIONS_URL } from '../urls/navigation';
@@ -35,6 +41,7 @@ describe('Alerts detection rules', () => {
after(() => {
esArchiverUnload('prebuilt_rules_loaded');
+ cy.clock().invoke('restore');
});
it('Sorts by activated rules', () => {
@@ -75,4 +82,34 @@ describe('Alerts detection rules', () => {
});
});
});
+
+ it('Auto refreshes rules', () => {
+ cy.clock(Date.now());
+
+ loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
+ waitForAlertsPanelToBeLoaded();
+ waitForAlertsIndexToBeCreated();
+ goToManageAlertsDetectionRules();
+ waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded();
+
+ // mock 1 minute passing to make sure refresh
+ // is conducted
+ checkAutoRefresh(DEFAULT_RULE_REFRESH_INTERVAL_VALUE, 'be.visible');
+
+ // mock 45 minutes passing to check that idle modal shows
+ // and refreshing is paused
+ checkAllRulesIdleModal('be.visible');
+ checkAutoRefresh(DEFAULT_RULE_REFRESH_INTERVAL_VALUE, 'not.be.visible');
+
+ // clicking on modal to continue, should resume refreshing
+ dismissAllRulesIdleModal();
+ checkAutoRefresh(DEFAULT_RULE_REFRESH_INTERVAL_VALUE, 'be.visible');
+
+ // if mouse movement detected, idle modal should not
+ // show after 45 min
+ resetAllRulesIdleModalTimeout();
+ cy.get(RULE_AUTO_REFRESH_IDLE_MODAL).should('not.exist');
+
+ cy.clock().invoke('restore');
+ });
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts
index 13fa9592469e4..9eb49c19c23f6 100644
--- a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts
@@ -142,10 +142,11 @@ describe('Events Viewer', () => {
});
});
- context.skip('Events columns', () => {
+ context('Events columns', () => {
before(() => {
loginAndWaitForPage(HOSTS_URL);
openEvents();
+ cy.scrollTo('bottom');
waitsForEventsToBeLoaded();
});
@@ -160,9 +161,8 @@ describe('Events Viewer', () => {
const expectedOrderAfterDragAndDrop =
'message@timestamphost.nameevent.moduleevent.datasetevent.actionuser.namesource.ipdestination.ip';
- cy.scrollTo('bottom');
cy.get(HEADERS_GROUP).invoke('text').should('equal', originalColumnOrder);
- dragAndDropColumn({ column: 0, newPosition: 1 });
+ dragAndDropColumn({ column: 0, newPosition: 0 });
cy.get(HEADERS_GROUP).invoke('text').should('equal', expectedOrderAfterDragAndDrop);
});
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts
index 383ebe2220585..d518f9e42f21f 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts
@@ -13,7 +13,8 @@ import { TABLE_COLUMN_EVENTS_MESSAGE } from '../screens/hosts/external_events';
import { waitsForEventsToBeLoaded, openEventsViewerFieldsBrowser } from '../tasks/hosts/events';
import { removeColumn, resetFields } from '../tasks/timeline';
-describe('persistent timeline', () => {
+// Failing: See https://github.com/elastic/kibana/issues/75794
+describe.skip('persistent timeline', () => {
before(() => {
loginAndWaitForPage(HOSTS_URL);
openEvents();
diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts
index 0d0ea8460edf1..5ac8cd8f6cc9f 100644
--- a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts
@@ -10,7 +10,7 @@ export const CREATE_NEW_RULE_BTN = '[data-test-subj="create-new-rule"]';
export const COLLAPSED_ACTION_BTN = '[data-test-subj="euiCollapsedItemActionsButton"]';
-export const CUSTOM_RULES_BTN = '[data-test-subj="show-custom-rules-filter-button"]';
+export const CUSTOM_RULES_BTN = '[data-test-subj="showCustomRulesFilterButton"]';
export const DELETE_RULE_ACTION_BTN = '[data-test-subj="deleteRuleAction"]';
@@ -18,7 +18,7 @@ export const EDIT_RULE_ACTION_BTN = '[data-test-subj="editRuleAction"]';
export const DELETE_RULE_BULK_BTN = '[data-test-subj="deleteRuleBulk"]';
-export const ELASTIC_RULES_BTN = '[data-test-subj="show-elastic-rules-filter-button"]';
+export const ELASTIC_RULES_BTN = '[data-test-subj="showElasticRulesFilterButton"]';
export const EXPORT_ACTION_BTN = '[data-test-subj="exportRuleAction"]';
@@ -31,7 +31,7 @@ export const LOAD_PREBUILT_RULES_BTN = '[data-test-subj="load-prebuilt-rules"]';
export const LOADING_INITIAL_PREBUILT_RULES_TABLE =
'[data-test-subj="initialLoadingPanelAllRulesTable"]';
-export const LOADING_SPINNER = '[data-test-subj="loading-spinner"]';
+export const ASYNC_LOADING_PROGRESS = '[data-test-subj="loadingRulesInfoProgress"]';
export const NEXT_BTN = '[data-test-subj="pagination-button-next"]';
@@ -64,3 +64,7 @@ export const SHOWING_RULES_TEXT = '[data-test-subj="showingRules"]';
export const SORT_RULES_BTN = '[data-test-subj="tableHeaderSortButton"]';
export const THREE_HUNDRED_ROWS = '[data-test-subj="tablePagination-300-rows"]';
+
+export const RULE_AUTO_REFRESH_IDLE_MODAL = '[data-test-subj="allRulesIdleModal"]';
+
+export const RULE_AUTO_REFRESH_IDLE_MODAL_CONTINUE = '[data-test-subj="allRulesIdleModal"] button';
diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts
index 0434de7bff88e..cf507924a753f 100644
--- a/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts
@@ -34,3 +34,6 @@ export const LOAD_MORE =
'[data-test-subj="events-viewer-panel"] [data-test-subj="TimelineMoreButton"';
export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]';
+
+export const EVENTS_VIEWER_PAGINATION =
+ '[data-test-subj="events-viewer-panel"] [data-test-subj="timeline-pagination"]';
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 1c430e12b6b73..d4602dcd16db8 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
@@ -13,7 +13,6 @@ import {
DELETE_RULE_BULK_BTN,
LOAD_PREBUILT_RULES_BTN,
LOADING_INITIAL_PREBUILT_RULES_TABLE,
- LOADING_SPINNER,
PAGINATION_POPOVER_BTN,
RELOAD_PREBUILT_RULES_BTN,
RULE_CHECKBOX,
@@ -26,6 +25,9 @@ import {
EXPORT_ACTION_BTN,
EDIT_RULE_ACTION_BTN,
NEXT_BTN,
+ ASYNC_LOADING_PROGRESS,
+ RULE_AUTO_REFRESH_IDLE_MODAL,
+ RULE_AUTO_REFRESH_IDLE_MODAL_CONTINUE,
} from '../screens/alerts_detection_rules';
import { ALL_ACTIONS, DELETE_RULE } from '../screens/rule_details';
@@ -66,8 +68,8 @@ export const exportFirstRule = () => {
export const filterByCustomRules = () => {
cy.get(CUSTOM_RULES_BTN).click({ force: true });
- cy.get(LOADING_SPINNER).should('exist');
- cy.get(LOADING_SPINNER).should('not.exist');
+ cy.get(ASYNC_LOADING_PROGRESS).should('exist');
+ cy.get(ASYNC_LOADING_PROGRESS).should('not.exist');
};
export const goToCreateNewRule = () => {
@@ -119,6 +121,32 @@ export const waitForRuleToBeActivated = () => {
};
export const waitForRulesToBeLoaded = () => {
- cy.get(LOADING_SPINNER).should('exist');
- cy.get(LOADING_SPINNER).should('not.exist');
+ cy.get(ASYNC_LOADING_PROGRESS).should('exist');
+ cy.get(ASYNC_LOADING_PROGRESS).should('not.exist');
+};
+
+// when using, ensure you've called cy.clock prior in test
+export const checkAutoRefresh = (ms: number, condition: string) => {
+ cy.get(ASYNC_LOADING_PROGRESS).should('not.be.visible');
+ cy.tick(ms);
+ cy.get(ASYNC_LOADING_PROGRESS).should(condition);
+};
+
+export const dismissAllRulesIdleModal = () => {
+ cy.get(RULE_AUTO_REFRESH_IDLE_MODAL_CONTINUE)
+ .eq(1)
+ .should('exist')
+ .click({ force: true, multiple: true });
+ cy.get(RULE_AUTO_REFRESH_IDLE_MODAL).should('not.be.visible');
+};
+
+export const checkAllRulesIdleModal = (condition: string) => {
+ cy.tick(2700000);
+ cy.get(RULE_AUTO_REFRESH_IDLE_MODAL).should(condition);
+};
+
+export const resetAllRulesIdleModalTimeout = () => {
+ cy.tick(2000000);
+ cy.window().trigger('mousemove', { force: true });
+ cy.tick(700000);
};
diff --git a/x-pack/plugins/security_solution/cypress/tasks/common.ts b/x-pack/plugins/security_solution/cypress/tasks/common.ts
index e16db54599981..bb009f34b02d6 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/common.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/common.ts
@@ -23,14 +23,14 @@ export const drag = (subject: JQuery) => {
clientY: subjectLocation.top,
force: true,
})
- .wait(3000)
+ .wait(300)
.trigger('mousemove', {
button: primaryButton,
clientX: subjectLocation.left + dndSloppyClickDetectionThreshold,
clientY: subjectLocation.top,
force: true,
})
- .wait(3000);
+ .wait(300);
};
/** Drags the subject being dragged on the specified drop target, but does not drop it */
@@ -42,11 +42,17 @@ export const dragWithoutDrop = (dropTarget: JQuery) => {
/** "Drops" the subject being dragged on the specified drop target */
export const drop = (dropTarget: JQuery) => {
+ const targetLocation = dropTarget[0].getBoundingClientRect();
cy.wrap(dropTarget)
- .trigger('mousemove', { button: primaryButton, force: true })
- .wait(3000)
+ .trigger('mousemove', {
+ button: primaryButton,
+ clientX: targetLocation.left,
+ clientY: targetLocation.top,
+ force: true,
+ })
+ .wait(300)
.trigger('mouseup', { force: true })
- .wait(3000);
+ .wait(300);
};
export const reload = (afterReload: () => void) => {
diff --git a/x-pack/plugins/security_solution/cypress/tasks/hosts/events.ts b/x-pack/plugins/security_solution/cypress/tasks/hosts/events.ts
index 226178cd92f18..401a78767ac57 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/hosts/events.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/hosts/events.ts
@@ -8,6 +8,7 @@ import { drag, drop } from '../common';
import {
CLOSE_MODAL,
EVENTS_VIEWER_FIELDS_BUTTON,
+ EVENTS_VIEWER_PAGINATION,
FIELDS_BROWSER_CONTAINER,
HOST_GEO_CITY_NAME_CHECKBOX,
HOST_GEO_COUNTRY_NAME_CHECKBOX,
@@ -16,6 +17,7 @@ import {
SERVER_SIDE_EVENT_COUNT,
} from '../../screens/hosts/events';
import { DRAGGABLE_HEADER } from '../../screens/timeline';
+import { REFRESH_BUTTON } from '../../screens/security_header';
export const addsHostGeoCityNameToHeader = () => {
cy.get(HOST_GEO_CITY_NAME_CHECKBOX).check({
@@ -53,7 +55,9 @@ export const opensInspectQueryModal = () => {
};
export const waitsForEventsToBeLoaded = () => {
- cy.get(SERVER_SIDE_EVENT_COUNT).should('exist').invoke('text').should('not.equal', '0');
+ cy.get(SERVER_SIDE_EVENT_COUNT).should('not.have.text', '0');
+ cy.get(REFRESH_BUTTON).should('not.have.text', 'Updating');
+ cy.get(EVENTS_VIEWER_PAGINATION).should('exist');
};
export const dragAndDropColumn = ({
diff --git a/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap
index f2d2d23d60fb1..d3d20c7183570 100644
--- a/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap
@@ -7,7 +7,9 @@ exports[`HeaderSection it renders 1`] = `
-
+
= ({
@@ -57,10 +58,11 @@ const HeaderSectionComponent: React.FC = ({
title,
titleSize = 'm',
tooltip,
+ growLeftSplit = true,
}) => (
-
+
diff --git a/x-pack/plugins/security_solution/public/common/components/last_updated/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/last_updated/index.test.tsx
new file mode 100644
index 0000000000000..db42794448c53
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/last_updated/index.test.tsx
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { mount } from 'enzyme';
+import { I18nProvider } from '@kbn/i18n/react';
+
+import { LastUpdatedAt } from './';
+
+describe('LastUpdatedAt', () => {
+ beforeEach(() => {
+ Date.now = jest.fn().mockReturnValue(1603995369774);
+ });
+
+ test('it renders correct relative time', () => {
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.text()).toEqual(' Updated 2 minutes ago');
+ });
+
+ test('it only renders icon if "compact" is true', () => {
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.text()).toEqual('');
+ expect(wrapper.find('[data-test-subj="last-updated-at-clock-icon"]').exists()).toBeTruthy();
+ });
+
+ test('it renders updating text if "showUpdating" is true', () => {
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.text()).toEqual(' Updating...');
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/last_updated/index.tsx b/x-pack/plugins/security_solution/public/common/components/last_updated/index.tsx
new file mode 100644
index 0000000000000..ef4ff0123dd1c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/last_updated/index.tsx
@@ -0,0 +1,83 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiIcon, EuiText, EuiToolTip } from '@elastic/eui';
+import { FormattedRelative } from '@kbn/i18n/react';
+import React, { useEffect, useMemo, useState } from 'react';
+
+import * as i18n from './translations';
+
+interface LastUpdatedAtProps {
+ compact?: boolean;
+ updatedAt: number;
+ showUpdating?: boolean;
+}
+
+export const Updated = React.memo<{ date: number; prefix: string; updatedAt: number }>(
+ ({ date, prefix, updatedAt }) => (
+ <>
+ {prefix}
+ {
+
+ }
+ >
+ )
+);
+
+Updated.displayName = 'Updated';
+
+const prefix = ` ${i18n.UPDATED} `;
+
+export const LastUpdatedAt = React.memo(
+ ({ compact = false, updatedAt, showUpdating = false }) => {
+ const [date, setDate] = useState(Date.now());
+
+ function tick() {
+ setDate(Date.now());
+ }
+
+ useEffect(() => {
+ const timerID = setInterval(() => tick(), 10000);
+ return () => {
+ clearInterval(timerID);
+ };
+ }, []);
+
+ const updateText = useMemo(() => {
+ if (showUpdating) {
+ return {i18n.UPDATING} ;
+ }
+
+ if (!compact) {
+ return ;
+ }
+
+ return null;
+ }, [compact, date, showUpdating, updatedAt]);
+
+ return (
+
+
+ >
+ }
+ >
+
+
+ {updateText}
+
+
+ );
+ }
+);
+
+LastUpdatedAt.displayName = 'LastUpdatedAt';
diff --git a/x-pack/plugins/security_solution/public/common/components/last_updated/translations.ts b/x-pack/plugins/security_solution/public/common/components/last_updated/translations.ts
new file mode 100644
index 0000000000000..77278563b24d5
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/last_updated/translations.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const UPDATING = i18n.translate('xpack.securitySolution.lastUpdated.updating', {
+ defaultMessage: 'Updating...',
+});
+
+export const UPDATED = i18n.translate('xpack.securitySolution.lastUpdated.updated', {
+ defaultMessage: 'Updated',
+});
diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts
index 06c152b94cfd8..38ae49ba3b19c 100644
--- a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts
@@ -27,6 +27,10 @@ import {
DEFAULT_REFRESH_RATE_INTERVAL,
DEFAULT_TIME_RANGE,
DEFAULT_TO,
+ DEFAULT_RULES_TABLE_REFRESH_SETTING,
+ DEFAULT_RULE_REFRESH_INTERVAL_ON,
+ DEFAULT_RULE_REFRESH_INTERVAL_VALUE,
+ DEFAULT_RULE_REFRESH_IDLE_VALUE,
} from '../../../../common/constants';
import { StartServices } from '../../../types';
import { createSecuritySolutionStorageMock } from '../../mock/mock_local_storage';
@@ -48,6 +52,11 @@ const mockUiSettings: Record = {
[DEFAULT_DATE_FORMAT_TZ]: 'UTC',
[DEFAULT_DATE_FORMAT]: 'MMM D, YYYY @ HH:mm:ss.SSS',
[DEFAULT_DARK_MODE]: false,
+ [DEFAULT_RULES_TABLE_REFRESH_SETTING]: {
+ on: DEFAULT_RULE_REFRESH_INTERVAL_ON,
+ value: DEFAULT_RULE_REFRESH_INTERVAL_VALUE,
+ idleTimeout: DEFAULT_RULE_REFRESH_IDLE_VALUE,
+ },
};
export const createUseUiSettingMock = () => (key: string, defaultValue?: unknown): unknown => {
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx
index 164b1df8463e6..221963767caad 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx
@@ -95,7 +95,7 @@ export const THREAT_MATCH_INDEX_HELPER_TEXT = i18n.translate(
export const THREAT_MATCH_REQUIRED = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredError',
{
- defaultMessage: 'At least one threat match is required.',
+ defaultMessage: 'At least one indicator match is required.',
}
);
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx
index 6800743db738e..2b03d6dd4de36 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx
@@ -210,7 +210,7 @@ export const getColumns = ({
getEmptyTagValue()
) : (
-
+
);
},
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx
index 1a4c2d405dca3..be42d7b3212fd 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx
@@ -6,13 +6,21 @@
import React from 'react';
import { shallow, mount } from 'enzyme';
+import { waitFor } from '@testing-library/react';
import '../../../../../common/mock/match_media';
import '../../../../../common/mock/formatted_relative';
-import { TestProviders } from '../../../../../common/mock';
-import { waitFor } from '@testing-library/react';
import { AllRules } from './index';
-import { useKibana } from '../../../../../common/lib/kibana';
+import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana';
+import { useRules, useRulesStatuses } from '../../../../containers/detection_engine/rules';
+import { TestProviders } from '../../../../../common/mock';
+import { createUseUiSetting$Mock } from '../../../../../common/lib/kibana/kibana_react.mock';
+import {
+ DEFAULT_RULE_REFRESH_INTERVAL_ON,
+ DEFAULT_RULE_REFRESH_INTERVAL_VALUE,
+ DEFAULT_RULE_REFRESH_IDLE_VALUE,
+ DEFAULT_RULES_TABLE_REFRESH_SETTING,
+} from '../../../../../../common/constants';
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
@@ -27,66 +35,33 @@ jest.mock('react-router-dom', () => {
jest.mock('../../../../../common/components/link_to');
jest.mock('../../../../../common/lib/kibana');
+jest.mock('../../../../containers/detection_engine/rules');
const useKibanaMock = useKibana as jest.Mocked;
+const mockUseUiSetting$ = useUiSetting$ as jest.Mock;
-jest.mock('./reducer', () => {
- return {
- allRulesReducer: jest.fn().mockReturnValue(() => ({
- exportRuleIds: [],
- filterOptions: {
- filter: 'some filter',
- sortField: 'some sort field',
- sortOrder: 'desc',
- },
- loadingRuleIds: [],
- loadingRulesAction: null,
- pagination: {
- page: 1,
- perPage: 20,
- total: 1,
- },
- rules: [
- {
- actions: [],
- created_at: '2020-02-14T19:49:28.178Z',
- created_by: 'elastic',
- description: 'jibber jabber',
- enabled: false,
- false_positives: [],
- filters: [],
- from: 'now-660s',
- id: 'rule-id-1',
- immutable: true,
- index: ['endgame-*'],
- interval: '10m',
- language: 'kuery',
- max_signals: 100,
- name: 'Credential Dumping - Detected - Elastic Endpoint',
- output_index: '.siem-signals-default',
- query: 'host.name:*',
- references: [],
- risk_score: 73,
- rule_id: '571afc56-5ed9-465d-a2a9-045f099f6e7e',
- severity: 'high',
- tags: ['Elastic', 'Endpoint'],
- threat: [],
- throttle: null,
- to: 'now',
- type: 'query',
- updated_at: '2020-02-14T19:49:28.320Z',
- updated_by: 'elastic',
- version: 1,
- },
- ],
- selectedRuleIds: [],
- })),
- };
-});
+describe('AllRules', () => {
+ const mockRefetchRulesData = jest.fn();
-jest.mock('../../../../containers/detection_engine/rules', () => {
- return {
- useRules: jest.fn().mockReturnValue([
+ beforeEach(() => {
+ jest.useFakeTimers();
+
+ mockUseUiSetting$.mockImplementation((key, defaultValue) => {
+ const useUiSetting$Mock = createUseUiSetting$Mock();
+
+ return key === DEFAULT_RULES_TABLE_REFRESH_SETTING
+ ? [
+ {
+ on: DEFAULT_RULE_REFRESH_INTERVAL_ON,
+ value: DEFAULT_RULE_REFRESH_INTERVAL_VALUE,
+ idleTimeout: DEFAULT_RULE_REFRESH_IDLE_VALUE,
+ },
+ jest.fn(),
+ ]
+ : useUiSetting$Mock(key, defaultValue);
+ });
+
+ (useRules as jest.Mock).mockReturnValue([
false,
{
page: 1,
@@ -126,8 +101,10 @@ jest.mock('../../../../containers/detection_engine/rules', () => {
},
],
},
- ]),
- useRulesStatuses: jest.fn().mockReturnValue({
+ mockRefetchRulesData,
+ ]);
+
+ (useRulesStatuses as jest.Mock).mockReturnValue({
loading: false,
rulesStatuses: [
{
@@ -150,21 +127,8 @@ jest.mock('../../../../containers/detection_engine/rules', () => {
name: 'Test rule',
},
],
- }),
- };
-});
-
-jest.mock('react-router-dom', () => {
- const originalModule = jest.requireActual('react-router-dom');
-
- return {
- ...originalModule,
- useHistory: jest.fn(),
- };
-});
+ });
-describe('AllRules', () => {
- beforeEach(() => {
useKibanaMock().services.application.capabilities = {
navLinks: {},
management: {},
@@ -172,6 +136,12 @@ describe('AllRules', () => {
actions: { show: true },
};
});
+
+ afterEach(() => {
+ jest.clearAllTimers();
+ jest.clearAllMocks();
+ });
+
it('renders correctly', () => {
const wrapper = shallow(
{
expect(wrapper.find('[title="All rules"]')).toHaveLength(1);
});
+ it('it pulls from uiSettings to determine default refresh values', async () => {
+ mount(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(mockRefetchRulesData).not.toHaveBeenCalled();
+
+ jest.advanceTimersByTime(DEFAULT_RULE_REFRESH_INTERVAL_VALUE);
+ expect(mockRefetchRulesData).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ // refresh functionality largely tested in cypress tests
+ it('it pulls from storage and does not set an auto refresh interval if storage indicates refresh is paused', async () => {
+ mockUseUiSetting$.mockImplementation(() => [
+ {
+ on: false,
+ value: DEFAULT_RULE_REFRESH_INTERVAL_VALUE,
+ idleTimeout: DEFAULT_RULE_REFRESH_IDLE_VALUE,
+ },
+ jest.fn(),
+ ]);
+
+ const wrapper = mount(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(mockRefetchRulesData).not.toHaveBeenCalled();
+
+ jest.advanceTimersByTime(DEFAULT_RULE_REFRESH_INTERVAL_VALUE);
+ expect(mockRefetchRulesData).not.toHaveBeenCalled();
+
+ wrapper.find('[data-test-subj="refreshSettings"] button').first().simulate('click');
+
+ wrapper.find('[data-test-subj="refreshSettingsSwitch"]').first().simulate('click');
+
+ jest.advanceTimersByTime(DEFAULT_RULE_REFRESH_INTERVAL_VALUE);
+ expect(mockRefetchRulesData).not.toHaveBeenCalled();
+ });
+ });
+
describe('rules tab', () => {
- it('renders correctly', async () => {
+ it('renders all rules tab by default', async () => {
const wrapper = mount(
{
/>
);
- const monitoringTab = wrapper.find('[data-test-subj="allRulesTableTab-monitoring"] button');
- monitoringTab.simulate('click');
await waitFor(() => {
+ const monitoringTab = wrapper.find('[data-test-subj="allRulesTableTab-monitoring"] button');
+ monitoringTab.simulate('click');
+
wrapper.update();
expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeTruthy();
expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeFalsy();
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx
index 86b3daddd6c19..663a4bb242c06 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx
@@ -6,15 +6,18 @@
import {
EuiBasicTable,
- EuiContextMenuPanel,
EuiLoadingContent,
EuiSpacer,
EuiTab,
EuiTabs,
+ EuiProgress,
+ EuiOverlayMask,
+ EuiConfirmModal,
} from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
import { useHistory } from 'react-router-dom';
import uuid from 'uuid';
+import { debounce } from 'lodash/fp';
import {
useRules,
@@ -27,14 +30,7 @@ import {
RulesSortingFields,
} from '../../../../containers/detection_engine/rules';
import { HeaderSection } from '../../../../../common/components/header_section';
-import {
- UtilityBar,
- UtilityBarAction,
- UtilityBarGroup,
- UtilityBarSection,
- UtilityBarText,
-} from '../../../../../common/components/utility_bar';
-import { useKibana } from '../../../../../common/lib/kibana';
+import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana';
import { useStateToaster } from '../../../../../common/components/toasters';
import { Loader } from '../../../../../common/components/loader';
import { Panel } from '../../../../../common/components/panel';
@@ -55,6 +51,9 @@ import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_l
import { SecurityPageName } from '../../../../../app/types';
import { useFormatUrl } from '../../../../../common/components/link_to';
import { isBoolean } from '../../../../../common/utils/privileges';
+import { AllRulesUtilityBar } from './utility_bar';
+import { LastUpdatedAt } from '../../../../../common/components/last_updated';
+import { DEFAULT_RULES_TABLE_REFRESH_SETTING } from '../../../../../../common/constants';
const INITIAL_SORT_FIELD = 'enabled';
const initialState: State = {
@@ -73,6 +72,9 @@ const initialState: State = {
},
rules: [],
selectedRuleIds: [],
+ lastUpdated: 0,
+ showIdleModal: false,
+ isRefreshOn: true,
};
interface AllRulesProps {
@@ -129,6 +131,18 @@ export const AllRules = React.memo(
}) => {
const [initLoading, setInitLoading] = useState(true);
const tableRef = useRef();
+ const {
+ services: {
+ application: {
+ capabilities: { actions },
+ },
+ },
+ } = useKibana();
+ const [defaultAutoRefreshSetting] = useUiSetting$<{
+ on: boolean;
+ value: number;
+ idleTimeout: number;
+ }>(DEFAULT_RULES_TABLE_REFRESH_SETTING);
const [
{
exportRuleIds,
@@ -138,9 +152,16 @@ export const AllRules = React.memo(
pagination,
rules,
selectedRuleIds,
+ lastUpdated,
+ showIdleModal,
+ isRefreshOn,
},
dispatch,
- ] = useReducer(allRulesReducer(tableRef), initialState);
+ ] = useReducer(allRulesReducer(tableRef), {
+ ...initialState,
+ lastUpdated: Date.now(),
+ isRefreshOn: defaultAutoRefreshSetting.on,
+ });
const { loading: isLoadingRulesStatuses, rulesStatuses } = useRulesStatuses(rules);
const history = useHistory();
const [, dispatchToaster] = useStateToaster();
@@ -159,6 +180,26 @@ export const AllRules = React.memo(
});
}, []);
+ const setShowIdleModal = useCallback((show: boolean) => {
+ dispatch({
+ type: 'setShowIdleModal',
+ show,
+ });
+ }, []);
+
+ const setLastRefreshDate = useCallback(() => {
+ dispatch({
+ type: 'setLastRefreshDate',
+ });
+ }, []);
+
+ const setAutoRefreshOn = useCallback((on: boolean) => {
+ dispatch({
+ type: 'setAutoRefreshOn',
+ on,
+ });
+ }, []);
+
const [isLoadingRules, , reFetchRulesData] = useRules({
pagination,
filterOptions,
@@ -181,34 +222,25 @@ export const AllRules = React.memo(
rulesNotInstalled,
rulesNotUpdated
);
- const {
- services: {
- application: {
- capabilities: { actions },
- },
- },
- } = useKibana();
const hasActionsPrivileges = useMemo(() => (isBoolean(actions.show) ? actions.show : true), [
actions,
]);
const getBatchItemsPopoverContent = useCallback(
- (closePopover: () => void) => (
-
- ),
+ (closePopover: () => void): JSX.Element[] => {
+ return getBatchItems({
+ closePopover,
+ dispatch,
+ dispatchToaster,
+ hasMlPermissions,
+ hasActionsPrivileges,
+ loadingRuleIds,
+ selectedRuleIds,
+ reFetchRules: reFetchRulesData,
+ rules,
+ });
+ },
[
dispatch,
dispatchToaster,
@@ -328,6 +360,94 @@ export const AllRules = React.memo(
return false;
}, [loadingRuleIds, loadingRulesAction]);
+ const handleRefreshData = useCallback((): void => {
+ if (reFetchRulesData != null && !isLoadingAnActionOnRule) {
+ reFetchRulesData(true);
+ setLastRefreshDate();
+ }
+ }, [reFetchRulesData, isLoadingAnActionOnRule, setLastRefreshDate]);
+
+ const handleResetIdleTimer = useCallback((): void => {
+ if (isRefreshOn) {
+ setShowIdleModal(true);
+ setAutoRefreshOn(false);
+ }
+ }, [setShowIdleModal, setAutoRefreshOn, isRefreshOn]);
+
+ const debounceResetIdleTimer = useMemo(() => {
+ return debounce(defaultAutoRefreshSetting.idleTimeout, handleResetIdleTimer);
+ }, [handleResetIdleTimer, defaultAutoRefreshSetting.idleTimeout]);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ if (isRefreshOn) {
+ handleRefreshData();
+ }
+ }, defaultAutoRefreshSetting.value);
+
+ return () => {
+ clearInterval(interval);
+ };
+ }, [isRefreshOn, handleRefreshData, defaultAutoRefreshSetting.value]);
+
+ const handleIdleModalContinue = useCallback((): void => {
+ setShowIdleModal(false);
+ handleRefreshData();
+ setAutoRefreshOn(true);
+ }, [setShowIdleModal, setAutoRefreshOn, handleRefreshData]);
+
+ const handleAutoRefreshSwitch = useCallback(
+ (refreshOn: boolean) => {
+ if (refreshOn) {
+ handleRefreshData();
+ }
+ setAutoRefreshOn(refreshOn);
+ },
+ [setAutoRefreshOn, handleRefreshData]
+ );
+
+ useEffect(() => {
+ debounceResetIdleTimer();
+
+ window.addEventListener('mousemove', debounceResetIdleTimer, { passive: true });
+ window.addEventListener('keydown', debounceResetIdleTimer);
+
+ return () => {
+ window.removeEventListener('mousemove', debounceResetIdleTimer);
+ window.removeEventListener('keydown', debounceResetIdleTimer);
+ };
+ }, [handleResetIdleTimer, debounceResetIdleTimer]);
+
+ const shouldShowRulesTable = useMemo(
+ (): boolean => showRulesTable({ rulesCustomInstalled, rulesInstalled }) && !initLoading,
+ [initLoading, rulesCustomInstalled, rulesInstalled]
+ );
+
+ const shouldShowPrepackagedRulesPrompt = useMemo(
+ (): boolean =>
+ rulesCustomInstalled != null &&
+ rulesCustomInstalled === 0 &&
+ prePackagedRuleStatus === 'ruleNotInstalled' &&
+ !initLoading,
+ [initLoading, prePackagedRuleStatus, rulesCustomInstalled]
+ );
+
+ const handleGenericDownloaderSuccess = useCallback(
+ (exportCount) => {
+ dispatch({ type: 'loadingRuleIds', ids: [], actionType: null });
+ dispatchToaster({
+ type: 'addToaster',
+ toast: {
+ id: uuid.v4(),
+ title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount),
+ color: 'success',
+ iconType: 'check',
+ },
+ });
+ },
+ [dispatchToaster]
+ );
+
const tabs = useMemo(
() => (
@@ -353,27 +473,37 @@ export const AllRules = React.memo(
{
- dispatch({ type: 'loadingRuleIds', ids: [], actionType: null });
- dispatchToaster({
- type: 'addToaster',
- toast: {
- id: uuid.v4(),
- title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount),
- color: 'success',
- iconType: 'check',
- },
- });
- }}
+ onExportSuccess={handleGenericDownloaderSuccess}
exportSelectedData={exportRules}
/>
{tabs}
-
+
<>
-
+ {(isLoadingRules || isLoadingRulesStatuses) && (
+
+ )}
+
+ }
+ >
(
/>
- {(loading || isLoadingRules || isLoadingAnActionOnRule || isLoadingRulesStatuses) &&
- !initLoading && (
-
- )}
- {rulesCustomInstalled != null &&
- rulesCustomInstalled === 0 &&
- prePackagedRuleStatus === 'ruleNotInstalled' &&
- !initLoading && (
-
- )}
+ {isLoadingAnActionOnRule && !initLoading && (
+
+ )}
+ {shouldShowPrepackagedRulesPrompt && (
+
+ )}
{initLoading && (
)}
- {showRulesTable({ rulesCustomInstalled, rulesInstalled }) && !initLoading && (
+ {showIdleModal && (
+
+
+ {i18n.REFRESH_PROMPT_BODY}
+
+
+ )}
+ {shouldShowRulesTable && (
<>
-
-
-
-
- {i18n.SHOWING_RULES(pagination.total ?? 0)}
-
-
-
-
- {i18n.SELECTED_RULES(selectedRuleIds.length)}
- {!hasNoPermissions && (
-
- {i18n.BATCH_ACTIONS}
-
- )}
- reFetchRulesData(true)}
- >
- {i18n.REFRESH}
-
-
-
-
+
{
+ let reducer: (state: State, action: Action) => State;
+
+ beforeEach(() => {
+ jest.useFakeTimers();
+ jest
+ .spyOn(global.Date, 'now')
+ .mockImplementationOnce(() => new Date('2020-10-31T11:01:58.135Z').valueOf());
+ reducer = allRulesReducer({ current: undefined });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('#exportRuleIds', () => {
+ test('should update state with rules to be exported', () => {
+ const { loadingRuleIds, loadingRulesAction, exportRuleIds } = reducer(initialState, {
+ type: 'exportRuleIds',
+ ids: ['123', '456'],
+ });
+
+ expect(loadingRuleIds).toEqual(['123', '456']);
+ expect(exportRuleIds).toEqual(['123', '456']);
+ expect(loadingRulesAction).toEqual('export');
+ });
+ });
+
+ describe('#loadingRuleIds', () => {
+ test('should update state with rule ids with a pending action', () => {
+ const { loadingRuleIds, loadingRulesAction } = reducer(initialState, {
+ type: 'loadingRuleIds',
+ ids: ['123', '456'],
+ actionType: 'enable',
+ });
+
+ expect(loadingRuleIds).toEqual(['123', '456']);
+ expect(loadingRulesAction).toEqual('enable');
+ });
+
+ test('should update loadingIds to empty array if action is null', () => {
+ const { loadingRuleIds, loadingRulesAction } = reducer(initialState, {
+ type: 'loadingRuleIds',
+ ids: ['123', '456'],
+ actionType: null,
+ });
+
+ expect(loadingRuleIds).toEqual([]);
+ expect(loadingRulesAction).toBeNull();
+ });
+
+ test('should append rule ids to any existing loading ids', () => {
+ const { loadingRuleIds, loadingRulesAction } = reducer(
+ { ...initialState, loadingRuleIds: ['abc'] },
+ {
+ type: 'loadingRuleIds',
+ ids: ['123', '456'],
+ actionType: 'duplicate',
+ }
+ );
+
+ expect(loadingRuleIds).toEqual(['abc', '123', '456']);
+ expect(loadingRulesAction).toEqual('duplicate');
+ });
+ });
+
+ describe('#selectedRuleIds', () => {
+ test('should update state with selected rule ids', () => {
+ const { selectedRuleIds } = reducer(initialState, {
+ type: 'selectedRuleIds',
+ ids: ['123', '456'],
+ });
+
+ expect(selectedRuleIds).toEqual(['123', '456']);
+ });
+ });
+
+ describe('#setRules', () => {
+ test('should update rules and reset loading/selected rule ids', () => {
+ const { selectedRuleIds, loadingRuleIds, loadingRulesAction, pagination, rules } = reducer(
+ initialState,
+ {
+ type: 'setRules',
+ rules: [mockRule('someRuleId')],
+ pagination: {
+ page: 1,
+ perPage: 20,
+ total: 0,
+ },
+ }
+ );
+
+ expect(rules).toEqual([mockRule('someRuleId')]);
+ expect(selectedRuleIds).toEqual([]);
+ expect(loadingRuleIds).toEqual([]);
+ expect(loadingRulesAction).toBeNull();
+ expect(pagination).toEqual({
+ page: 1,
+ perPage: 20,
+ total: 0,
+ });
+ });
+ });
+
+ describe('#updateRules', () => {
+ test('should return existing and new rules', () => {
+ const existingRule = { ...mockRule('123'), rule_id: 'rule-123' };
+ const { rules, loadingRulesAction } = reducer(
+ { ...initialState, rules: [existingRule] },
+ {
+ type: 'updateRules',
+ rules: [mockRule('someRuleId')],
+ }
+ );
+
+ expect(rules).toEqual([existingRule, mockRule('someRuleId')]);
+ expect(loadingRulesAction).toBeNull();
+ });
+
+ test('should return updated rule', () => {
+ const updatedRule = { ...mockRule('someRuleId'), description: 'updated rule' };
+ const { rules, loadingRulesAction } = reducer(
+ { ...initialState, rules: [mockRule('someRuleId')] },
+ {
+ type: 'updateRules',
+ rules: [updatedRule],
+ }
+ );
+
+ expect(rules).toEqual([updatedRule]);
+ expect(loadingRulesAction).toBeNull();
+ });
+
+ test('should return updated existing loading rule ids', () => {
+ const existingRule = { ...mockRule('someRuleId'), id: '123', rule_id: 'rule-123' };
+ const { loadingRuleIds, loadingRulesAction } = reducer(
+ {
+ ...initialState,
+ rules: [existingRule],
+ loadingRuleIds: ['123'],
+ loadingRulesAction: 'enable',
+ },
+ {
+ type: 'updateRules',
+ rules: [mockRule('someRuleId')],
+ }
+ );
+
+ expect(loadingRuleIds).toEqual(['123']);
+ expect(loadingRulesAction).toEqual('enable');
+ });
+ });
+
+ describe('#updateFilterOptions', () => {
+ test('should return existing and new rules', () => {
+ const paginationMock: PaginationOptions = {
+ page: 1,
+ perPage: 20,
+ total: 0,
+ };
+ const filterMock: FilterOptions = {
+ filter: 'host.name:*',
+ sortField: 'enabled',
+ sortOrder: 'desc',
+ };
+ const { filterOptions, pagination } = reducer(initialState, {
+ type: 'updateFilterOptions',
+ filterOptions: filterMock,
+ pagination: paginationMock,
+ });
+
+ expect(filterOptions).toEqual(filterMock);
+ expect(pagination).toEqual(paginationMock);
+ });
+ });
+
+ describe('#failure', () => {
+ test('should reset rules value to empty array', () => {
+ const { rules } = reducer(initialState, {
+ type: 'failure',
+ });
+
+ expect(rules).toEqual([]);
+ });
+ });
+
+ describe('#setLastRefreshDate', () => {
+ test('should update last refresh date with current date', () => {
+ const { lastUpdated } = reducer(initialState, {
+ type: 'setLastRefreshDate',
+ });
+
+ expect(lastUpdated).toEqual(1604142118135);
+ });
+ });
+
+ describe('#setShowIdleModal', () => {
+ test('should hide idle modal and restart refresh if "show" is false', () => {
+ const { showIdleModal, isRefreshOn } = reducer(initialState, {
+ type: 'setShowIdleModal',
+ show: false,
+ });
+
+ expect(showIdleModal).toBeFalsy();
+ expect(isRefreshOn).toBeTruthy();
+ });
+
+ test('should show idle modal and pause refresh if "show" is true', () => {
+ const { showIdleModal, isRefreshOn } = reducer(initialState, {
+ type: 'setShowIdleModal',
+ show: true,
+ });
+
+ expect(showIdleModal).toBeTruthy();
+ expect(isRefreshOn).toBeFalsy();
+ });
+ });
+
+ describe('#setAutoRefreshOn', () => {
+ test('should pause auto refresh if "paused" is true', () => {
+ const { isRefreshOn } = reducer(initialState, {
+ type: 'setAutoRefreshOn',
+ on: true,
+ });
+
+ expect(isRefreshOn).toBeTruthy();
+ });
+
+ test('should resume auto refresh if "paused" is false', () => {
+ const { isRefreshOn } = reducer(initialState, {
+ type: 'setAutoRefreshOn',
+ on: false,
+ });
+
+ expect(isRefreshOn).toBeFalsy();
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/reducer.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/reducer.ts
index ff9b41bed06f5..d603e5791f5ce 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/reducer.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/reducer.ts
@@ -20,6 +20,9 @@ export interface State {
pagination: PaginationOptions;
rules: Rule[];
selectedRuleIds: string[];
+ lastUpdated: number;
+ showIdleModal: boolean;
+ isRefreshOn: boolean;
}
export type Action =
@@ -33,7 +36,10 @@ export type Action =
filterOptions: Partial;
pagination: Partial;
}
- | { type: 'failure' };
+ | { type: 'failure' }
+ | { type: 'setLastRefreshDate' }
+ | { type: 'setShowIdleModal'; show: boolean }
+ | { type: 'setAutoRefreshOn'; on: boolean };
export const allRulesReducer = (
tableRef: React.MutableRefObject | undefined>
@@ -85,27 +91,24 @@ export const allRulesReducer = (
};
}
case 'updateRules': {
- if (state.rules != null) {
- const ruleIds = state.rules.map((r) => r.id);
- const updatedRules = action.rules.reduce((rules, updatedRule) => {
- let newRules = rules;
- if (ruleIds.includes(updatedRule.id)) {
- newRules = newRules.map((r) => (updatedRule.id === r.id ? updatedRule : r));
- } else {
- newRules = [...newRules, updatedRule];
- }
- return newRules;
- }, state.rules);
- const updatedRuleIds = action.rules.map((r) => r.id);
- const newLoadingRuleIds = state.loadingRuleIds.filter((id) => !updatedRuleIds.includes(id));
- return {
- ...state,
- rules: updatedRules,
- loadingRuleIds: newLoadingRuleIds,
- loadingRulesAction: newLoadingRuleIds.length === 0 ? null : state.loadingRulesAction,
- };
- }
- return state;
+ const ruleIds = state.rules.map((r) => r.id);
+ const updatedRules = action.rules.reduce((rules, updatedRule) => {
+ let newRules = rules;
+ if (ruleIds.includes(updatedRule.id)) {
+ newRules = newRules.map((r) => (updatedRule.id === r.id ? updatedRule : r));
+ } else {
+ newRules = [...newRules, updatedRule];
+ }
+ return newRules;
+ }, state.rules);
+ const updatedRuleIds = action.rules.map((r) => r.id);
+ const newLoadingRuleIds = state.loadingRuleIds.filter((id) => !updatedRuleIds.includes(id));
+ return {
+ ...state,
+ rules: updatedRules,
+ loadingRuleIds: newLoadingRuleIds,
+ loadingRulesAction: newLoadingRuleIds.length === 0 ? null : state.loadingRulesAction,
+ };
}
case 'updateFilterOptions': {
return {
@@ -126,6 +129,25 @@ export const allRulesReducer = (
rules: [],
};
}
+ case 'setLastRefreshDate': {
+ return {
+ ...state,
+ lastUpdated: Date.now(),
+ };
+ }
+ case 'setShowIdleModal': {
+ return {
+ ...state,
+ showIdleModal: action.show,
+ isRefreshOn: !action.show,
+ };
+ }
+ case 'setAutoRefreshOn': {
+ return {
+ ...state,
+ isRefreshOn: action.on,
+ };
+ }
default:
return state;
}
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx
index 92f69d79110d2..a8205c24dca65 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx
@@ -5,16 +5,47 @@
*/
import React from 'react';
-import { shallow } from 'enzyme';
+import { mount } from 'enzyme';
+import { act } from '@testing-library/react';
import { RulesTableFilters } from './rules_table_filters';
describe('RulesTableFilters', () => {
- it('renders correctly', () => {
- const wrapper = shallow(
-
- );
+ it('renders no numbers next to rule type button filter if none exist', async () => {
+ await act(async () => {
+ const wrapper = mount(
+
+ );
- expect(wrapper.find('[data-test-subj="show-elastic-rules-filter-button"]')).toHaveLength(1);
+ expect(wrapper.find('[data-test-subj="showElasticRulesFilterButton"]').at(0).text()).toEqual(
+ 'Elastic rules'
+ );
+ expect(wrapper.find('[data-test-subj="showCustomRulesFilterButton"]').at(0).text()).toEqual(
+ 'Custom rules'
+ );
+ });
+ });
+
+ it('renders number of custom and prepackaged rules', async () => {
+ await act(async () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="showElasticRulesFilterButton"]').at(0).text()).toEqual(
+ 'Elastic rules (9)'
+ );
+ expect(wrapper.find('[data-test-subj="showCustomRulesFilterButton"]').at(0).text()).toEqual(
+ 'Custom rules (10)'
+ );
+ });
});
});
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx
index 0f201fcbaa441..0b83a8437cc1a 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx
@@ -14,6 +14,7 @@ import {
EuiFlexItem,
} from '@elastic/eui';
import { isEqual } from 'lodash/fp';
+
import * as i18n from '../../translations';
import { FilterOptions } from '../../../../../containers/detection_engine/rules';
@@ -76,7 +77,7 @@ const RulesTableFiltersComponent = ({
return (
-
+
@@ -102,7 +104,7 @@ const RulesTableFiltersComponent = ({
{i18n.ELASTIC_RULES}
@@ -111,7 +113,7 @@ const RulesTableFiltersComponent = ({
<>
{i18n.CUSTOM_RULES}
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.test.tsx
new file mode 100644
index 0000000000000..3d49295bde50a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.test.tsx
@@ -0,0 +1,116 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { mount } from 'enzyme';
+import { ThemeProvider } from 'styled-components';
+import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
+import { waitFor } from '@testing-library/react';
+
+import { AllRulesUtilityBar } from './utility_bar';
+
+describe('AllRules', () => {
+ it('renders AllRulesUtilityBar total rules and selected rules', () => {
+ const wrapper = mount(
+ ({ eui: euiDarkVars, darkMode: true })}>
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="showingRules"]').at(0).text()).toEqual('Showing 4 rules');
+ expect(wrapper.find('[data-test-subj="selectedRules"]').at(0).text()).toEqual(
+ 'Selected 1 rule'
+ );
+ });
+
+ it('renders utility actions if user has permissions', () => {
+ const wrapper = mount(
+ ({ eui: euiDarkVars, darkMode: true })}>
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="bulkActions"]').exists()).toBeTruthy();
+ });
+
+ it('renders no utility actions if user has no permissions', () => {
+ const wrapper = mount(
+ ({ eui: euiDarkVars, darkMode: true })}>
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="bulkActions"]').exists()).toBeFalsy();
+ });
+
+ it('invokes refresh on refresh action click', () => {
+ const mockRefresh = jest.fn();
+ const wrapper = mount(
+ ({ eui: euiDarkVars, darkMode: true })}>
+
+
+ );
+
+ wrapper.find('[data-test-subj="refreshRulesAction"] button').at(0).simulate('click');
+
+ expect(mockRefresh).toHaveBeenCalled();
+ });
+
+ it('invokes onRefreshSwitch when auto refresh switch is clicked', async () => {
+ const mockSwitch = jest.fn();
+ const wrapper = mount(
+ ({ eui: euiDarkVars, darkMode: true })}>
+
+
+ );
+
+ await waitFor(() => {
+ wrapper.find('[data-test-subj="refreshSettings"] button').first().simulate('click');
+ wrapper.find('[data-test-subj="refreshSettingsSwitch"] button').first().simulate('click');
+ expect(mockSwitch).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx
new file mode 100644
index 0000000000000..3553dcc9b7c14
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx
@@ -0,0 +1,118 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiContextMenuPanel, EuiSwitch, EuiSwitchEvent } from '@elastic/eui';
+import React, { useCallback } from 'react';
+
+import {
+ UtilityBar,
+ UtilityBarAction,
+ UtilityBarGroup,
+ UtilityBarSection,
+ UtilityBarText,
+} from '../../../../../common/components/utility_bar';
+import * as i18n from '../translations';
+
+interface AllRulesUtilityBarProps {
+ userHasNoPermissions: boolean;
+ numberSelectedRules: number;
+ paginationTotal: number;
+ isAutoRefreshOn: boolean;
+ onRefresh: (refreshRule: boolean) => void;
+ onGetBatchItemsPopoverContent: (closePopover: () => void) => JSX.Element[];
+ onRefreshSwitch: (checked: boolean) => void;
+}
+
+export const AllRulesUtilityBar = React.memo(
+ ({
+ userHasNoPermissions,
+ onRefresh,
+ paginationTotal,
+ numberSelectedRules,
+ onGetBatchItemsPopoverContent,
+ isAutoRefreshOn,
+ onRefreshSwitch,
+ }) => {
+ const handleGetBatchItemsPopoverContent = useCallback(
+ (closePopover: () => void) => (
+
+ ),
+ [onGetBatchItemsPopoverContent]
+ );
+
+ const handleAutoRefreshSwitch = useCallback(
+ (closePopover: () => void) => (e: EuiSwitchEvent) => {
+ onRefreshSwitch(e.target.checked);
+ closePopover();
+ },
+ [onRefreshSwitch]
+ );
+
+ const handleGetRefreshSettingsPopoverContent = useCallback(
+ (closePopover: () => void) => (
+ ,
+ ]}
+ />
+ ),
+ [isAutoRefreshOn, handleAutoRefreshSwitch]
+ );
+
+ return (
+
+
+
+
+ {i18n.SHOWING_RULES(paginationTotal)}
+
+
+
+
+
+ {i18n.SELECTED_RULES(numberSelectedRules)}
+
+ {!userHasNoPermissions && (
+
+ {i18n.BATCH_ACTIONS}
+
+ )}
+
+ {i18n.REFRESH}
+
+
+ {i18n.REFRESH_RULE_POPOVER_LABEL}
+
+
+
+
+ );
+ }
+);
+
+AllRulesUtilityBar.displayName = 'AllRulesUtilityBar';
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts
index d20b97a98fbf5..38fb457185b67 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts
@@ -554,3 +554,38 @@ export const IMPORT_FAILED_DETAILED = (ruleId: string, statusCode: number, messa
defaultMessage: 'Rule ID: {ruleId}\n Status Code: {statusCode}\n Message: {message}',
}
);
+
+export const REFRESH_PROMPT_TITLE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.refreshPromptTitle',
+ {
+ defaultMessage: 'Are you still there?',
+ }
+);
+
+export const REFRESH_PROMPT_CONFIRM = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.refreshPromptConfirm',
+ {
+ defaultMessage: 'Continue',
+ }
+);
+
+export const REFRESH_PROMPT_BODY = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.refreshPromptBody',
+ {
+ defaultMessage: 'Rule auto-refresh has been paused. Click "Continue" to resume.',
+ }
+);
+
+export const REFRESH_RULE_POPOVER_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.refreshRulePopoverDescription',
+ {
+ defaultMessage: 'Automatically refresh table',
+ }
+);
+
+export const REFRESH_RULE_POPOVER_LABEL = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.refreshRulePopoverLabel',
+ {
+ defaultMessage: 'Refresh settings',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx
index 4119127d5a108..f56d7d90cf2df 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx
@@ -25,10 +25,10 @@ import styled from 'styled-components';
import { LoadingPanel } from '../../loading';
import { OnChangeItemsPerPage, OnChangePage } from '../events';
-import { LastUpdatedAt } from './last_updated';
import * as i18n from './translations';
import { useEventDetailsWidthContext } from '../../../../common/components/events_viewer/event_details_width_context';
import { useManageTimeline } from '../../manage_timeline';
+import { LastUpdatedAt } from '../../../../common/components/last_updated';
export const isCompactFooter = (width: number): boolean => width < 600;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/last_updated.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/last_updated.tsx
deleted file mode 100644
index 06ece50690c09..0000000000000
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/last_updated.tsx
+++ /dev/null
@@ -1,68 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { EuiIcon, EuiText, EuiToolTip } from '@elastic/eui';
-import { FormattedRelative } from '@kbn/i18n/react';
-import React, { useEffect, useState } from 'react';
-
-import * as i18n from './translations';
-
-interface LastUpdatedAtProps {
- compact?: boolean;
- updatedAt: number;
-}
-
-export const Updated = React.memo<{ date: number; prefix: string; updatedAt: number }>(
- ({ date, prefix, updatedAt }) => (
- <>
- {prefix}
- {
-
- }
- >
- )
-);
-
-Updated.displayName = 'Updated';
-
-const prefix = ` ${i18n.UPDATED} `;
-
-export const LastUpdatedAt = React.memo(({ compact = false, updatedAt }) => {
- const [date, setDate] = useState(Date.now());
-
- function tick() {
- setDate(Date.now());
- }
-
- useEffect(() => {
- const timerID = setInterval(() => tick(), 10000);
- return () => {
- clearInterval(timerID);
- };
- }, []);
-
- return (
-
-
- >
- }
- >
-
-
- {!compact ? : null}
-
-
- );
-});
-
-LastUpdatedAt.displayName = 'LastUpdatedAt';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/translations.ts
index f581d0757bc3c..016406d6bd061 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/translations.ts
@@ -36,10 +36,6 @@ export const TOTAL_COUNT_OF_EVENTS = i18n.translate(
}
);
-export const UPDATED = i18n.translate('xpack.securitySolution.footer.updated', {
- defaultMessage: 'Updated',
-});
-
export const AUTO_REFRESH_ACTIVE = i18n.translate(
'xpack.securitySolution.footer.autoRefreshActiveDescription',
{
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts
index 94b820344b37c..773e84d9c88fc 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts
@@ -407,6 +407,8 @@ export const getResult = (): RuleAlertType => ({
note: '# Investigative notes',
version: 1,
exceptionsList: getListArrayMock(),
+ concurrentSearches: undefined,
+ itemsPerSearch: undefined,
},
createdAt: new Date('2019-12-13T16:40:33.400Z'),
updatedAt: new Date('2019-12-13T16:40:33.400Z'),
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts
index 8c7a19869ce18..aa409580df965 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts
@@ -102,6 +102,8 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) =>
threat_mapping: threatMapping,
threat_query: threatQuery,
threat_language: threatLanguage,
+ concurrent_searches: concurrentSearches,
+ items_per_search: itemsPerSearch,
threshold,
throttle,
timestamp_override: timestampOverride,
@@ -193,6 +195,8 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) =>
threatQuery,
threatIndex,
threatLanguage,
+ concurrentSearches,
+ itemsPerSearch,
threshold,
timestampOverride,
references,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts
index 6ba7bc78fbded..97c05b4626ddc 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts
@@ -85,6 +85,8 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void
threat_query: threatQuery,
threat_mapping: threatMapping,
threat_language: threatLanguage,
+ concurrent_searches: concurrentSearches,
+ items_per_search: itemsPerSearch,
throttle,
timestamp_override: timestampOverride,
to,
@@ -182,6 +184,8 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void
threatQuery,
threatMapping,
threatLanguage,
+ concurrentSearches,
+ itemsPerSearch,
timestampOverride,
references,
note,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts
index 4d992c6c7029d..4b75127af1bc7 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts
@@ -9,6 +9,7 @@ import { getFindResultStatus, ruleStatusRequest, getResult } from '../__mocks__/
import { serverMock, requestContextMock, requestMock } from '../__mocks__';
import { findRulesStatusesRoute } from './find_rules_status_route';
import { RuleStatusResponse } from '../../rules/types';
+import { AlertExecutionStatusErrorReasons } from '../../../../../../alerts/common';
jest.mock('../../signals/rule_status_service');
@@ -57,7 +58,7 @@ describe('find_statuses', () => {
status: 'error',
lastExecutionDate: failingExecutionRule.executionStatus.lastExecutionDate,
error: {
- reason: 'read',
+ reason: AlertExecutionStatusErrorReasons.Read,
message: 'oops',
},
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts
index 7cbcf25590921..688036c59c8ff 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts
@@ -169,6 +169,8 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP
threat_query: threatQuery,
threat_mapping: threatMapping,
threat_language: threatLanguage,
+ concurrent_searches: concurrentSearches,
+ items_per_search: itemsPerSearch,
threshold,
timestamp_override: timestampOverride,
to,
@@ -235,6 +237,8 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP
threatQuery,
threatMapping,
threatLanguage,
+ concurrentSearches,
+ itemsPerSearch,
timestampOverride,
references,
note,
@@ -284,6 +288,8 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP
threatQuery,
threatMapping,
threatLanguage,
+ concurrentSearches,
+ itemsPerSearch,
references,
note,
version,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts
index 4c310774ec72b..7dfb4daa1a0a2 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts
@@ -97,6 +97,8 @@ export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) =>
threat_query: threatQuery,
threat_mapping: threatMapping,
threat_language: threatLanguage,
+ concurrent_searches: concurrentSearches,
+ items_per_search: itemsPerSearch,
timestamp_override: timestampOverride,
throttle,
references,
@@ -162,6 +164,8 @@ export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) =>
threatQuery,
threatMapping,
threatLanguage,
+ concurrentSearches,
+ itemsPerSearch,
timestampOverride,
references,
note,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts
index dbdcd9844c0a7..aadb13ef54e72 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts
@@ -83,6 +83,8 @@ export const patchRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => {
threat_query: threatQuery,
threat_mapping: threatMapping,
threat_language: threatLanguage,
+ concurrent_searches: concurrentSearches,
+ items_per_search: itemsPerSearch,
timestamp_override: timestampOverride,
throttle,
references,
@@ -161,6 +163,8 @@ export const patchRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => {
threatQuery,
threatMapping,
threatLanguage,
+ concurrentSearches,
+ itemsPerSearch,
timestampOverride,
references,
note,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts
index b93b3f319193f..f4a31c2bb456d 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts
@@ -102,6 +102,8 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) =>
threat_query: threatQuery,
threat_mapping: threatMapping,
threat_language: threatLanguage,
+ concurrent_searches: concurrentSearches,
+ items_per_search: itemsPerSearch,
throttle,
timestamp_override: timestampOverride,
references,
@@ -174,6 +176,8 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) =>
threatQuery,
threatMapping,
threatLanguage,
+ concurrentSearches,
+ itemsPerSearch,
timestampOverride,
references,
note,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts
index ea19fed5d6668..7ad525b67f7aa 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts
@@ -86,6 +86,8 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => {
threat_query: threatQuery,
threat_mapping: threatMapping,
threat_language: threatLanguage,
+ concurrent_searches: concurrentSearches,
+ items_per_search: itemsPerSearch,
throttle,
timestamp_override: timestampOverride,
references,
@@ -163,6 +165,8 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => {
threatQuery,
threatMapping,
threatLanguage,
+ concurrentSearches,
+ itemsPerSearch,
timestampOverride,
references,
note,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts
index fb4ba855f6536..7360dc77aac22 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts
@@ -151,6 +151,8 @@ export const transformAlertToRule = (
threat_query: alert.params.threatQuery,
threat_mapping: alert.params.threatMapping,
threat_language: alert.params.threatLanguage,
+ concurrent_searches: alert.params.concurrentSearches,
+ items_per_search: alert.params.itemsPerSearch,
throttle: ruleActions?.ruleThrottle || 'no_actions',
timestamp_override: alert.params.timestampOverride,
note: alert.params.note,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts
index 25e47b38e8a56..b613061ac85f2 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts
@@ -27,6 +27,7 @@ import {
import { responseMock } from './__mocks__';
import { exampleRuleStatus, exampleFindRuleStatusResponse } from '../signals/__mocks__/es_results';
import { getResult } from './__mocks__/request_responses';
+import { AlertExecutionStatusErrorReasons } from '../../../../../alerts/common';
let alertsClient: ReturnType;
@@ -464,7 +465,7 @@ describe('utils', () => {
status: 'error',
lastExecutionDate: foundRule.executionStatus.lastExecutionDate,
error: {
- reason: 'read',
+ reason: AlertExecutionStatusErrorReasons.Read,
message: 'oops',
},
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts
index 271b1043ea568..68199c531a2fe 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts
@@ -43,6 +43,8 @@ export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({
threatFilters: undefined,
threatMapping: undefined,
threatLanguage: undefined,
+ concurrentSearches: undefined,
+ itemsPerSearch: undefined,
threatQuery: undefined,
threatIndex: undefined,
threshold: undefined,
@@ -94,6 +96,8 @@ export const getCreateMlRulesOptionsMock = (): CreateRulesOptions => ({
threatMapping: undefined,
threatQuery: undefined,
threatLanguage: undefined,
+ concurrentSearches: undefined,
+ itemsPerSearch: undefined,
threshold: undefined,
timestampOverride: undefined,
to: 'now',
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts
index 776882d0f8494..3c814ce7e6606 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts
@@ -46,6 +46,8 @@ export const createRules = async ({
threatFilters,
threatIndex,
threatLanguage,
+ concurrentSearches,
+ itemsPerSearch,
threatQuery,
threatMapping,
threshold,
@@ -96,6 +98,8 @@ export const createRules = async ({
threatFilters,
threatIndex,
threatQuery,
+ concurrentSearches,
+ itemsPerSearch,
threatMapping,
threatLanguage,
timestampOverride,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts
index 0a43c652234d0..4c01318f02cde 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts
@@ -51,6 +51,8 @@ export const installPrepackagedRules = (
threat_filters: threatFilters,
threat_mapping: threatMapping,
threat_language: threatLanguage,
+ concurrent_searches: concurrentSearches,
+ items_per_search: itemsPerSearch,
threat_query: threatQuery,
threat_index: threatIndex,
threshold,
@@ -103,6 +105,8 @@ export const installPrepackagedRules = (
threatFilters,
threatMapping,
threatLanguage,
+ concurrentSearches,
+ itemsPerSearch,
threatQuery,
threatIndex,
threshold,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts
index ef7cd35f28f1b..60f1d599470e3 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts
@@ -154,6 +154,8 @@ export const getPatchRulesOptionsMock = (): PatchRulesOptions => ({
threatQuery: undefined,
threatMapping: undefined,
threatLanguage: undefined,
+ concurrentSearches: undefined,
+ itemsPerSearch: undefined,
timestampOverride: undefined,
to: 'now',
type: 'query',
@@ -203,6 +205,8 @@ export const getPatchMlRulesOptionsMock = (): PatchRulesOptions => ({
threatQuery: undefined,
threatMapping: undefined,
threatLanguage: undefined,
+ concurrentSearches: undefined,
+ itemsPerSearch: undefined,
timestampOverride: undefined,
to: 'now',
type: 'machine_learning',
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts
index 1982dcf9dd9b6..22b2593283696 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts
@@ -49,6 +49,8 @@ export const patchRules = async ({
threatQuery,
threatMapping,
threatLanguage,
+ concurrentSearches,
+ itemsPerSearch,
timestampOverride,
to,
type,
@@ -97,6 +99,8 @@ export const patchRules = async ({
threatQuery,
threatMapping,
threatLanguage,
+ concurrentSearches,
+ itemsPerSearch,
timestampOverride,
to,
type,
@@ -141,6 +145,8 @@ export const patchRules = async ({
threatQuery,
threatMapping,
threatLanguage,
+ concurrentSearches,
+ itemsPerSearch,
timestampOverride,
to,
type,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
index fb4763a982f43..f6ab3fb0c3ed2 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
@@ -92,6 +92,8 @@ import {
ThreatMappingOrUndefined,
ThreatFiltersOrUndefined,
ThreatLanguageOrUndefined,
+ ConcurrentSearchesOrUndefined,
+ ItemsPerSearchOrUndefined,
} from '../../../../common/detection_engine/schemas/types/threat_mapping';
import { AlertsClient, PartialAlert } from '../../../../../alerts/server';
@@ -234,6 +236,8 @@ export interface CreateRulesOptions {
threatIndex: ThreatIndexOrUndefined;
threatQuery: ThreatQueryOrUndefined;
threatMapping: ThreatMappingOrUndefined;
+ concurrentSearches: ConcurrentSearchesOrUndefined;
+ itemsPerSearch: ItemsPerSearchOrUndefined;
threatLanguage: ThreatLanguageOrUndefined;
timestampOverride: TimestampOverrideOrUndefined;
to: To;
@@ -284,6 +288,8 @@ export interface UpdateRulesOptions {
threatIndex: ThreatIndexOrUndefined;
threatQuery: ThreatQueryOrUndefined;
threatMapping: ThreatMappingOrUndefined;
+ itemsPerSearch: ItemsPerSearchOrUndefined;
+ concurrentSearches: ConcurrentSearchesOrUndefined;
threatLanguage: ThreatLanguageOrUndefined;
timestampOverride: TimestampOverrideOrUndefined;
to: To;
@@ -327,6 +333,8 @@ export interface PatchRulesOptions {
severityMapping: SeverityMappingOrUndefined;
tags: TagsOrUndefined;
threat: ThreatOrUndefined;
+ itemsPerSearch: ItemsPerSearchOrUndefined;
+ concurrentSearches: ConcurrentSearchesOrUndefined;
threshold: ThresholdOrUndefined;
threatFilters: ThreatFiltersOrUndefined;
threatIndex: ThreatIndexOrUndefined;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts
index c685c4198c119..3d4b27b74c0af 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts
@@ -52,6 +52,8 @@ export const updatePrepackagedRules = async (
threat_query: threatQuery,
threat_mapping: threatMapping,
threat_language: threatLanguage,
+ concurrent_searches: concurrentSearches,
+ items_per_search: itemsPerSearch,
timestamp_override: timestampOverride,
references,
version,
@@ -107,6 +109,8 @@ export const updatePrepackagedRules = async (
threatQuery,
threatMapping,
threatLanguage,
+ concurrentSearches,
+ itemsPerSearch,
references,
version,
note,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts
index a33651580ef22..34be0f6ad843d 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts
@@ -49,6 +49,8 @@ export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({
threatMapping: undefined,
threatLanguage: undefined,
timestampOverride: undefined,
+ concurrentSearches: undefined,
+ itemsPerSearch: undefined,
to: 'now',
type: 'query',
references: ['http://www.example.com'],
@@ -99,6 +101,8 @@ export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({
threatMapping: undefined,
threatLanguage: undefined,
timestampOverride: undefined,
+ concurrentSearches: undefined,
+ itemsPerSearch: undefined,
to: 'now',
type: 'machine_learning',
references: ['http://www.example.com'],
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts
index 3da921ed47f26..5168affca5c62 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts
@@ -50,6 +50,8 @@ export const updateRules = async ({
threatQuery,
threatMapping,
threatLanguage,
+ concurrentSearches,
+ itemsPerSearch,
timestampOverride,
to,
type,
@@ -99,6 +101,8 @@ export const updateRules = async ({
threatQuery,
threatMapping,
threatLanguage,
+ concurrentSearches,
+ itemsPerSearch,
timestampOverride,
to,
type,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts
index 654383ff97c7a..8555af424ecd7 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts
@@ -60,6 +60,8 @@ describe('utils', () => {
threatQuery: undefined,
threatMapping: undefined,
threatLanguage: undefined,
+ concurrentSearches: undefined,
+ itemsPerSearch: undefined,
to: undefined,
timestampOverride: undefined,
type: undefined,
@@ -108,6 +110,8 @@ describe('utils', () => {
threatQuery: undefined,
threatMapping: undefined,
threatLanguage: undefined,
+ concurrentSearches: undefined,
+ itemsPerSearch: undefined,
to: undefined,
timestampOverride: undefined,
type: undefined,
@@ -158,6 +162,8 @@ describe('utils', () => {
threatLanguage: undefined,
to: undefined,
timestampOverride: undefined,
+ concurrentSearches: undefined,
+ itemsPerSearch: undefined,
type: undefined,
references: undefined,
version: undefined,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts
index a9a100543b528..83d9e3fd3e59f 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts
@@ -43,6 +43,8 @@ import {
} from '../../../../common/detection_engine/schemas/common/schemas';
import { PartialFilter } from '../types';
import {
+ ConcurrentSearchesOrUndefined,
+ ItemsPerSearchOrUndefined,
ListArrayOrUndefined,
ThreatFiltersOrUndefined,
ThreatIndexOrUndefined,
@@ -98,6 +100,8 @@ export interface UpdateProperties {
threatQuery: ThreatQueryOrUndefined;
threatMapping: ThreatMappingOrUndefined;
threatLanguage: ThreatLanguageOrUndefined;
+ concurrentSearches: ConcurrentSearchesOrUndefined;
+ itemsPerSearch: ItemsPerSearchOrUndefined;
timestampOverride: TimestampOverrideOrUndefined;
to: ToOrUndefined;
type: TypeOrUndefined;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_threat_data.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_threat_data.sh
index 23c1914387c44..4807afd71e8d2 100755
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_threat_data.sh
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_threat_data.sh
@@ -12,7 +12,7 @@ set -e
# Adds port mock data to a threat list for testing.
# Example: ./create_threat_data.sh
-# Example: ./create_threat_data.sh 1000 2000
+# Example: ./create_threat_data.sh 1 500
START=${1:-1}
END=${2:-1000}
@@ -22,7 +22,7 @@ do {
curl -s -k \
-H "Content-Type: application/json" \
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
- -X PUT ${ELASTICSEARCH_URL}/mock-threat-list/_doc/$i \
+ -X PUT ${ELASTICSEARCH_URL}/mock-threat-list-1/_doc/$i \
--data "
{
\"@timestamp\": \"$(date -u +"%Y-%m-%dT%H:%M:%SZ")\",
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_threat_mapping_perf.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_threat_mapping_perf.json
new file mode 100644
index 0000000000000..c573db7fbca35
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_threat_mapping_perf.json
@@ -0,0 +1,32 @@
+{
+ "concurrent_searches": 10,
+ "items_per_search": 10,
+ "index": ["auditbeat-*", "endgame-*", "filebeat-*", "logs-*", "packetbeat-*", "winlogbeat-*"],
+ "name": "Indicator Match Concurrent Searches",
+ "description": "Does 100 Concurrent searches with 10 items per search",
+ "rule_id": "indicator_concurrent_search",
+ "risk_score": 1,
+ "severity": "high",
+ "type": "threat_match",
+ "query": "*:*",
+ "tags": ["concurrent_searches_test", "from_script"],
+ "threat_index": ["mock-threat-list-1"],
+ "threat_language": "kuery",
+ "threat_query": "*:*",
+ "threat_mapping": [
+ {
+ "entries": [
+ {
+ "field": "source.port",
+ "type": "mapping",
+ "value": "source.port"
+ },
+ {
+ "field": "source.ip",
+ "type": "mapping",
+ "value": "source.ip"
+ }
+ ]
+ }
+ ]
+}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
index 4559a658c9583..92e6b9562d970 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
@@ -68,6 +68,8 @@ export const sampleRuleAlertParams = (
threat: undefined,
version: 1,
exceptionsList: getListArrayMock(),
+ concurrentSearches: undefined,
+ itemsPerSearch: undefined,
});
export const sampleRuleSO = (): SavedObject => {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts
index cfe71f66395b0..50e740e81830f 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts
@@ -54,6 +54,8 @@ const signalSchema = schema.object({
threatQuery: schema.maybe(schema.string()),
threatMapping: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))),
threatLanguage: schema.maybe(schema.string()),
+ concurrentSearches: schema.maybe(schema.number()),
+ itemsPerSearch: schema.maybe(schema.number()),
});
/**
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
index 415abc9d995fb..dc68e3949eb36 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
@@ -504,7 +504,7 @@ describe('rules_notification_alert_type', () => {
await alert.executor(payload);
expect(logger.error).toHaveBeenCalled();
expect(logger.error.mock.calls[0][0]).toContain(
- 'An error occurred during rule execution: message: "Threat Match rule is missing threatQuery and/or threatIndex and/or threatMapping: threatQuery: "undefined" threatIndex: "undefined" threatMapping: "undefined"" name: "Detect Root/Admin Users" id: "04128c15-0d1b-4716-a4c5-46997ac7f3bd" rule id: "rule-1" signals index: ".siem-signals"'
+ 'An error occurred during rule execution: message: "Indicator match is missing threatQuery and/or threatIndex and/or threatMapping: threatQuery: "undefined" threatIndex: "undefined" threatMapping: "undefined"" name: "Detect Root/Admin Users" id: "04128c15-0d1b-4716-a4c5-46997ac7f3bd" rule id: "rule-1" signals index: ".siem-signals"'
);
});
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
index a0d5c833b208c..1d2b1c23f868f 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
@@ -119,6 +119,8 @@ export const signalRulesAlertType = ({
timestampOverride,
type,
exceptionsList,
+ concurrentSearches,
+ itemsPerSearch,
} = params;
const searchAfterSize = Math.min(maxSignals, DEFAULT_SEARCH_AFTER_PAGE_SIZE);
@@ -360,7 +362,7 @@ export const signalRulesAlertType = ({
) {
throw new Error(
[
- 'Threat Match rule is missing threatQuery and/or threatIndex and/or threatMapping:',
+ 'Indicator match is missing threatQuery and/or threatIndex and/or threatMapping:',
`threatQuery: "${threatQuery}"`,
`threatIndex: "${threatIndex}"`,
`threatMapping: "${threatMapping}"`,
@@ -403,6 +405,8 @@ export const signalRulesAlertType = ({
threatLanguage,
buildRuleMessage,
threatIndex,
+ concurrentSearches: concurrentSearches ?? 1,
+ itemsPerSearch: itemsPerSearch ?? 9000,
});
} else if (type === 'query' || type === 'saved_query') {
const inputIndex = await getInputIndex(services, version, index);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts
index 85d172b3631a9..8eed838fc9680 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts
@@ -19,28 +19,32 @@ import {
} from './build_threat_mapping_filter';
import {
getThreatMappingMock,
- getThreatListSearchResponseMock,
getThreatListItemMock,
getThreatMappingFilterMock,
getFilterThreatMapping,
getThreatMappingFiltersShouldMock,
getThreatMappingFilterShouldMock,
+ getThreatListSearchResponseMock,
} from './build_threat_mapping_filter.mock';
-import { BooleanFilter } from './types';
+import { BooleanFilter, ThreatListItem } from './types';
describe('build_threat_mapping_filter', () => {
describe('buildThreatMappingFilter', () => {
test('it should throw if given a chunk over 1024 in size', () => {
const threatMapping = getThreatMappingMock();
- const threatList = getThreatListSearchResponseMock();
+ const threatList = getThreatListSearchResponseMock().hits.hits;
expect(() =>
- buildThreatMappingFilter({ threatMapping, threatList, chunkSize: 1025 })
+ buildThreatMappingFilter({
+ threatMapping,
+ threatList,
+ chunkSize: 1025,
+ })
).toThrow('chunk sizes cannot exceed 1024 in size');
});
test('it should NOT throw if given a chunk under 1024 in size', () => {
const threatMapping = getThreatMappingMock();
- const threatList = getThreatListSearchResponseMock();
+ const threatList = getThreatListSearchResponseMock().hits.hits;
expect(() =>
buildThreatMappingFilter({ threatMapping, threatList, chunkSize: 1023 })
).not.toThrow();
@@ -48,30 +52,30 @@ describe('build_threat_mapping_filter', () => {
test('it should create the correct entries when using the default mocks', () => {
const threatMapping = getThreatMappingMock();
- const threatList = getThreatListSearchResponseMock();
+ const threatList = getThreatListSearchResponseMock().hits.hits;
const filter = buildThreatMappingFilter({ threatMapping, threatList });
expect(filter).toEqual(getThreatMappingFilterMock());
});
test('it should not mutate the original threatMapping', () => {
const threatMapping = getThreatMappingMock();
- const threatList = getThreatListSearchResponseMock();
+ const threatList = getThreatListSearchResponseMock().hits.hits;
buildThreatMappingFilter({ threatMapping, threatList });
expect(threatMapping).toEqual(getThreatMappingMock());
});
test('it should not mutate the original threatListItem', () => {
const threatMapping = getThreatMappingMock();
- const threatList = getThreatListSearchResponseMock();
+ const threatList = getThreatListSearchResponseMock().hits.hits;
buildThreatMappingFilter({ threatMapping, threatList });
- expect(threatList).toEqual(getThreatListSearchResponseMock());
+ expect(threatList).toEqual(getThreatListSearchResponseMock().hits.hits);
});
});
describe('filterThreatMapping', () => {
test('it should not remove any entries when using the default mocks', () => {
const threatMapping = getThreatMappingMock();
- const threatListItem = getThreatListItemMock();
+ const threatListItem = getThreatListSearchResponseMock().hits.hits[0];
const item = filterThreatMapping({ threatMapping, threatListItem });
const expected = getFilterThreatMapping();
@@ -80,7 +84,7 @@ describe('build_threat_mapping_filter', () => {
test('it should only give one filtered element if only 1 element is defined', () => {
const [firstElement] = getThreatMappingMock(); // get only the first element
- const threatListItem = getThreatListItemMock();
+ const threatListItem = getThreatListSearchResponseMock().hits.hits[0];
const item = filterThreatMapping({ threatMapping: [firstElement], threatListItem });
const [firstElementFilter] = getFilterThreatMapping(); // get only the first element to compare
@@ -89,7 +93,7 @@ describe('build_threat_mapping_filter', () => {
test('it should not mutate the original threatMapping', () => {
const threatMapping = getThreatMappingMock();
- const threatListItem = getThreatListItemMock();
+ const threatListItem = getThreatListSearchResponseMock().hits.hits[0];
filterThreatMapping({
threatMapping,
@@ -100,13 +104,13 @@ describe('build_threat_mapping_filter', () => {
test('it should not mutate the original threatListItem', () => {
const threatMapping = getThreatMappingMock();
- const threatListItem = getThreatListItemMock();
+ const threatListItem = getThreatListSearchResponseMock().hits.hits[0];
filterThreatMapping({
threatMapping,
threatListItem,
});
- expect(threatListItem).toEqual(getThreatListItemMock());
+ expect(threatListItem).toEqual(getThreatListSearchResponseMock().hits.hits[0]);
});
test('it should remove the entire "AND" clause if one of the pieces of data is missing from the list', () => {
@@ -166,9 +170,11 @@ describe('build_threat_mapping_filter', () => {
},
],
threatListItem: {
- '@timestamp': '2020-09-09T21:59:13Z',
- host: {
- name: 'host-1',
+ _source: {
+ '@timestamp': '2020-09-09T21:59:13Z',
+ host: {
+ name: 'host-1',
+ },
},
},
});
@@ -189,7 +195,7 @@ describe('build_threat_mapping_filter', () => {
describe('createInnerAndClauses', () => {
test('it should return two clauses given a single entry', () => {
const [{ entries: threatMappingEntries }] = getThreatMappingMock(); // get the first element
- const threatListItem = getThreatListItemMock();
+ const threatListItem = getThreatListSearchResponseMock().hits.hits[0];
const innerClause = createInnerAndClauses({ threatMappingEntries, threatListItem });
const {
bool: {
@@ -219,7 +225,7 @@ describe('build_threat_mapping_filter', () => {
type: 'mapping',
},
];
- const threatListItem = getThreatListItemMock();
+ const threatListItem = getThreatListSearchResponseMock().hits.hits[0];
const innerClause = createInnerAndClauses({ threatMappingEntries, threatListItem });
const {
bool: {
@@ -248,7 +254,7 @@ describe('build_threat_mapping_filter', () => {
type: 'mapping',
},
];
- const threatListItem = getThreatListItemMock();
+ const threatListItem = getThreatListSearchResponseMock().hits.hits[0];
const innerClause = createInnerAndClauses({ threatMappingEntries, threatListItem });
const {
bool: {
@@ -275,7 +281,7 @@ describe('build_threat_mapping_filter', () => {
type: 'mapping',
},
];
- const threatListItem = getThreatListItemMock();
+ const threatListItem = getThreatListSearchResponseMock().hits.hits[0];
const innerClause = createInnerAndClauses({ threatMappingEntries, threatListItem });
expect(innerClause).toEqual([]);
});
@@ -284,27 +290,31 @@ describe('build_threat_mapping_filter', () => {
describe('createAndOrClauses', () => {
test('it should return all clauses given the entries', () => {
const threatMapping = getThreatMappingMock();
- const threatListItem = getThreatListItemMock();
+ const threatListItem = getThreatListSearchResponseMock().hits.hits[0];
const innerClause = createAndOrClauses({ threatMapping, threatListItem });
expect(innerClause).toEqual(getThreatMappingFilterShouldMock());
});
test('it should filter out data from entries that do not have mappings', () => {
const threatMapping = getThreatMappingMock();
- const threatListItem = { ...getThreatListItemMock(), foo: 'bar' };
+ const threatListItem = getThreatListSearchResponseMock().hits.hits[0];
+ threatListItem._source = {
+ ...getThreatListSearchResponseMock().hits.hits[0]._source,
+ foo: 'bar',
+ };
const innerClause = createAndOrClauses({ threatMapping, threatListItem });
expect(innerClause).toEqual(getThreatMappingFilterShouldMock());
});
test('it should return an empty boolean given an empty array', () => {
- const threatListItem = getThreatListItemMock();
+ const threatListItem = getThreatListSearchResponseMock().hits.hits[0];
const innerClause = createAndOrClauses({ threatMapping: [], threatListItem });
expect(innerClause).toEqual({ bool: { minimum_should_match: 1, should: [] } });
});
test('it should return an empty boolean clause given an empty object for a threat list item', () => {
const threatMapping = getThreatMappingMock();
- const innerClause = createAndOrClauses({ threatMapping, threatListItem: {} });
+ const innerClause = createAndOrClauses({ threatMapping, threatListItem: { _source: {} } });
expect(innerClause).toEqual({ bool: { minimum_should_match: 1, should: [] } });
});
});
@@ -312,7 +322,7 @@ describe('build_threat_mapping_filter', () => {
describe('buildEntriesMappingFilter', () => {
test('it should return all clauses given the entries', () => {
const threatMapping = getThreatMappingMock();
- const threatList = getThreatListSearchResponseMock();
+ const threatList = getThreatListSearchResponseMock().hits.hits;
const mapping = buildEntriesMappingFilter({
threatMapping,
threatList,
@@ -326,8 +336,7 @@ describe('build_threat_mapping_filter', () => {
test('it should return empty "should" given an empty threat list', () => {
const threatMapping = getThreatMappingMock();
- const threatList = getThreatListSearchResponseMock();
- threatList.hits.hits = [];
+ const threatList: ThreatListItem[] = [];
const mapping = buildEntriesMappingFilter({
threatMapping,
threatList,
@@ -340,7 +349,7 @@ describe('build_threat_mapping_filter', () => {
});
test('it should return empty "should" given an empty threat mapping', () => {
- const threatList = getThreatListSearchResponseMock();
+ const threatList = getThreatListSearchResponseMock().hits.hits;
const mapping = buildEntriesMappingFilter({
threatMapping: [],
threatList,
@@ -374,7 +383,7 @@ describe('build_threat_mapping_filter', () => {
},
],
];
- const threatList = getThreatListSearchResponseMock();
+ const threatList = getThreatListSearchResponseMock().hits.hits;
const mapping = buildEntriesMappingFilter({
threatMapping,
threatList,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts
index 346f156a9ec33..294d97e0bf2f1 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts
@@ -53,9 +53,9 @@ export const filterThreatMapping = ({
}: FilterThreatMappingOptions): ThreatMapping =>
threatMapping
.map((threatMap) => {
- const atLeastOneItemMissingInThreatList = threatMap.entries.some(
- (entry) => get(entry.value, threatListItem) == null
- );
+ const atLeastOneItemMissingInThreatList = threatMap.entries.some((entry) => {
+ return get(entry.value, threatListItem._source) == null;
+ });
if (atLeastOneItemMissingInThreatList) {
return { ...threatMap, entries: [] };
} else {
@@ -69,7 +69,7 @@ export const createInnerAndClauses = ({
threatListItem,
}: CreateInnerAndClausesOptions): BooleanFilter[] => {
return threatMappingEntries.reduce((accum, threatMappingEntry) => {
- const value = get(threatMappingEntry.value, threatListItem);
+ const value = get(threatMappingEntry.value, threatListItem._source);
if (value != null) {
// These values could be potentially 10k+ large so mutating the array intentionally
accum.push({
@@ -114,24 +114,21 @@ export const buildEntriesMappingFilter = ({
threatList,
chunkSize,
}: BuildEntriesMappingFilterOptions): BooleanFilter => {
- const combinedShould = threatList.hits.hits.reduce(
- (accum, threatListSearchItem) => {
- const filteredEntries = filterThreatMapping({
- threatMapping,
- threatListItem: threatListSearchItem._source,
- });
- const queryWithAndOrClause = createAndOrClauses({
- threatMapping: filteredEntries,
- threatListItem: threatListSearchItem._source,
- });
- if (queryWithAndOrClause.bool.should.length !== 0) {
- // These values can be 10k+ large, so using a push here for performance
- accum.push(queryWithAndOrClause);
- }
- return accum;
- },
- []
- );
+ const combinedShould = threatList.reduce((accum, threatListSearchItem) => {
+ const filteredEntries = filterThreatMapping({
+ threatMapping,
+ threatListItem: threatListSearchItem,
+ });
+ const queryWithAndOrClause = createAndOrClauses({
+ threatMapping: filteredEntries,
+ threatListItem: threatListSearchItem,
+ });
+ if (queryWithAndOrClause.bool.should.length !== 0) {
+ // These values can be 10k+ large, so using a push here for performance
+ accum.push(queryWithAndOrClause);
+ }
+ return accum;
+ }, []);
const should = splitShouldClauses({ should: combinedShould, chunkSize });
return { bool: { should, minimum_should_match: 1 } };
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts
index 037f91240edfa..43fb759d07620 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts
@@ -4,13 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { getThreatList } from './get_threat_list';
import { buildThreatMappingFilter } from './build_threat_mapping_filter';
import { getFilter } from '../get_filter';
import { searchAfterAndBulkCreate } from '../search_after_bulk_create';
-import { CreateThreatSignalOptions, ThreatSignalResults } from './types';
-import { combineResults } from './utils';
+import { CreateThreatSignalOptions } from './types';
+import { SearchAfterAndBulkCreateReturnType } from '../types';
export const createThreatSignal = async ({
threatMapping,
@@ -41,28 +40,11 @@ export const createThreatSignal = async ({
refresh,
tags,
throttle,
- threatFilters,
- threatQuery,
- threatLanguage,
buildRuleMessage,
- threatIndex,
name,
currentThreatList,
currentResult,
-}: CreateThreatSignalOptions): Promise => {
- const threatList = await getThreatList({
- callCluster: services.callCluster,
- exceptionItems,
- query: threatQuery,
- language: threatLanguage,
- threatFilters,
- index: threatIndex,
- searchAfter: currentThreatList.hits.hits[currentThreatList.hits.hits.length - 1].sort,
- sortField: undefined,
- sortOrder: undefined,
- listClient,
- });
-
+}: CreateThreatSignalOptions): Promise => {
const threatFilter = buildThreatMappingFilter({
threatMapping,
threatList: currentThreatList,
@@ -71,7 +53,12 @@ export const createThreatSignal = async ({
if (threatFilter.query.bool.should.length === 0) {
// empty threat list and we do not want to return everything as being
// a hit so opt to return the existing result.
- return { threatList, results: currentResult };
+ logger.debug(
+ buildRuleMessage(
+ 'Indicator items are empty after filtering for missing data, returning without attempting a match'
+ )
+ );
+ return currentResult;
} else {
const esFilter = await getFilter({
type,
@@ -83,7 +70,13 @@ export const createThreatSignal = async ({
index: inputIndex,
lists: exceptionItems,
});
- const newResult = await searchAfterAndBulkCreate({
+
+ logger.debug(
+ buildRuleMessage(
+ `${threatFilter.query.bool.should.length} indicator items are being checked for existence of matches`
+ )
+ );
+ const result = await searchAfterAndBulkCreate({
gap,
previousStartedAt,
listClient,
@@ -110,7 +103,15 @@ export const createThreatSignal = async ({
throttle,
buildRuleMessage,
});
- const results = combineResults(currentResult, newResult);
- return { threatList, results };
+ logger.debug(
+ buildRuleMessage(
+ `${
+ threatFilter.query.bool.should.length
+ } items have completed match checks and the total times to search were ${
+ result.searchAfterTimes.length !== 0 ? result.searchAfterTimes : '(unknown) '
+ }ms`
+ )
+ );
+ return result;
}
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts
index 8be76dc8caf0f..e90c45d40de95 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts
@@ -4,11 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { getThreatList } from './get_threat_list';
+import chunk from 'lodash/fp/chunk';
+import { getThreatList, getThreatListCount } from './get_threat_list';
import { CreateThreatSignalsOptions } from './types';
import { createThreatSignal } from './create_threat_signal';
import { SearchAfterAndBulkCreateReturnType } from '../types';
+import { combineConcurrentResults } from './utils';
export const createThreatSignals = async ({
threatMapping,
@@ -45,7 +47,12 @@ export const createThreatSignals = async ({
buildRuleMessage,
threatIndex,
name,
+ concurrentSearches,
+ itemsPerSearch,
}: CreateThreatSignalsOptions): Promise => {
+ logger.debug(buildRuleMessage('Indicator matching rule starting'));
+ const perPage = concurrentSearches * itemsPerSearch;
+
let results: SearchAfterAndBulkCreateReturnType = {
success: true,
bulkCreateTimes: [],
@@ -55,6 +62,16 @@ export const createThreatSignals = async ({
errors: [],
};
+ let threatListCount = await getThreatListCount({
+ callCluster: services.callCluster,
+ exceptionItems,
+ threatFilters,
+ query: threatQuery,
+ language: threatLanguage,
+ index: threatIndex,
+ });
+ logger.debug(buildRuleMessage(`Total indicator items: ${threatListCount}`));
+
let threatList = await getThreatList({
callCluster: services.callCluster,
exceptionItems,
@@ -66,47 +83,89 @@ export const createThreatSignals = async ({
searchAfter: undefined,
sortField: undefined,
sortOrder: undefined,
+ logger,
+ buildRuleMessage,
+ perPage,
});
- while (threatList.hits.hits.length !== 0 && results.createdSignalsCount <= params.maxSignals) {
- ({ threatList, results } = await createThreatSignal({
- threatMapping,
- query,
- inputIndex,
- type,
- filters,
- language,
- savedId,
- services,
+ while (threatList.hits.hits.length !== 0) {
+ const chunks = chunk(itemsPerSearch, threatList.hits.hits);
+ logger.debug(buildRuleMessage(`${chunks.length} concurrent indicator searches are starting.`));
+ const concurrentSearchesPerformed = chunks.map>(
+ (slicedChunk) =>
+ createThreatSignal({
+ threatMapping,
+ query,
+ inputIndex,
+ type,
+ filters,
+ language,
+ savedId,
+ services,
+ exceptionItems,
+ gap,
+ previousStartedAt,
+ listClient,
+ logger,
+ eventsTelemetry,
+ alertId,
+ outputIndex,
+ params,
+ searchAfterSize,
+ actions,
+ createdBy,
+ createdAt,
+ updatedBy,
+ updatedAt,
+ interval,
+ enabled,
+ tags,
+ refresh,
+ throttle,
+ buildRuleMessage,
+ name,
+ currentThreatList: slicedChunk,
+ currentResult: results,
+ })
+ );
+ const searchesPerformed = await Promise.all(concurrentSearchesPerformed);
+ results = combineConcurrentResults(results, searchesPerformed);
+ threatListCount -= threatList.hits.hits.length;
+ logger.debug(
+ buildRuleMessage(
+ `Concurrent indicator match searches completed with ${results.createdSignalsCount} signals found`,
+ `search times of ${results.searchAfterTimes}ms,`,
+ `bulk create times ${results.bulkCreateTimes}ms,`,
+ `all successes are ${results.success}`
+ )
+ );
+ if (results.createdSignalsCount >= params.maxSignals) {
+ logger.debug(
+ buildRuleMessage(
+ `Indicator match has reached its max signals count ${params.maxSignals}. Additional indicator items not checked are ${threatListCount}`
+ )
+ );
+ break;
+ }
+ logger.debug(buildRuleMessage(`Indicator items left to check are ${threatListCount}`));
+
+ threatList = await getThreatList({
+ callCluster: services.callCluster,
exceptionItems,
- gap,
- previousStartedAt,
- listClient,
- logger,
- eventsTelemetry,
- alertId,
- outputIndex,
- params,
- searchAfterSize,
- actions,
- createdBy,
- createdAt,
- updatedBy,
- updatedAt,
- interval,
- enabled,
- tags,
- refresh,
- throttle,
+ query: threatQuery,
+ language: threatLanguage,
threatFilters,
- threatQuery,
+ index: threatIndex,
+ searchAfter: threatList.hits.hits[threatList.hits.hits.length - 1].sort,
+ sortField: undefined,
+ sortOrder: undefined,
+ listClient,
buildRuleMessage,
- threatIndex,
- threatLanguage,
- name,
- currentThreatList: threatList,
- currentResult: results,
- }));
+ logger,
+ perPage,
+ });
}
+
+ logger.debug(buildRuleMessage('Indicator matching rule has completed'));
return results;
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts
index 3147eb1705168..aba3f6f69d706 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts
@@ -10,6 +10,7 @@ import {
GetSortWithTieBreakerOptions,
GetThreatListOptions,
SortWithTieBreaker,
+ ThreatListCountOptions,
ThreatListItem,
} from './types';
@@ -30,6 +31,8 @@ export const getThreatList = async ({
exceptionItems,
threatFilters,
listClient,
+ buildRuleMessage,
+ logger,
}: GetThreatListOptions): Promise> => {
const calculatedPerPage = perPage ?? MAX_PER_PAGE;
if (calculatedPerPage > 10000) {
@@ -43,6 +46,11 @@ export const getThreatList = async ({
exceptionItems
);
+ logger.debug(
+ buildRuleMessage(
+ `Querying the indicator items from the index: "${index}" with searchAfter: "${searchAfter}" for up to ${calculatedPerPage} indicator items`
+ )
+ );
const response: SearchResponse = await callCluster('search', {
body: {
query: queryFilter,
@@ -58,6 +66,8 @@ export const getThreatList = async ({
index,
size: calculatedPerPage,
});
+
+ logger.debug(buildRuleMessage(`Retrieved indicator items of size: ${response.hits.hits.length}`));
return response;
};
@@ -89,3 +99,30 @@ export const getSortWithTieBreaker = ({
}
}
};
+
+export const getThreatListCount = async ({
+ callCluster,
+ query,
+ language,
+ threatFilters,
+ index,
+ exceptionItems,
+}: ThreatListCountOptions): Promise => {
+ const queryFilter = getQueryFilter(
+ query,
+ language ?? 'kuery',
+ threatFilters,
+ index,
+ exceptionItems
+ );
+ const response: {
+ count: number;
+ } = await callCluster('count', {
+ body: {
+ query: queryFilter,
+ },
+ ignoreUnavailable: true,
+ index,
+ });
+ return response.count;
+};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts
index 0078cf1b3c64f..2e32a4e682403 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts
@@ -5,7 +5,6 @@
*/
import { Duration } from 'moment';
-import { SearchResponse } from 'elasticsearch';
import { ListClient } from '../../../../../../lists/server';
import {
Type,
@@ -17,6 +16,8 @@ import {
ThreatMappingEntries,
ThreatIndex,
ThreatLanguageOrUndefined,
+ ConcurrentSearches,
+ ItemsPerSearch,
} from '../../../../../common/detection_engine/schemas/types/threat_mapping';
import { PartialFilter, RuleTypeParams } from '../../types';
import { AlertServices } from '../../../../../../alerts/server';
@@ -62,6 +63,8 @@ export interface CreateThreatSignalsOptions {
threatIndex: ThreatIndex;
threatLanguage: ThreatLanguageOrUndefined;
name: string;
+ concurrentSearches: ConcurrentSearches;
+ itemsPerSearch: ItemsPerSearch;
}
export interface CreateThreatSignalOptions {
@@ -93,24 +96,15 @@ export interface CreateThreatSignalOptions {
tags: string[];
refresh: false | 'wait_for';
throttle: string;
- threatFilters: PartialFilter[];
- threatQuery: ThreatQuery;
buildRuleMessage: BuildRuleMessage;
- threatIndex: ThreatIndex;
- threatLanguage: ThreatLanguageOrUndefined;
name: string;
- currentThreatList: SearchResponse;
+ currentThreatList: ThreatListItem[];
currentResult: SearchAfterAndBulkCreateReturnType;
}
-export interface ThreatSignalResults {
- threatList: SearchResponse;
- results: SearchAfterAndBulkCreateReturnType;
-}
-
export interface BuildThreatMappingFilterOptions {
threatMapping: ThreatMapping;
- threatList: SearchResponse;
+ threatList: ThreatListItem[];
chunkSize?: number;
}
@@ -131,7 +125,7 @@ export interface CreateAndOrClausesOptions {
export interface BuildEntriesMappingFilterOptions {
threatMapping: ThreatMapping;
- threatList: SearchResponse;
+ threatList: ThreatListItem[];
chunkSize: number;
}
@@ -156,6 +150,17 @@ export interface GetThreatListOptions {
threatFilters: PartialFilter[];
exceptionItems: ExceptionListItemSchema[];
listClient: ListClient;
+ buildRuleMessage: BuildRuleMessage;
+ logger: Logger;
+}
+
+export interface ThreatListCountOptions {
+ callCluster: ILegacyScopedClusterClient['callAsCurrentUser'];
+ query: string;
+ language: ThreatLanguageOrUndefined;
+ threatFilters: PartialFilter[];
+ index: string[];
+ exceptionItems: ExceptionListItemSchema[];
}
export interface GetSortWithTieBreakerOptions {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts
index 27593b40b0c8f..840d64381c793 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts
@@ -6,7 +6,13 @@
import { SearchAfterAndBulkCreateReturnType } from '../types';
-import { calculateAdditiveMax, combineResults } from './utils';
+import {
+ calculateAdditiveMax,
+ calculateMax,
+ calculateMaxLookBack,
+ combineConcurrentResults,
+ combineResults,
+} from './utils';
describe('utils', () => {
describe('calculateAdditiveMax', () => {
@@ -156,4 +162,383 @@ describe('utils', () => {
);
});
});
+
+ describe('calculateMax', () => {
+ test('it should return 0 for two empty arrays', () => {
+ const max = calculateMax([], []);
+ expect(max).toEqual('0');
+ });
+
+ test('it should return 5 for two arrays with the numbers 5', () => {
+ const max = calculateMax(['5'], ['5']);
+ expect(max).toEqual('5');
+ });
+
+ test('it should return 5 for two arrays with second array having just 5', () => {
+ const max = calculateMax([], ['5']);
+ expect(max).toEqual('5');
+ });
+
+ test('it should return 5 for two arrays with first array having just 5', () => {
+ const max = calculateMax(['5'], []);
+ expect(max).toEqual('5');
+ });
+
+ test('it should return 10 for the max of the two arrays when the max of each array is 10', () => {
+ const max = calculateMax(['3', '5', '1'], ['3', '5', '10']);
+ expect(max).toEqual('10');
+ });
+
+ test('it should return 10 for the max of the two arrays when the max of the first is 10', () => {
+ const max = calculateMax(['3', '5', '10'], ['3', '5', '1']);
+ expect(max).toEqual('10');
+ });
+ });
+
+ describe('calculateMaxLookBack', () => {
+ test('it should return null if both are null', () => {
+ const max = calculateMaxLookBack(null, null);
+ expect(max).toEqual(null);
+ });
+
+ test('it should return undefined if both are undefined', () => {
+ const max = calculateMaxLookBack(undefined, undefined);
+ expect(max).toEqual(undefined);
+ });
+
+ test('it should return null if both one is null and other other is undefined', () => {
+ const max = calculateMaxLookBack(undefined, null);
+ expect(max).toEqual(null);
+ });
+
+ test('it should return null if both one is null and other other is undefined with flipped arguments', () => {
+ const max = calculateMaxLookBack(null, undefined);
+ expect(max).toEqual(null);
+ });
+
+ test('it should return a date time if one argument is null', () => {
+ const max = calculateMaxLookBack(null, new Date('2020-09-16T03:34:32.390Z'));
+ expect(max).toEqual(new Date('2020-09-16T03:34:32.390Z'));
+ });
+
+ test('it should return a date time if one argument is null with flipped arguments', () => {
+ const max = calculateMaxLookBack(new Date('2020-09-16T03:34:32.390Z'), null);
+ expect(max).toEqual(new Date('2020-09-16T03:34:32.390Z'));
+ });
+
+ test('it should return a date time if one argument is undefined', () => {
+ const max = calculateMaxLookBack(new Date('2020-09-16T03:34:32.390Z'), undefined);
+ expect(max).toEqual(new Date('2020-09-16T03:34:32.390Z'));
+ });
+
+ test('it should return a date time if one argument is undefined with flipped arguments', () => {
+ const max = calculateMaxLookBack(undefined, new Date('2020-09-16T03:34:32.390Z'));
+ expect(max).toEqual(new Date('2020-09-16T03:34:32.390Z'));
+ });
+
+ test('it should return a date time that is larger than the other', () => {
+ const max = calculateMaxLookBack(
+ new Date('2020-10-16T03:34:32.390Z'),
+ new Date('2020-09-16T03:34:32.390Z')
+ );
+ expect(max).toEqual(new Date('2020-10-16T03:34:32.390Z'));
+ });
+
+ test('it should return a date time that is larger than the other with arguments flipped', () => {
+ const max = calculateMaxLookBack(
+ new Date('2020-09-16T03:34:32.390Z'),
+ new Date('2020-10-16T03:34:32.390Z')
+ );
+ expect(max).toEqual(new Date('2020-10-16T03:34:32.390Z'));
+ });
+ });
+
+ describe('combineConcurrentResults', () => {
+ test('it should use the maximum found if given an empty array for newResults', () => {
+ const existingResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+ const expectedResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['30'], // max value from existingResult.searchAfterTimes
+ bulkCreateTimes: ['25'], // max value from existingResult.bulkCreateTimes
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+ const combinedResults = combineConcurrentResults(existingResult, []);
+ expect(combinedResults).toEqual(expectedResult);
+ });
+
+ test('it should work with empty arrays for searchAfterTimes and bulkCreateTimes', () => {
+ const existingResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+ const newResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: [],
+ bulkCreateTimes: [],
+ lastLookBackDate: undefined,
+ createdSignalsCount: 0,
+ errors: [],
+ };
+ const expectedResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['30'], // max value from existingResult.searchAfterTimes
+ bulkCreateTimes: ['25'], // max value from existingResult.bulkCreateTimes
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+
+ const combinedResults = combineConcurrentResults(existingResult, [newResult]);
+ expect(combinedResults).toEqual(expectedResult);
+ });
+
+ test('it should get the max of two new results and then combine the result with an existingResult correctly', () => {
+ const existingResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'], // max is 30
+ bulkCreateTimes: ['5', '15', '25'], // max is 25
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+ const newResult1: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'),
+ createdSignalsCount: 5,
+ errors: [],
+ };
+ const newResult2: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['40', '5', '15'],
+ bulkCreateTimes: ['50', '5', '15'],
+ lastLookBackDate: new Date('2020-09-16T04:34:32.390Z'),
+ createdSignalsCount: 8,
+ errors: [],
+ };
+
+ const expectedResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['70'], // max value between newResult1 and newResult2 + max array value of existingResult (40 + 30 = 70)
+ bulkCreateTimes: ['75'], // max value between newResult1 and newResult2 + max array value of existingResult (50 + 25 = 75)
+ lastLookBackDate: new Date('2020-09-16T04:34:32.390Z'), // max lastLookBackDate
+ createdSignalsCount: 16, // all the signals counted together (8 + 5 + 3)
+ errors: [],
+ };
+
+ const combinedResults = combineConcurrentResults(existingResult, [newResult1, newResult2]);
+ expect(combinedResults).toEqual(expectedResult);
+ });
+
+ test('it should get the max of two new results and then combine the result with an existingResult correctly when the results are flipped around', () => {
+ const existingResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'], // max is 30
+ bulkCreateTimes: ['5', '15', '25'], // max is 25
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+ const newResult1: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'),
+ createdSignalsCount: 5,
+ errors: [],
+ };
+ const newResult2: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['40', '5', '15'],
+ bulkCreateTimes: ['50', '5', '15'],
+ lastLookBackDate: new Date('2020-09-16T04:34:32.390Z'),
+ createdSignalsCount: 8,
+ errors: [],
+ };
+
+ const expectedResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['70'], // max value between newResult1 and newResult2 + max array value of existingResult (40 + 30 = 70)
+ bulkCreateTimes: ['75'], // max value between newResult1 and newResult2 + max array value of existingResult (50 + 25 = 75)
+ lastLookBackDate: new Date('2020-09-16T04:34:32.390Z'), // max lastLookBackDate
+ createdSignalsCount: 16, // all the signals counted together (8 + 5 + 3)
+ errors: [],
+ };
+
+ const combinedResults = combineConcurrentResults(existingResult, [newResult2, newResult1]); // two array elements are flipped
+ expect(combinedResults).toEqual(expectedResult);
+ });
+
+ test('it should return the max date correctly if one date contains a null', () => {
+ const existingResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'], // max is 30
+ bulkCreateTimes: ['5', '15', '25'], // max is 25
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+ const newResult1: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'),
+ createdSignalsCount: 5,
+ errors: [],
+ };
+ const newResult2: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['40', '5', '15'],
+ bulkCreateTimes: ['50', '5', '15'],
+ lastLookBackDate: null,
+ createdSignalsCount: 8,
+ errors: [],
+ };
+
+ const expectedResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['70'], // max value between newResult1 and newResult2 + max array value of existingResult (40 + 30 = 70)
+ bulkCreateTimes: ['75'], // max value between newResult1 and newResult2 + max array value of existingResult (50 + 25 = 75)
+ lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), // max lastLookBackDate
+ createdSignalsCount: 16, // all the signals counted together (8 + 5 + 3)
+ errors: [],
+ };
+
+ const combinedResults = combineConcurrentResults(existingResult, [newResult1, newResult2]);
+ expect(combinedResults).toEqual(expectedResult);
+ });
+
+ test('it should combine two results with success set to "true" if both are "true"', () => {
+ const existingResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+
+ const newResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+ const combinedResults = combineConcurrentResults(existingResult, [newResult]);
+ expect(combinedResults.success).toEqual(true);
+ });
+
+ test('it should combine two results with success set to "false" if one of them is "false"', () => {
+ const existingResult: SearchAfterAndBulkCreateReturnType = {
+ success: false,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+
+ const newResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+ const combinedResults = combineConcurrentResults(existingResult, [newResult]);
+ expect(combinedResults.success).toEqual(false);
+ });
+
+ test('it should use the latest date if it is set in the new result', () => {
+ const existingResult: SearchAfterAndBulkCreateReturnType = {
+ success: false,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+
+ const newResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'),
+ createdSignalsCount: 3,
+ errors: [],
+ };
+ const combinedResults = combineConcurrentResults(existingResult, [newResult]);
+ expect(combinedResults.lastLookBackDate?.toISOString()).toEqual('2020-09-16T03:34:32.390Z');
+ });
+
+ test('it should combine the searchAfterTimes and the bulkCreateTimes', () => {
+ const existingResult: SearchAfterAndBulkCreateReturnType = {
+ success: false,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+
+ const newResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'),
+ createdSignalsCount: 3,
+ errors: [],
+ };
+ const combinedResults = combineConcurrentResults(existingResult, [newResult]);
+ expect(combinedResults).toEqual(
+ expect.objectContaining({
+ searchAfterTimes: ['60'],
+ bulkCreateTimes: ['50'],
+ })
+ );
+ });
+
+ test('it should combine errors together without duplicates', () => {
+ const existingResult: SearchAfterAndBulkCreateReturnType = {
+ success: false,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: ['error 1', 'error 2', 'error 3'],
+ };
+
+ const newResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'),
+ createdSignalsCount: 3,
+ errors: ['error 4', 'error 1', 'error 3', 'error 5'],
+ };
+ const combinedResults = combineConcurrentResults(existingResult, [newResult]);
+ expect(combinedResults).toEqual(
+ expect.objectContaining({
+ errors: ['error 1', 'error 2', 'error 3', 'error 4', 'error 5'],
+ })
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts
index 401a4a1acb065..d6c91fad6d9cb 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts
@@ -19,6 +19,41 @@ export const calculateAdditiveMax = (existingTimers: string[], newTimers: string
return [String(numericNewTimerMax + numericExistingTimerMax)];
};
+/**
+ * Given two timers this will take the max of each and then get the max from each.
+ * Max(Max(timer_array_1), Max(timer_array_2))
+ * @param existingTimers String array of existing timers
+ * @param newTimers String array of new timers.
+ * @returns String array of the new maximum between the two timers
+ */
+export const calculateMax = (existingTimers: string[], newTimers: string[]): string => {
+ const numericNewTimerMax = Math.max(0, ...newTimers.map((time) => +time));
+ const numericExistingTimerMax = Math.max(0, ...existingTimers.map((time) => +time));
+ return String(Math.max(numericNewTimerMax, numericExistingTimerMax));
+};
+
+/**
+ * Given two dates this will return the larger of the two unless one of them is null
+ * or undefined. If both one or the other is null/undefined it will return the newDate.
+ * If there is a mix of "undefined" and "null", this will prefer to set it to "null" as having
+ * a higher value than "undefined"
+ * @param existingDate The existing date which can be undefined or null or a date
+ * @param newDate The new date which can be undefined or null or a date
+ */
+export const calculateMaxLookBack = (
+ existingDate: Date | null | undefined,
+ newDate: Date | null | undefined
+): Date | null | undefined => {
+ const newDateValue = newDate === null ? 1 : newDate === undefined ? 0 : newDate.valueOf();
+ const existingDateValue =
+ existingDate === null ? 1 : existingDate === undefined ? 0 : existingDate.valueOf();
+ if (newDateValue >= existingDateValue) {
+ return newDate;
+ } else {
+ return existingDate;
+ }
+};
+
/**
* Combines two results together and returns the results combined
* @param currentResult The current result to combine with a newResult
@@ -38,3 +73,39 @@ export const combineResults = (
createdSignalsCount: currentResult.createdSignalsCount + newResult.createdSignalsCount,
errors: [...new Set([...currentResult.errors, ...newResult.errors])],
});
+
+/**
+ * Combines two results together and returns the results combined
+ * @param currentResult The current result to combine with a newResult
+ * @param newResult The new result to combine
+ */
+export const combineConcurrentResults = (
+ currentResult: SearchAfterAndBulkCreateReturnType,
+ newResult: SearchAfterAndBulkCreateReturnType[]
+): SearchAfterAndBulkCreateReturnType => {
+ const maxedNewResult = newResult.reduce(
+ (accum, item) => {
+ const maxSearchAfterTime = calculateMax(accum.searchAfterTimes, item.searchAfterTimes);
+ const maxBulkCreateTimes = calculateMax(accum.bulkCreateTimes, item.bulkCreateTimes);
+ const lastLookBackDate = calculateMaxLookBack(accum.lastLookBackDate, item.lastLookBackDate);
+ return {
+ success: accum.success && item.success,
+ searchAfterTimes: [maxSearchAfterTime],
+ bulkCreateTimes: [maxBulkCreateTimes],
+ lastLookBackDate,
+ createdSignalsCount: accum.createdSignalsCount + item.createdSignalsCount,
+ errors: [...new Set([...accum.errors, ...item.errors])],
+ };
+ },
+ {
+ success: true,
+ searchAfterTimes: [],
+ bulkCreateTimes: [],
+ lastLookBackDate: undefined,
+ createdSignalsCount: 0,
+ errors: [],
+ }
+ );
+
+ return combineResults(currentResult, maxedNewResult);
+};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts
index cf4d989c1f4c8..5cac76e2b0c01 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts
@@ -44,6 +44,8 @@ import {
ThreatQueryOrUndefined,
ThreatMappingOrUndefined,
ThreatLanguageOrUndefined,
+ ConcurrentSearchesOrUndefined,
+ ItemsPerSearchOrUndefined,
} from '../../../common/detection_engine/schemas/types/threat_mapping';
import { LegacyCallAPIOptions } from '../../../../../../src/core/server';
@@ -93,6 +95,8 @@ export interface RuleTypeParams {
references: References;
version: Version;
exceptionsList: ListArrayOrUndefined;
+ concurrentSearches: ConcurrentSearchesOrUndefined;
+ itemsPerSearch: ItemsPerSearchOrUndefined;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/x-pack/plugins/security_solution/server/ui_settings.ts b/x-pack/plugins/security_solution/server/ui_settings.ts
index 4b5261edcdfd0..6b10a9909e19c 100644
--- a/x-pack/plugins/security_solution/server/ui_settings.ts
+++ b/x-pack/plugins/security_solution/server/ui_settings.ts
@@ -23,6 +23,10 @@ import {
NEWS_FEED_URL_SETTING_DEFAULT,
IP_REPUTATION_LINKS_SETTING,
IP_REPUTATION_LINKS_SETTING_DEFAULT,
+ DEFAULT_RULES_TABLE_REFRESH_SETTING,
+ DEFAULT_RULE_REFRESH_INTERVAL_ON,
+ DEFAULT_RULE_REFRESH_INTERVAL_VALUE,
+ DEFAULT_RULE_REFRESH_IDLE_VALUE,
} from '../common/constants';
export const initUiSettings = (uiSettings: CoreSetup['uiSettings']) => {
@@ -112,6 +116,31 @@ export const initUiSettings = (uiSettings: CoreSetup['uiSettings']) => {
requiresPageReload: true,
schema: schema.boolean(),
},
+ [DEFAULT_RULES_TABLE_REFRESH_SETTING]: {
+ name: i18n.translate('xpack.securitySolution.uiSettings.rulesTableRefresh', {
+ defaultMessage: 'Rules auto refresh',
+ }),
+ description: i18n.translate(
+ 'xpack.securitySolution.uiSettings.rulesTableRefreshDescription',
+ {
+ defaultMessage:
+ 'Enables auto refresh on the all rules and monitoring tables, in milliseconds
',
+ }
+ ),
+ type: 'json',
+ value: `{
+ "on": ${DEFAULT_RULE_REFRESH_INTERVAL_ON},
+ "value": ${DEFAULT_RULE_REFRESH_INTERVAL_VALUE},
+ "idleTimeout": ${DEFAULT_RULE_REFRESH_IDLE_VALUE}
+}`,
+ category: ['securitySolution'],
+ requiresPageReload: true,
+ schema: schema.object({
+ idleTimeout: schema.number({ min: 300000 }),
+ value: schema.number({ min: 60000 }),
+ on: schema.boolean(),
+ }),
+ },
[NEWS_FEED_URL_SETTING]: {
name: i18n.translate('xpack.securitySolution.uiSettings.newsFeedUrl', {
defaultMessage: 'News feed URL',
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts
index 3f5addb77cb33..48847686828a9 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts
@@ -25,6 +25,7 @@ describe('ActionContext', () => {
date: '2020-01-01T00:00:00.000Z',
group: '[group]',
value: 42,
+ function: 'count > 4',
};
const context = addMessages({ name: '[alert-name]' }, base, params);
expect(context.title).toMatchInlineSnapshot(
@@ -53,6 +54,7 @@ describe('ActionContext', () => {
date: '2020-01-01T00:00:00.000Z',
group: '[group]',
value: 42,
+ function: 'avg([aggField]) > 4.2',
};
const context = addMessages({ name: '[alert-name]' }, base, params);
expect(context.title).toMatchInlineSnapshot(
@@ -80,6 +82,7 @@ describe('ActionContext', () => {
date: '2020-01-01T00:00:00.000Z',
group: '[group]',
value: 4,
+ function: 'count between 4,5',
};
const context = addMessages({ name: '[alert-name]' }, base, params);
expect(context.title).toMatchInlineSnapshot(
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts
index 5135e31e9322c..9bb0df9d07fd4 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts
@@ -27,6 +27,8 @@ export interface BaseActionContext extends AlertInstanceContext {
date: string;
// the value that met the threshold
value: number;
+ // the function that is used
+ function: string;
}
export function addMessages(
@@ -42,9 +44,6 @@ export function addMessages(
},
});
- const agg = params.aggField ? `${params.aggType}(${params.aggField})` : `${params.aggType}`;
- const humanFn = `${agg} ${params.thresholdComparator} ${params.threshold.join(',')}`;
-
const window = `${params.timeWindowSize}${params.timeWindowUnit}`;
const message = i18n.translate(
'xpack.stackAlerts.indexThreshold.alertTypeContextMessageDescription',
@@ -55,7 +54,7 @@ export function addMessages(
name: alertInfo.name,
group: baseContext.group,
value: baseContext.value,
- function: humanFn,
+ function: baseContext.function,
window,
date: baseContext.date,
},
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts
index 2f0cf3cbbcd16..d75f3af22ab06 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts
@@ -46,6 +46,10 @@ describe('alertType', () => {
"description": "The value that exceeded the threshold.",
"name": "value",
},
+ Object {
+ "description": "A string describing the threshold comparator and threshold",
+ "name": "function",
+ },
],
"params": Array [
Object {
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts
index 2a1ed429b7fe1..e0a9cd981dac0 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts
@@ -83,6 +83,13 @@ export function getAlertType(service: Service): AlertType {
return {
@@ -107,6 +114,7 @@ export function getAlertType(service: Service): AlertType void;
+ setActionGroupIdByIndex?: (group: string, index: number) => void;
setAlertProperty: (actions: AlertAction[]) => void;
setActionParamsProperty: (key: string, value: any, index: number) => void;
http: HttpSetup;
- actionTypeRegistry: TypeRegistry;
- toastNotifications: Pick<
- ToastsApi,
- 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
- >;
+ actionTypeRegistry: ActionTypeRegistryContract;
+ toastNotifications: ToastsSetup;
+ docLinks: DocLinksStart;
actionTypes?: ActionType[];
messageVariables?: ActionVariable[];
defaultActionMessage?: string;
- consumer: string;
+ capabilities: ApplicationStart['capabilities'];
}
```
@@ -1339,17 +1339,20 @@ interface ActionAccordionFormProps {
|Property|Description|
|---|---|
|actions|List of actions comes from alert.actions property.|
-|defaultActionGroupId|Default action group id to which each new action will belong to.|
+|defaultActionGroupId|Default action group id to which each new action will belong by default.|
+|actionGroups|Optional. List of action groups to which new action can be assigned. The RunWhen field is only displayed when these action groups are specified|
|setActionIdByIndex|Function for changing action 'id' by the proper index in alert.actions array.|
+|setActionGroupIdByIndex|Function for changing action 'group' by the proper index in alert.actions array.|
|setAlertProperty|Function for changing alert property 'actions'. Used when deleting action from the array to reset it.|
|setActionParamsProperty|Function for changing action key/value property by index in alert.actions array.|
|http|HttpSetup needed for executing API calls.|
|actionTypeRegistry|Registry for action types.|
-|toastNotifications|Toast messages.|
+|toastNotifications|Toast messages Plugin Setup Contract.|
+|docLinks|Documentation links Plugin Start Contract.|
|actionTypes|Optional property, which allowes to define a list of available actions specific for a current plugin.|
|actionTypes|Optional property, which allowes to define a list of variables for action 'message' property.|
|defaultActionMessage|Optional property, which allowes to define a message value for action with 'message' property.|
-|consumer|Name of the plugin that creates an action.|
+|capabilities|Kibana core's Capabilities ApplicationStart['capabilities'].|
AlertsContextProvider value options:
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts
index d105e2c510bcd..a5b2fbb37e838 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts
@@ -5,7 +5,6 @@
*/
import { lazy } from 'react';
import { i18n } from '@kbn/i18n';
-
import { AlertTypeModel } from '../../../../types';
import { validateExpression } from './validation';
import { IndexThresholdAlertParams } from './types';
@@ -26,6 +25,12 @@ export function getAlertType(): AlertTypeModel import('./expression')),
validate: validateExpression,
+ defaultActionMessage: i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinAlertTypes.threshold.alertDefaultActionMessage',
+ {
+ defaultMessage: `alert \\{\\{alertName\\}\\} group \\{\\{context.group\\}\\} value \\{\\{context.value\\}\\} exceeded threshold \\{\\{context.function\\}\\} over \\{\\{params.timeWindowSize\\}\\}\\{\\{params.timeWindowUnit\\}\\} on \\{\\{context.date\\}\\}`,
+ }
+ ),
requiresAppContext: false,
};
}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.scss b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.scss
index 24dbb865742d8..bb622829e997a 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.scss
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.scss
@@ -3,9 +3,15 @@
}
.actAccordionActionForm {
- .euiCard {
- box-shadow: none;
- }
+ background-color: $euiColorLightestShade;
+}
+
+.actAccordionActionForm .euiCard {
+ box-shadow: none;
+}
+
+.actAccordionActionForm__button {
+ padding: $euiSizeM;
}
.actConnectorsListGrid {
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx
index 7c718e8248e41..94452e70e6bfa 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx
@@ -6,7 +6,6 @@
import React, { Fragment, lazy } from 'react';
import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
import { coreMock } from '../../../../../../../src/core/public/mocks';
-import { ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ValidationResult, Alert, AlertAction } from '../../../types';
@@ -112,8 +111,6 @@ describe('action_form', () => {
};
describe('action_form in alert', () => {
- let wrapper: ReactWrapper;
-
async function setup(customActions?: AlertAction[]) {
const { loadAllActions } = jest.requireMock('../../lib/action_connector_api');
loadAllActions.mockResolvedValueOnce([
@@ -217,7 +214,7 @@ describe('action_form', () => {
mutedInstanceIds: [],
} as unknown) as Alert;
- wrapper = mountWithIntl(
+ const wrapper = mountWithIntl(
{
setActionIdByIndex={(id: string, index: number) => {
initialAlert.actions[index].id = id;
}}
+ actionGroups={[{ id: 'default', name: 'Default' }]}
+ setActionGroupIdByIndex={(group: string, index: number) => {
+ initialAlert.actions[index].group = group;
+ }}
setAlertProperty={(_updatedActions: AlertAction[]) => {}}
setActionParamsProperty={(key: string, value: any, index: number) =>
(initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value })
@@ -297,10 +298,12 @@ describe('action_form', () => {
await nextTick();
wrapper.update();
});
+
+ return wrapper;
}
it('renders available action cards', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find(
`[data-test-subj="${actionType.id}-ActionTypeSelectOption"]`
);
@@ -314,7 +317,7 @@ describe('action_form', () => {
});
it('does not render action types disabled by config', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find(
'[data-test-subj="disabled-by-config-ActionTypeSelectOption"]'
);
@@ -322,52 +325,72 @@ describe('action_form', () => {
});
it('render action types which is preconfigured only (disabled by config and with preconfigured connectors)', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]');
expect(actionOption.exists()).toBeTruthy();
});
+ it('renders available action groups for the selected action type', async () => {
+ const wrapper = await setup();
+ const actionOption = wrapper.find(
+ `[data-test-subj="${actionType.id}-ActionTypeSelectOption"]`
+ );
+ actionOption.first().simulate('click');
+ const actionGroupsSelect = wrapper.find(
+ `[data-test-subj="addNewActionConnectorActionGroup-0"]`
+ );
+ expect((actionGroupsSelect.first().props() as any).options).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "data-test-subj": "addNewActionConnectorActionGroup-0-option-default",
+ "inputDisplay": "Default",
+ "value": "default",
+ },
+ ]
+ `);
+ });
+
it('renders available connectors for the selected action type', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find(
`[data-test-subj="${actionType.id}-ActionTypeSelectOption"]`
);
actionOption.first().simulate('click');
const combobox = wrapper.find(`[data-test-subj="selectActionConnector-${actionType.id}"]`);
expect((combobox.first().props() as any).options).toMatchInlineSnapshot(`
- Array [
- Object {
- "id": "test",
- "key": "test",
- "label": "Test connector ",
- },
- Object {
- "id": "test2",
- "key": "test2",
- "label": "Test connector 2 (preconfigured)",
- },
- ]
- `);
+ Array [
+ Object {
+ "id": "test",
+ "key": "test",
+ "label": "Test connector ",
+ },
+ Object {
+ "id": "test2",
+ "key": "test2",
+ "label": "Test connector 2 (preconfigured)",
+ },
+ ]
+ `);
});
it('renders only preconfigured connectors for the selected preconfigured action type', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]');
actionOption.first().simulate('click');
const combobox = wrapper.find('[data-test-subj="selectActionConnector-preconfigured"]');
expect((combobox.first().props() as any).options).toMatchInlineSnapshot(`
- Array [
- Object {
- "id": "test3",
- "key": "test3",
- "label": "Preconfigured Only (preconfigured)",
- },
- ]
- `);
+ Array [
+ Object {
+ "id": "test3",
+ "key": "test3",
+ "label": "Preconfigured Only (preconfigured)",
+ },
+ ]
+ `);
});
it('does not render "Add connector" button for preconfigured only action type', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]');
actionOption.first().simulate('click');
const preconfigPannel = wrapper.find('[data-test-subj="alertActionAccordion-default"]');
@@ -378,7 +401,7 @@ describe('action_form', () => {
});
it('renders action types disabled by license', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find(
'[data-test-subj="disabled-by-license-ActionTypeSelectOption"]'
);
@@ -391,7 +414,7 @@ describe('action_form', () => {
});
it(`shouldn't render action types without params component`, async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find(
`[data-test-subj="${actionTypeWithoutParams.id}-ActionTypeSelectOption"]`
);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx
index 74432157f5659..3a7341afe3e07 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment, Suspense, useState, useEffect } from 'react';
+import React, { Fragment, useState, useEffect, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@@ -14,25 +14,13 @@ import {
EuiIcon,
EuiTitle,
EuiSpacer,
- EuiFormRow,
- EuiComboBox,
EuiKeyPadMenuItem,
- EuiAccordion,
- EuiButtonIcon,
- EuiEmptyPrompt,
- EuiButtonEmpty,
EuiToolTip,
- EuiIconTip,
EuiLink,
- EuiCallOut,
- EuiHorizontalRule,
- EuiText,
- EuiLoadingSpinner,
} from '@elastic/eui';
import { HttpSetup, ToastsSetup, ApplicationStart, DocLinksStart } from 'kibana/public';
import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api';
import {
- IErrorObject,
ActionTypeModel,
ActionTypeRegistryContract,
AlertAction,
@@ -43,15 +31,19 @@ import {
} from '../../../types';
import { SectionLoading } from '../../components/section_loading';
import { ConnectorAddModal } from './connector_add_modal';
+import { ActionTypeForm, ActionTypeFormProps } from './action_type_form';
+import { AddConnectorInline } from './connector_add_inline';
import { actionTypeCompare } from '../../lib/action_type_compare';
import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled';
import { VIEW_LICENSE_OPTIONS_LINK, DEFAULT_HIDDEN_ACTION_TYPES } from '../../../common/constants';
-import { hasSaveActionsCapability } from '../../lib/capabilities';
+import { ActionGroup } from '../../../../../alerts/common';
-interface ActionAccordionFormProps {
+export interface ActionAccordionFormProps {
actions: AlertAction[];
defaultActionGroupId: string;
+ actionGroups?: ActionGroup[];
setActionIdByIndex: (id: string, index: number) => void;
+ setActionGroupIdByIndex?: (group: string, index: number) => void;
setAlertProperty: (actions: AlertAction[]) => void;
setActionParamsProperty: (key: string, value: any, index: number) => void;
http: HttpSetup;
@@ -74,7 +66,9 @@ interface ActiveActionConnectorState {
export const ActionForm = ({
actions,
defaultActionGroupId,
+ actionGroups,
setActionIdByIndex,
+ setActionGroupIdByIndex,
setAlertProperty,
setActionParamsProperty,
http,
@@ -88,8 +82,6 @@ export const ActionForm = ({
capabilities,
docLinks,
}: ActionAccordionFormProps) => {
- const canSave = hasSaveActionsCapability(capabilities);
-
const [addModalVisible, setAddModalVisibility] = useState(false);
const [activeActionItem, setActiveActionItem] = useState(
undefined
@@ -101,6 +93,10 @@ export const ActionForm = ({
const [actionTypesIndex, setActionTypesIndex] = useState(undefined);
const [emptyActionsIds, setEmptyActionsIds] = useState([]);
+ const closeAddConnectorModal = useCallback(() => setAddModalVisibility(false), [
+ setAddModalVisibility,
+ ]);
+
// load action types
useEffect(() => {
(async () => {
@@ -183,359 +179,6 @@ export const ActionForm = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [actions, connectors]);
- const preconfiguredMessage = i18n.translate(
- 'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage',
- {
- defaultMessage: '(preconfigured)',
- }
- );
-
- const getSelectedOptions = (actionItemId: string) => {
- const selectedConnector = connectors.find((connector) => connector.id === actionItemId);
- if (
- !selectedConnector ||
- // if selected connector is not preconfigured and action type is for preconfiguration only,
- // do not show regular connectors of this type
- (actionTypesIndex &&
- !actionTypesIndex[selectedConnector.actionTypeId].enabledInConfig &&
- !selectedConnector.isPreconfigured)
- ) {
- return [];
- }
- const optionTitle = `${selectedConnector.name} ${
- selectedConnector.isPreconfigured ? preconfiguredMessage : ''
- }`;
- return [
- {
- label: optionTitle,
- value: optionTitle,
- id: actionItemId,
- 'data-test-subj': 'itemActionConnector',
- },
- ];
- };
-
- const getActionTypeForm = (
- actionItem: AlertAction,
- actionConnector: ActionConnector,
- actionParamsErrors: {
- errors: IErrorObject;
- },
- index: number
- ) => {
- if (!actionTypesIndex) {
- return null;
- }
-
- const actionType = actionTypesIndex[actionItem.actionTypeId];
-
- const optionsList = connectors
- .filter(
- (connectorItem) =>
- connectorItem.actionTypeId === actionItem.actionTypeId &&
- // include only enabled by config connectors or preconfigured
- (actionType.enabledInConfig || connectorItem.isPreconfigured)
- )
- .map(({ name, id, isPreconfigured }) => ({
- label: `${name} ${isPreconfigured ? preconfiguredMessage : ''}`,
- key: id,
- id,
- }));
- const actionTypeRegistered = actionTypeRegistry.get(actionConnector.actionTypeId);
- if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null;
- const ParamsFieldsComponent = actionTypeRegistered.actionParamsFields;
- const checkEnabledResult = checkActionFormActionTypeEnabled(
- actionTypesIndex[actionConnector.actionTypeId],
- connectors.filter((connector) => connector.isPreconfigured)
- );
-
- const accordionContent = checkEnabledResult.isEnabled ? (
-
-
-
-
- }
- labelAppend={
- canSave &&
- actionTypesIndex &&
- actionTypesIndex[actionConnector.actionTypeId].enabledInConfig ? (
- {
- setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
- setAddModalVisibility(true);
- }}
- >
-
-
- ) : null
- }
- >
- {
- setActionIdByIndex(selectedOptions[0].id ?? '', index);
- }}
- isClearable={false}
- />
-
-
-
-
- {ParamsFieldsComponent ? (
-
-
-
-
-
- }
- >
-
-
- ) : null}
-
- ) : (
- checkEnabledResult.messageCard
- );
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- {checkEnabledResult.isEnabled === false && (
-
-
-
- )}
-
-
-
-
-
-
- }
- extraAction={
- {
- const updatedActions = actions.filter(
- (_item: AlertAction, i: number) => i !== index
- );
- setAlertProperty(updatedActions);
- setIsAddActionPanelOpen(
- updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length ===
- 0
- );
- setActiveActionItem(undefined);
- }}
- />
- }
- paddingSize="l"
- >
- {accordionContent}
-
-
-
- );
- };
-
- const getAddConnectorsForm = (actionItem: AlertAction, index: number) => {
- const actionTypeName = actionTypesIndex
- ? actionTypesIndex[actionItem.actionTypeId].name
- : actionItem.actionTypeId;
- const actionTypeRegistered = actionTypeRegistry.get(actionItem.actionTypeId);
- if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null;
-
- const noConnectorsLabel = (
-
- );
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
- extraAction={
- {
- const updatedActions = actions.filter(
- (_item: AlertAction, i: number) => i !== index
- );
- setAlertProperty(updatedActions);
- setIsAddActionPanelOpen(
- updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length ===
- 0
- );
- setActiveActionItem(undefined);
- }}
- />
- }
- paddingSize="l"
- >
- {canSave ? (
- actionItem.id === emptyId) ? (
- noConnectorsLabel
- ) : (
-
- )
- }
- actions={[
- {
- setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
- setAddModalVisibility(true);
- }}
- >
-
- ,
- ]}
- />
- ) : (
-
-
-
-
-
- )}
-
-
-
- );
- };
-
function addActionType(actionTypeModel: ActionTypeModel) {
if (!defaultActionGroupId) {
toastNotifications!.addDanger({
@@ -628,116 +271,172 @@ export const ActionForm = ({
});
}
- const alertActionsList = actions.map((actionItem: AlertAction, index: number) => {
- const actionConnector = connectors.find((field) => field.id === actionItem.id);
- // connectors doesn't exists
- if (!actionConnector) {
- return getAddConnectorsForm(actionItem, index);
- }
-
- const actionErrors: { errors: IErrorObject } = actionTypeRegistry
- .get(actionItem.actionTypeId)
- ?.validateParams(actionItem.params);
-
- return getActionTypeForm(actionItem, actionConnector, actionErrors, index);
- });
-
- return (
+ return isLoadingConnectors ? (
+
+
+
+ ) : (
- {isLoadingConnectors ? (
-
+
+
-
- ) : (
-
-
-
-
+
+
+ {actionTypesIndex &&
+ actions.map((actionItem: AlertAction, index: number) => {
+ const actionConnector = connectors.find((field) => field.id === actionItem.id);
+ // connectors doesn't exists
+ if (!actionConnector) {
+ return (
+ {
+ const updatedActions = actions.filter(
+ (_item: AlertAction, i: number) => i !== index
+ );
+ setAlertProperty(updatedActions);
+ setIsAddActionPanelOpen(
+ updatedActions.filter((item: AlertAction) => item.id !== actionItem.id)
+ .length === 0
+ );
+ setActiveActionItem(undefined);
+ }}
+ onAddConnector={() => {
+ setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
+ setAddModalVisibility(true);
+ }}
/>
-
-
-
- {alertActionsList}
- {isAddActionPanelOpen === false ? (
-
-
-
-
- setIsAddActionPanelOpen(true)}
- >
-
-
-
-
-
- ) : null}
- {isAddActionPanelOpen ? (
-
-
-
-
-
+ );
+ }
+
+ const actionParamsErrors: ActionTypeFormProps['actionParamsErrors'] = actionTypeRegistry
+ .get(actionItem.actionTypeId)
+ ?.validateParams(actionItem.params);
+
+ return (
+ {
+ setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
+ setAddModalVisibility(true);
+ }}
+ onConnectorSelected={(id: string) => {
+ setActionIdByIndex(id, index);
+ }}
+ onDeleteAction={() => {
+ const updatedActions = actions.filter(
+ (_item: AlertAction, i: number) => i !== index
+ );
+ setAlertProperty(updatedActions);
+ setIsAddActionPanelOpen(
+ updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length ===
+ 0
+ );
+ setActiveActionItem(undefined);
+ }}
+ />
+ );
+ })}
+
+ {isAddActionPanelOpen ? (
+
+
+
+
+
+
+
+
+
+ {hasDisabledByLicenseActionTypes && (
+
+
+
+
-
-
-
- {hasDisabledByLicenseActionTypes && (
-
-
-
-
-
-
-
-
-
- )}
-
-
-
- {isLoadingActionTypes ? (
-
-
-
- ) : (
- actionTypeNodes
- )}
-
-
- ) : null}
+
+
+
+
+ )}
+
+
+
+ {isLoadingActionTypes ? (
+
+
+
+ ) : (
+ actionTypeNodes
+ )}
+
+ ) : (
+
+
+ setIsAddActionPanelOpen(true)}
+ >
+
+
+
+
)}
- {actionTypesIndex && activeActionItem ? (
+ {actionTypesIndex && activeActionItem && addModalVisible ? (
{
connectors.push(savedAction);
setActionIdByIndex(savedAction.id, activeActionItem.index);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx
new file mode 100644
index 0000000000000..38468283b9c19
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx
@@ -0,0 +1,339 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { Fragment, Suspense, useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiSpacer,
+ EuiFormRow,
+ EuiComboBox,
+ EuiAccordion,
+ EuiButtonIcon,
+ EuiButtonEmpty,
+ EuiIconTip,
+ EuiText,
+ EuiFormLabel,
+ EuiFormControlLayout,
+ EuiSuperSelect,
+ EuiLoadingSpinner,
+ EuiBadge,
+} from '@elastic/eui';
+import { IErrorObject, AlertAction, ActionTypeIndex, ActionConnector } from '../../../types';
+import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled';
+import { hasSaveActionsCapability } from '../../lib/capabilities';
+import { ActionAccordionFormProps } from './action_form';
+
+export type ActionTypeFormProps = {
+ actionItem: AlertAction;
+ actionConnector: ActionConnector;
+ actionParamsErrors: {
+ errors: IErrorObject;
+ };
+ index: number;
+ onAddConnector: () => void;
+ onConnectorSelected: (id: string) => void;
+ onDeleteAction: () => void;
+ setActionParamsProperty: (key: string, value: any, index: number) => void;
+ actionTypesIndex: ActionTypeIndex;
+ connectors: ActionConnector[];
+} & Pick<
+ ActionAccordionFormProps,
+ | 'defaultActionGroupId'
+ | 'actionGroups'
+ | 'setActionGroupIdByIndex'
+ | 'setActionParamsProperty'
+ | 'http'
+ | 'actionTypeRegistry'
+ | 'toastNotifications'
+ | 'docLinks'
+ | 'messageVariables'
+ | 'defaultActionMessage'
+ | 'capabilities'
+>;
+
+const preconfiguredMessage = i18n.translate(
+ 'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage',
+ {
+ defaultMessage: '(preconfigured)',
+ }
+);
+
+export const ActionTypeForm = ({
+ actionItem,
+ actionConnector,
+ actionParamsErrors,
+ index,
+ onAddConnector,
+ onConnectorSelected,
+ onDeleteAction,
+ setActionParamsProperty,
+ actionTypesIndex,
+ connectors,
+ http,
+ toastNotifications,
+ docLinks,
+ capabilities,
+ actionTypeRegistry,
+ defaultActionGroupId,
+ defaultActionMessage,
+ messageVariables,
+ actionGroups,
+ setActionGroupIdByIndex,
+}: ActionTypeFormProps) => {
+ const [isOpen, setIsOpen] = useState(true);
+
+ const canSave = hasSaveActionsCapability(capabilities);
+ const getSelectedOptions = (actionItemId: string) => {
+ const selectedConnector = connectors.find((connector) => connector.id === actionItemId);
+ if (
+ !selectedConnector ||
+ // if selected connector is not preconfigured and action type is for preconfiguration only,
+ // do not show regular connectors of this type
+ (actionTypesIndex &&
+ !actionTypesIndex[selectedConnector.actionTypeId].enabledInConfig &&
+ !selectedConnector.isPreconfigured)
+ ) {
+ return [];
+ }
+ const optionTitle = `${selectedConnector.name} ${
+ selectedConnector.isPreconfigured ? preconfiguredMessage : ''
+ }`;
+ return [
+ {
+ label: optionTitle,
+ value: optionTitle,
+ id: actionItemId,
+ 'data-test-subj': 'itemActionConnector',
+ },
+ ];
+ };
+
+ const actionType = actionTypesIndex[actionItem.actionTypeId];
+
+ const optionsList = connectors
+ .filter(
+ (connectorItem) =>
+ connectorItem.actionTypeId === actionItem.actionTypeId &&
+ // include only enabled by config connectors or preconfigured
+ (actionType.enabledInConfig || connectorItem.isPreconfigured)
+ )
+ .map(({ name, id, isPreconfigured }) => ({
+ label: `${name} ${isPreconfigured ? preconfiguredMessage : ''}`,
+ key: id,
+ id,
+ }));
+ const actionTypeRegistered = actionTypeRegistry.get(actionConnector.actionTypeId);
+ if (!actionTypeRegistered) return null;
+
+ const ParamsFieldsComponent = actionTypeRegistered.actionParamsFields;
+ const checkEnabledResult = checkActionFormActionTypeEnabled(
+ actionTypesIndex[actionConnector.actionTypeId],
+ connectors.filter((connector) => connector.isPreconfigured)
+ );
+
+ const defaultActionGroup = actionGroups?.find(({ id }) => id === defaultActionGroupId);
+ const selectedActionGroup =
+ actionGroups?.find(({ id }) => id === actionItem.group) ?? defaultActionGroup;
+
+ const accordionContent = checkEnabledResult.isEnabled ? (
+
+ {actionGroups && selectedActionGroup && setActionGroupIdByIndex && (
+
+
+
+
+
+
+ }
+ >
+ ({
+ value,
+ inputDisplay: name,
+ 'data-test-subj': `addNewActionConnectorActionGroup-${index}-option-${value}`,
+ }))}
+ valueOfSelected={selectedActionGroup.id}
+ onChange={(group) => {
+ setActionGroupIdByIndex(group, index);
+ }}
+ />
+
+
+
+
+
+ )}
+
+
+
+ }
+ labelAppend={
+ canSave &&
+ actionTypesIndex &&
+ actionTypesIndex[actionConnector.actionTypeId].enabledInConfig ? (
+
+
+
+ ) : null
+ }
+ >
+ {
+ onConnectorSelected(selectedOptions[0].id ?? '');
+ }}
+ isClearable={false}
+ />
+
+
+
+
+ {ParamsFieldsComponent ? (
+
+
+
+
+
+ }
+ >
+
+
+ ) : null}
+
+ ) : (
+ checkEnabledResult.messageCard
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {selectedActionGroup && !isOpen && (
+
+ {selectedActionGroup.name}
+
+ )}
+
+ {checkEnabledResult.isEnabled === false && (
+
+
+
+ )}
+
+
+
+
+
+
+ }
+ extraAction={
+
+ }
+ >
+ {accordionContent}
+
+
+
+ );
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx
new file mode 100644
index 0000000000000..97baf4a36cb4c
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx
@@ -0,0 +1,153 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { Fragment } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiSpacer,
+ EuiAccordion,
+ EuiButtonIcon,
+ EuiEmptyPrompt,
+ EuiCallOut,
+ EuiText,
+} from '@elastic/eui';
+import { AlertAction, ActionTypeIndex } from '../../../types';
+import { hasSaveActionsCapability } from '../../lib/capabilities';
+import { ActionAccordionFormProps } from './action_form';
+
+type AddConnectorInFormProps = {
+ actionTypesIndex: ActionTypeIndex;
+ actionItem: AlertAction;
+ index: number;
+ onAddConnector: () => void;
+ onDeleteConnector: () => void;
+ emptyActionsIds: string[];
+} & Pick;
+
+export const AddConnectorInline = ({
+ actionTypesIndex,
+ actionItem,
+ index,
+ onAddConnector,
+ onDeleteConnector,
+ actionTypeRegistry,
+ emptyActionsIds,
+ defaultActionGroupId,
+ capabilities,
+}: AddConnectorInFormProps) => {
+ const canSave = hasSaveActionsCapability(capabilities);
+
+ const actionTypeName = actionTypesIndex
+ ? actionTypesIndex[actionItem.actionTypeId].name
+ : actionItem.actionTypeId;
+ const actionTypeRegistered = actionTypeRegistry.get(actionItem.actionTypeId);
+ if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null;
+
+ const noConnectorsLabel = (
+
+ );
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ extraAction={
+
+ }
+ paddingSize="l"
+ >
+ {canSave ? (
+ actionItem.id === emptyId) ? (
+ noConnectorsLabel
+ ) : (
+
+ )
+ }
+ actions={[
+
+
+ ,
+ ]}
+ />
+ ) : (
+
+
+
+
+
+ )}
+
+
+
+ );
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx
index cba9eea3cf3f7..71a3936ed5055 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx
@@ -65,8 +65,7 @@ describe('connector_add_modal', () => {
const wrapper = mountWithIntl(
{}}
+ onClose={() => {}}
actionType={actionType}
http={deps!.http}
actionTypeRegistry={deps!.actionTypeRegistry}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx
index 13ec8395aa557..de27256bf566c 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx
@@ -32,8 +32,7 @@ import {
interface ConnectorAddModalProps {
actionType: ActionType;
- addModalVisible: boolean;
- setAddModalVisibility: React.Dispatch>;
+ onClose: () => void;
postSaveEventHandler?: (savedAction: ActionConnector) => void;
http: HttpSetup;
actionTypeRegistry: ActionTypeRegistryContract;
@@ -48,8 +47,7 @@ interface ConnectorAddModalProps {
export const ConnectorAddModal = ({
actionType,
- addModalVisible,
- setAddModalVisibility,
+ onClose,
postSaveEventHandler,
http,
toastNotifications,
@@ -79,14 +77,11 @@ export const ConnectorAddModal = ({
>(undefined);
const closeModal = useCallback(() => {
- setAddModalVisibility(false);
setConnector(initialConnector);
setServerError(undefined);
- }, [initialConnector, setAddModalVisibility]);
+ onClose();
+ }, [initialConnector, onClose]);
- if (!addModalVisible) {
- return null;
- }
const actionTypeModel = actionTypeRegistry.get(actionType.id);
const errors = {
...actionTypeModel?.validateConnector(connector).errors,
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx
index 662db81101eee..70b6fb0b750dd 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx
@@ -11,7 +11,10 @@ import { Alert, ActionType, ValidationResult } from '../../../../types';
import { EuiTitle, EuiBadge, EuiFlexItem, EuiSwitch, EuiButtonEmpty, EuiText } from '@elastic/eui';
import { ViewInApp } from './view_in_app';
import { coreMock } from 'src/core/public/mocks';
-import { ALERTS_FEATURE_ID } from '../../../../../../alerts/common';
+import {
+ AlertExecutionStatusErrorReasons,
+ ALERTS_FEATURE_ID,
+} from '../../../../../../alerts/common';
const mockes = coreMock.createSetup();
@@ -125,7 +128,7 @@ describe('alert_details', () => {
status: 'error',
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
error: {
- reason: 'unknown',
+ reason: AlertExecutionStatusErrorReasons.Unknown,
message: 'test',
},
},
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx
index 9a637ea750f81..20ad9a8d7c701 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment, useState, useEffect, Suspense } from 'react';
+import React, { Fragment, useState, useEffect, Suspense, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@@ -153,9 +153,17 @@ export const AlertForm = ({
setAlertTypeModel(alert.alertTypeId ? alertTypeRegistry.get(alert.alertTypeId) : null);
}, [alert, alertTypeRegistry]);
- const setAlertProperty = (key: string, value: any) => {
- dispatch({ command: { type: 'setProperty' }, payload: { key, value } });
- };
+ const setAlertProperty = useCallback(
+ (key: string, value: any) => {
+ dispatch({ command: { type: 'setProperty' }, payload: { key, value } });
+ },
+ [dispatch]
+ );
+
+ const setActions = useCallback(
+ (updatedActions: AlertAction[]) => setAlertProperty('actions', updatedActions),
+ [setAlertProperty]
+ );
const setAlertParams = (key: string, value: any) => {
dispatch({ command: { type: 'setAlertParams' }, payload: { key, value } });
@@ -169,9 +177,12 @@ export const AlertForm = ({
dispatch({ command: { type: 'setAlertActionProperty' }, payload: { key, value, index } });
};
- const setActionParamsProperty = (key: string, value: any, index: number) => {
- dispatch({ command: { type: 'setAlertActionParams' }, payload: { key, value, index } });
- };
+ const setActionParamsProperty = useCallback(
+ (key: string, value: any, index: number) => {
+ dispatch({ command: { type: 'setAlertActionParams' }, payload: { key, value, index } });
+ },
+ [dispatch]
+ );
const tagsOptions = alert.tags ? alert.tags.map((label: string) => ({ label })) : [];
@@ -202,6 +213,7 @@ export const AlertForm = ({
label={item.name}
onClick={() => {
setAlertProperty('alertTypeId', item.id);
+ setActions([]);
setAlertTypeModel(item);
setAlertProperty('params', {});
if (alertTypesIndex && alertTypesIndex.has(item.id)) {
@@ -289,26 +301,25 @@ export const AlertForm = ({
/>
) : null}
- {canShowActions && defaultActionGroupId ? (
+ {canShowActions &&
+ defaultActionGroupId &&
+ alertTypeModel &&
+ alertTypesIndex?.has(alert.alertTypeId) ? (
- a.name.toUpperCase().localeCompare(b.name.toUpperCase())
- )
- : undefined
- }
+ messageVariables={actionVariablesFromAlertType(
+ alertTypesIndex.get(alert.alertTypeId)!
+ ).sort((a, b) => a.name.toUpperCase().localeCompare(b.name.toUpperCase()))}
defaultActionGroupId={defaultActionGroupId}
+ actionGroups={alertTypesIndex.get(alert.alertTypeId)!.actionGroups}
setActionIdByIndex={(id: string, index: number) => setActionProperty('id', id, index)}
- setAlertProperty={(updatedActions: AlertAction[]) =>
- setAlertProperty('actions', updatedActions)
- }
- setActionParamsProperty={(key: string, value: any, index: number) =>
- setActionParamsProperty(key, value, index)
+ setActionGroupIdByIndex={(group: string, index: number) =>
+ setActionProperty('group', group, index)
}
+ setAlertProperty={setActions}
+ setActionParamsProperty={setActionParamsProperty}
http={http}
actionTypeRegistry={actionTypeRegistry}
defaultActionMessage={alertTypeModel?.defaultActionMessage}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx
index 18cc7b540296e..c434ca9d21402 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx
@@ -17,7 +17,10 @@ import { AppContextProvider } from '../../../app_context';
import { chartPluginMock } from '../../../../../../../../src/plugins/charts/public/mocks';
import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks';
import { alertingPluginMock } from '../../../../../../alerts/public/mocks';
-import { ALERTS_FEATURE_ID } from '../../../../../../alerts/common';
+import {
+ AlertExecutionStatusErrorReasons,
+ ALERTS_FEATURE_ID,
+} from '../../../../../../alerts/common';
import { featuresPluginMock } from '../../../../../../features/public/mocks';
jest.mock('../../../lib/action_connector_api', () => ({
@@ -245,7 +248,7 @@ describe('alerts_list component with items', () => {
status: 'error',
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
error: {
- reason: 'unknown',
+ reason: AlertExecutionStatusErrorReasons.Unknown,
message: 'test',
},
},
diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap
index ab38ee9adc6c2..6fcce75cea70e 100644
--- a/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap
+++ b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap
@@ -1823,9 +1823,7 @@ exports[`EmptyState component renders error message when an error occurs 1`] = `
-
+
There was an error fetching your data.
diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx
index 6328789d03f29..0de5cd3ab31be 100644
--- a/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx
@@ -42,7 +42,9 @@ describe('EmptyState component', () => {
it(`renders error message when an error occurs`, () => {
const errors: IHttpFetchError[] = [
- new HttpFetchError('There was an error fetching your data.', 'error', {} as any),
+ new HttpFetchError('There was an error fetching your data.', 'error', {} as any, {} as any, {
+ body: { message: 'There was an error fetching your data.' },
+ }),
];
const component = mountWithRouter(
diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx
index f7b77df8497f9..165b123d8884d 100644
--- a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx
@@ -15,7 +15,7 @@ interface EmptyStateErrorProps {
export const EmptyStateError = ({ errors }: EmptyStateErrorProps) => {
const unauthorized = errors.find(
- (error: Error) => error.message && error.message.includes('unauthorized')
+ (error: IHttpFetchError) => error.message && error.message.includes('unauthorized')
);
return (
@@ -46,7 +46,9 @@ export const EmptyStateError = ({ errors }: EmptyStateErrorProps) => {
body={
{!unauthorized &&
- errors.map((error: Error) => {error.message}
)}
+ errors.map((error: IHttpFetchError) => (
+ {error.body.message || error.message}
+ ))}
}
/>
diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx
index 1d8a7a771e0c5..352369cfdb72b 100644
--- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx
@@ -17,6 +17,7 @@ import { MonitorListComponent, noItemsMessage } from '../monitor_list';
import { renderWithRouter, shallowWithRouter } from '../../../../lib';
import * as redux from 'react-redux';
import moment from 'moment';
+import { IHttpFetchError } from '../../../../../../../../src/core/public';
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => {
return {
@@ -187,7 +188,11 @@ describe('MonitorList component', () => {
it('renders error list', () => {
const component = shallowWithRouter(
diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx
index 5e0cc5d3dee1d..f31e25484a936 100644
--- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx
@@ -187,7 +187,7 @@ export const MonitorListComponent: ({
(
{
@@ -41,7 +42,7 @@ export const monitorListReducer = handleActions(
error: undefined,
list: { ...action.payload },
}),
- [String(getMonitorListFailure)]: (state: MonitorList, action: Action) => ({
+ [String(getMonitorListFailure)]: (state: MonitorList, action: Action) => ({
...state,
error: action.payload,
loading: false,
diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts
index b75c729c2104a..cd98ba1600d34 100644
--- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts
+++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts
@@ -9,17 +9,20 @@ import {
IRouter,
SavedObjectsClientContract,
ISavedObjectsRepository,
- ILegacyScopedClusterClient,
+ IScopedClusterClient,
+ ElasticsearchClient,
} from 'src/core/server';
import { UMKibanaRoute } from '../../../rest_api';
import { PluginSetupContract } from '../../../../../features/server';
import { DynamicSettings } from '../../../../common/runtime_types';
import { MlPluginSetup as MlSetup } from '../../../../../ml/server';
-export type ESAPICaller = ILegacyScopedClusterClient['callAsCurrentUser'];
-
export type UMElasticsearchQueryFn = (
- params: { callES: ESAPICaller; dynamicSettings: DynamicSettings } & P
+ params: {
+ callES: ElasticsearchClient;
+ esClient?: IScopedClusterClient;
+ dynamicSettings: DynamicSettings;
+ } & P
) => Promise;
export type UMSavedObjectsQueryFn = (
diff --git a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts
index a8969f2621f29..2126b484b1cfd 100644
--- a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts
+++ b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts
@@ -5,10 +5,14 @@
*/
import moment from 'moment';
-import { ISavedObjectsRepository, SavedObjectsClientContract } from 'kibana/server';
+import {
+ ISavedObjectsRepository,
+ ILegacyScopedClusterClient,
+ SavedObjectsClientContract,
+ ElasticsearchClient,
+} from 'kibana/server';
import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { PageViewParams, UptimeTelemetry, Usage } from './types';
-import { ESAPICaller } from '../framework';
import { savedObjectsAdapter } from '../../saved_objects';
interface UptimeTelemetryCollector {
@@ -21,6 +25,8 @@ const BUCKET_SIZE = 3600;
const BUCKET_NUMBER = 24;
export class KibanaTelemetryAdapter {
+ public static callCluster: ILegacyScopedClusterClient['callAsCurrentUser'] | ElasticsearchClient;
+
public static registerUsageCollector = (
usageCollector: UsageCollectionSetup,
getSavedObjectsClient: () => ISavedObjectsRepository | undefined
@@ -125,7 +131,7 @@ export class KibanaTelemetryAdapter {
}
public static async countNoOfUniqueMonitorAndLocations(
- callCluster: ESAPICaller,
+ callCluster: ILegacyScopedClusterClient['callAsCurrentUser'] | ElasticsearchClient,
savedObjectsClient: ISavedObjectsRepository | SavedObjectsClientContract
) {
const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient);
@@ -187,7 +193,11 @@ export class KibanaTelemetryAdapter {
},
};
- const result = await callCluster('search', params);
+ const { body: result } =
+ typeof callCluster === 'function'
+ ? await callCluster('search', params)
+ : await callCluster.search(params);
+
const numberOfUniqueMonitors: number = result?.aggregations?.unique_monitors?.value ?? 0;
const numberOfUniqueLocations: number = result?.aggregations?.unique_locations?.value ?? 0;
const monitorNameStats: any = result?.aggregations?.monitor_name;
diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts
index 06b298aedeb2b..ccb1e5a40ad2d 100644
--- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts
+++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts
@@ -56,6 +56,8 @@ const mockOptions = (
services = alertsMock.createAlertServices(),
state = {}
): any => {
+ services.scopedClusterClient = jest.fn() as any;
+
services.savedObjectsClient.get.mockResolvedValue({
id: '',
type: '',
@@ -282,7 +284,8 @@ describe('status check alert', () => {
expect.assertions(5);
toISOStringSpy.mockImplementation(() => 'foo date string');
const mockGetter: jest.Mock = jest.fn();
- mockGetter.mockReturnValue([
+
+ mockGetter.mockReturnValueOnce([
{
monitorId: 'first',
location: 'harrisburg',
@@ -326,6 +329,7 @@ describe('status check alert', () => {
const state = await alert.executor(options);
const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results;
expect(mockGetter).toHaveBeenCalledTimes(1);
+
expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts
index 9dddc0035f690..d4c26fe83b5fc 100644
--- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts
+++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts
@@ -12,12 +12,12 @@ import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts';
import { commonStateTranslations, durationAnomalyTranslations } from './translations';
import { AnomaliesTableRecord } from '../../../../ml/common/types/anomalies';
import { getSeverityType } from '../../../../ml/common/util/anomaly_utils';
-import { savedObjectsAdapter } from '../saved_objects';
import { UptimeCorePlugins } from '../adapters/framework';
import { UptimeAlertTypeFactory } from './types';
import { Ping } from '../../../common/runtime_types/ping';
import { getMLJobId } from '../../../common/lib';
import { getLatestMonitor } from '../requests/get_latest_monitor';
+import { uptimeAlertWrapper } from './uptime_alert_wrapper';
const { DURATION_ANOMALY } = ACTION_GROUP_DEFINITIONS;
@@ -61,61 +61,58 @@ const getAnomalies = async (
);
};
-export const durationAnomalyAlertFactory: UptimeAlertTypeFactory = (_server, _libs, plugins) => ({
- id: 'xpack.uptime.alerts.durationAnomaly',
- name: durationAnomalyTranslations.alertFactoryName,
- validate: {
- params: schema.object({
- monitorId: schema.string(),
- severity: schema.number(),
- }),
- },
- defaultActionGroupId: DURATION_ANOMALY.id,
- actionGroups: [
- {
- id: DURATION_ANOMALY.id,
- name: DURATION_ANOMALY.name,
+export const durationAnomalyAlertFactory: UptimeAlertTypeFactory = (_server, _libs, plugins) =>
+ uptimeAlertWrapper({
+ id: 'xpack.uptime.alerts.durationAnomaly',
+ name: durationAnomalyTranslations.alertFactoryName,
+ validate: {
+ params: schema.object({
+ monitorId: schema.string(),
+ severity: schema.number(),
+ }),
},
- ],
- actionVariables: {
- context: [],
- state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations],
- },
- producer: 'uptime',
- async executor(options) {
- const {
- services: { alertInstanceFactory, callCluster, savedObjectsClient },
- state,
- params,
- } = options;
+ defaultActionGroupId: DURATION_ANOMALY.id,
+ actionGroups: [
+ {
+ id: DURATION_ANOMALY.id,
+ name: DURATION_ANOMALY.name,
+ },
+ ],
+ actionVariables: {
+ context: [],
+ state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations],
+ },
+ async executor({ options, esClient, savedObjectsClient, dynamicSettings }) {
+ const {
+ services: { alertInstanceFactory },
+ state,
+ params,
+ } = options;
- const { anomalies } =
- (await getAnomalies(plugins, savedObjectsClient, params, state.lastCheckedAt)) ?? {};
+ const { anomalies } =
+ (await getAnomalies(plugins, savedObjectsClient, params, state.lastCheckedAt)) ?? {};
- const foundAnomalies = anomalies?.length > 0;
+ const foundAnomalies = anomalies?.length > 0;
- if (foundAnomalies) {
- const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(
- savedObjectsClient
- );
- const monitorInfo = await getLatestMonitor({
- dynamicSettings,
- callES: callCluster,
- dateStart: 'now-15m',
- dateEnd: 'now',
- monitorId: params.monitorId,
- });
- anomalies.forEach((anomaly, index) => {
- const alertInstance = alertInstanceFactory(DURATION_ANOMALY.id + index);
- const summary = getAnomalySummary(anomaly, monitorInfo);
- alertInstance.replaceState({
- ...updateState(state, false),
- ...summary,
+ if (foundAnomalies) {
+ const monitorInfo = await getLatestMonitor({
+ dynamicSettings,
+ callES: esClient,
+ dateStart: 'now-15m',
+ dateEnd: 'now',
+ monitorId: params.monitorId,
+ });
+ anomalies.forEach((anomaly, index) => {
+ const alertInstance = alertInstanceFactory(DURATION_ANOMALY.id + index);
+ const summary = getAnomalySummary(anomaly, monitorInfo);
+ alertInstance.replaceState({
+ ...updateState(state, false),
+ ...summary,
+ });
+ alertInstance.scheduleActions(DURATION_ANOMALY.id);
});
- alertInstance.scheduleActions(DURATION_ANOMALY.id);
- });
- }
+ }
- return updateState(state, foundAnomalies);
- },
-});
+ return updateState(state, foundAnomalies);
+ },
+ });
diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts
index 7feb916046e3a..b1b3666b40dc6 100644
--- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts
+++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts
@@ -26,7 +26,6 @@ import { GetMonitorStatusResult } from '../requests/get_monitor_status';
import { UNNAMED_LOCATION } from '../../../common/constants';
import { uptimeAlertWrapper } from './uptime_alert_wrapper';
import { MonitorStatusTranslations } from '../../../common/translations';
-import { ESAPICaller } from '../adapters/framework';
import { getUptimeIndexPattern, IndexPatternTitleAndFields } from '../requests/get_index_pattern';
import { UMServerLibs } from '../lib';
@@ -81,7 +80,6 @@ export const generateFilterDSL = async (
export const formatFilterString = async (
dynamicSettings: DynamicSettings,
- callES: ESAPICaller,
esClient: ElasticsearchClient,
filters: StatusCheckFilters,
search: string,
@@ -90,9 +88,8 @@ export const formatFilterString = async (
await generateFilterDSL(
() =>
libs?.requests?.getIndexPattern
- ? libs?.requests?.getIndexPattern({ callES, esClient, dynamicSettings })
+ ? libs?.requests?.getIndexPattern({ esClient, dynamicSettings })
: getUptimeIndexPattern({
- callES,
esClient,
dynamicSettings,
}),
@@ -237,12 +234,15 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) =
],
state: [...commonMonitorStateI18, ...commonStateTranslations],
},
- async executor(
- { params: rawParams, state, services: { alertInstanceFactory } },
- callES,
+ async executor({
+ options: {
+ params: rawParams,
+ state,
+ services: { alertInstanceFactory },
+ },
esClient,
- dynamicSettings
- ) {
+ dynamicSettings,
+ }) {
const {
filters,
search,
@@ -258,7 +258,6 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) =
const filterString = await formatFilterString(
dynamicSettings,
- callES,
esClient,
filters,
search,
@@ -278,7 +277,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) =
// after that shouldCheckStatus should be explicitly false
if (!(!oldVersionTimeRange && shouldCheckStatus === false)) {
downMonitorsByLocation = await libs.requests.getMonitorStatus({
- callES,
+ callES: esClient,
dynamicSettings,
timerange,
numTimes,
@@ -311,7 +310,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) =
let availabilityResults: GetMonitorAvailabilityResult[] = [];
if (shouldCheckAvailability) {
availabilityResults = await libs.requests.getMonitorAvailability({
- callES,
+ callES: esClient,
dynamicSettings,
...availability,
filters: JSON.stringify(filterString) || undefined,
diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.ts
index d4853ad7a9cb0..11f602d10bf51 100644
--- a/x-pack/plugins/uptime/server/lib/alerts/tls.ts
+++ b/x-pack/plugins/uptime/server/lib/alerts/tls.ts
@@ -7,13 +7,13 @@
import moment from 'moment';
import { schema } from '@kbn/config-schema';
import { UptimeAlertTypeFactory } from './types';
-import { savedObjectsAdapter } from '../saved_objects';
import { updateState } from './common';
import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
import { Cert, CertResult } from '../../../common/runtime_types';
import { commonStateTranslations, tlsTranslations } from './translations';
import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs';
+import { uptimeAlertWrapper } from './uptime_alert_wrapper';
const { TLS } = ACTION_GROUP_DEFINITIONS;
@@ -82,74 +82,73 @@ export const getCertSummary = (
};
};
-export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({
- id: 'xpack.uptime.alerts.tls',
- name: tlsTranslations.alertFactoryName,
- validate: {
- params: schema.object({}),
- },
- defaultActionGroupId: TLS.id,
- actionGroups: [
- {
- id: TLS.id,
- name: TLS.name,
+export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, libs) =>
+ uptimeAlertWrapper({
+ id: 'xpack.uptime.alerts.tls',
+ name: tlsTranslations.alertFactoryName,
+ validate: {
+ params: schema.object({}),
},
- ],
- actionVariables: {
- context: [],
- state: [...tlsTranslations.actionVariables, ...commonStateTranslations],
- },
- producer: 'uptime',
- async executor(options) {
- const {
- services: { alertInstanceFactory, callCluster, savedObjectsClient },
- state,
- } = options;
- const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient);
-
- const { certs, total }: CertResult = await libs.requests.getCerts({
- callES: callCluster,
- dynamicSettings,
- from: DEFAULT_FROM,
- to: DEFAULT_TO,
- index: 0,
- size: DEFAULT_SIZE,
- notValidAfter: `now+${
- dynamicSettings?.certExpirationThreshold ??
- DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold
- }d`,
- notValidBefore: `now-${
- dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold
- }d`,
- sortBy: 'common_name',
- direction: 'desc',
- });
-
- const foundCerts = total > 0;
-
- if (foundCerts) {
- const absoluteExpirationThreshold = moment()
- .add(
- dynamicSettings.certExpirationThreshold ??
- DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
- 'd'
- )
- .valueOf();
- const absoluteAgeThreshold = moment()
- .subtract(
- dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
- 'd'
- )
- .valueOf();
- const alertInstance = alertInstanceFactory(TLS.id);
- const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold);
- alertInstance.replaceState({
- ...updateState(state, foundCerts),
- ...summary,
+ defaultActionGroupId: TLS.id,
+ actionGroups: [
+ {
+ id: TLS.id,
+ name: TLS.name,
+ },
+ ],
+ actionVariables: {
+ context: [],
+ state: [...tlsTranslations.actionVariables, ...commonStateTranslations],
+ },
+ async executor({ options, dynamicSettings, esClient }) {
+ const {
+ services: { alertInstanceFactory },
+ state,
+ } = options;
+
+ const { certs, total }: CertResult = await libs.requests.getCerts({
+ callES: esClient,
+ dynamicSettings,
+ from: DEFAULT_FROM,
+ to: DEFAULT_TO,
+ index: 0,
+ size: DEFAULT_SIZE,
+ notValidAfter: `now+${
+ dynamicSettings?.certExpirationThreshold ??
+ DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold
+ }d`,
+ notValidBefore: `now-${
+ dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold
+ }d`,
+ sortBy: 'common_name',
+ direction: 'desc',
});
- alertInstance.scheduleActions(TLS.id);
- }
- return updateState(state, foundCerts);
- },
-});
+ const foundCerts = total > 0;
+
+ if (foundCerts) {
+ const absoluteExpirationThreshold = moment()
+ .add(
+ dynamicSettings.certExpirationThreshold ??
+ DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
+ 'd'
+ )
+ .valueOf();
+ const absoluteAgeThreshold = moment()
+ .subtract(
+ dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
+ 'd'
+ )
+ .valueOf();
+ const alertInstance = alertInstanceFactory(TLS.id);
+ const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold);
+ alertInstance.replaceState({
+ ...updateState(state, foundCerts),
+ ...summary,
+ });
+ alertInstance.scheduleActions(TLS.id);
+ }
+
+ return updateState(state, foundCerts);
+ },
+ });
diff --git a/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts b/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts
index 390b6d347996c..0961eb6557891 100644
--- a/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts
+++ b/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts
@@ -4,18 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ILegacyScopedClusterClient, ElasticsearchClient } from 'kibana/server';
+import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server';
import { AlertExecutorOptions, AlertType, AlertTypeState } from '../../../../alerts/server';
import { savedObjectsAdapter } from '../saved_objects';
import { DynamicSettings } from '../../../common/runtime_types';
export interface UptimeAlertType extends Omit {
- executor: (
- options: AlertExecutorOptions,
- callES: ILegacyScopedClusterClient['callAsCurrentUser'],
- esClient: ElasticsearchClient,
- dynamicSettings: DynamicSettings
- ) => Promise;
+ executor: ({
+ options,
+ esClient,
+ dynamicSettings,
+ }: {
+ options: AlertExecutorOptions;
+ esClient: ElasticsearchClient;
+ dynamicSettings: DynamicSettings;
+ savedObjectsClient: SavedObjectsClientContract;
+ }) => Promise;
}
export const uptimeAlertWrapper = (uptimeAlert: UptimeAlertType) => ({
@@ -23,13 +27,13 @@ export const uptimeAlertWrapper = (uptimeAlert: UptimeAlertType) => ({
producer: 'uptime',
executor: async (options: AlertExecutorOptions) => {
const {
- services: { callCluster: callES, scopedClusterClient },
+ services: { scopedClusterClient: esClient, savedObjectsClient },
} = options;
const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(
options.services.savedObjectsClient
);
- return uptimeAlert.executor(options, callES, scopedClusterClient, dynamicSettings);
+ return uptimeAlert.executor({ options, esClient, dynamicSettings, savedObjectsClient });
},
});
diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap b/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap
index 97b97f8440758..6ab55c2afddda 100644
--- a/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap
+++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap
@@ -2,7 +2,6 @@
exports[`ElasticsearchMonitorsAdapter getMonitorChartsData will provide expected filters 1`] = `
Array [
- "search",
Object {
"body": Object {
"aggs": Object {
@@ -26,9 +25,6 @@ Array [
"buckets": 25,
"field": "@timestamp",
},
- "date_histogram": Object {
- "fixed_interval": "36000ms",
- },
},
},
"query": Object {
diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts
index 4faaed53bebf2..c0b94b19b7582 100644
--- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts
@@ -6,10 +6,10 @@
import { getCerts } from '../get_certs';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants';
+import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks';
describe('getCerts', () => {
let mockHits: any;
- let mockCallES: jest.Mock;
beforeEach(() => {
mockHits = [
@@ -79,17 +79,20 @@ describe('getCerts', () => {
},
},
];
- mockCallES = jest.fn();
- mockCallES.mockImplementation(() => ({
- hits: {
- hits: mockHits,
- },
- }));
});
it('parses query result and returns expected values', async () => {
+ const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
+ mockEsClient.search.mockResolvedValueOnce({
+ body: {
+ hits: {
+ hits: mockHits,
+ },
+ },
+ } as any);
+
const result = await getCerts({
- callES: mockCallES,
+ callES: mockEsClient,
dynamicSettings: {
heartbeatIndices: 'heartbeat*',
certAgeThreshold: DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
@@ -126,10 +129,9 @@ describe('getCerts', () => {
"total": 0,
}
`);
- expect(mockCallES.mock.calls).toMatchInlineSnapshot(`
+ expect(mockEsClient.search.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
- "search",
Object {
"body": Object {
"_source": Array [
diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts
index bd353b62df828..9503174ed104c 100644
--- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts
@@ -6,6 +6,7 @@
import { getLatestMonitor } from '../get_latest_monitor';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants';
+import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks';
describe('getLatestMonitor', () => {
let expectedGetLatestSearchParams: any;
@@ -44,29 +45,33 @@ describe('getLatestMonitor', () => {
},
};
mockEsSearchResult = {
- hits: {
- hits: [
- {
- _id: 'fejwio32',
- _source: {
- '@timestamp': '123456',
- monitor: {
- duration: {
- us: 12345,
+ body: {
+ hits: {
+ hits: [
+ {
+ _id: 'fejwio32',
+ _source: {
+ '@timestamp': '123456',
+ monitor: {
+ duration: {
+ us: 12345,
+ },
+ id: 'testMonitor',
+ status: 'down',
+ type: 'http',
},
- id: 'testMonitor',
- status: 'down',
- type: 'http',
},
},
- },
- ],
+ ],
+ },
},
};
});
it('returns data in expected shape', async () => {
- const mockEsClient = jest.fn(async (_request: any, _params: any) => mockEsSearchResult);
+ const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
+ mockEsClient.search.mockResolvedValueOnce(mockEsSearchResult);
+
const result = await getLatestMonitor({
callES: mockEsClient,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
@@ -94,6 +99,6 @@ describe('getLatestMonitor', () => {
expect(result.timestamp).toBe('123456');
expect(result.monitor).not.toBeFalsy();
expect(result?.monitor?.id).toBe('testMonitor');
- expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetLatestSearchParams);
+ expect(mockEsClient.search).toHaveBeenCalledWith(expectedGetLatestSearchParams);
});
});
diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts
index 015d9a4925f3e..e8df65d410167 100644
--- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts
@@ -72,7 +72,7 @@ const genBucketItem = ({
describe('monitor availability', () => {
describe('getMonitorAvailability', () => {
it('applies bool filters to params', async () => {
- const [callES, esMock] = setupMockEsCompositeQuery<
+ const esMock = setupMockEsCompositeQuery<
AvailabilityKey,
GetMonitorAvailabilityResult,
AvailabilityDoc
@@ -109,16 +109,15 @@ describe('monitor availability', () => {
}
}`;
await getMonitorAvailability({
- callES,
+ callES: esMock,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
filters: exampleFilter,
range: 2,
rangeUnit: 'w',
threshold: '54',
});
- expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(1);
- const [method, params] = esMock.callAsCurrentUser.mock.calls[0];
- expect(method).toEqual('search');
+ expect(esMock.search).toHaveBeenCalledTimes(1);
+ const [params] = esMock.search.mock.calls[0];
expect(params).toMatchInlineSnapshot(`
Object {
"body": Object {
@@ -245,7 +244,7 @@ describe('monitor availability', () => {
});
it('fetches a single page of results', async () => {
- const [callES, esMock] = setupMockEsCompositeQuery<
+ const esMock = setupMockEsCompositeQuery<
AvailabilityKey,
GetMonitorAvailabilityResult,
AvailabilityDoc
@@ -288,13 +287,12 @@ describe('monitor availability', () => {
threshold: '69',
};
const result = await getMonitorAvailability({
- callES,
+ callES: esMock,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
...clientParameters,
});
- expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(1);
- const [method, params] = esMock.callAsCurrentUser.mock.calls[0];
- expect(method).toEqual('search');
+ expect(esMock.search).toHaveBeenCalledTimes(1);
+ const [params] = esMock.search.mock.calls[0];
expect(params).toMatchInlineSnapshot(`
Object {
"body": Object {
@@ -458,7 +456,7 @@ describe('monitor availability', () => {
});
it('fetches multiple pages', async () => {
- const [callES, esMock] = setupMockEsCompositeQuery<
+ const esMock = setupMockEsCompositeQuery<
AvailabilityKey,
GetMonitorAvailabilityResult,
AvailabilityDoc
@@ -512,7 +510,7 @@ describe('monitor availability', () => {
genBucketItem
);
const result = await getMonitorAvailability({
- callES,
+ callES: esMock,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
range: 3,
rangeUnit: 'M',
@@ -606,9 +604,8 @@ describe('monitor availability', () => {
},
]
`);
- const [method, params] = esMock.callAsCurrentUser.mock.calls[0];
- expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(2);
- expect(method).toEqual('search');
+ const [params] = esMock.search.mock.calls[0];
+ expect(esMock.search).toHaveBeenCalledTimes(2);
expect(params).toMatchInlineSnapshot(`
Object {
"body": Object {
@@ -701,9 +698,9 @@ describe('monitor availability', () => {
"index": "heartbeat-8*",
}
`);
- expect(esMock.callAsCurrentUser.mock.calls[1]).toMatchInlineSnapshot(`
+
+ expect(esMock.search.mock.calls[1]).toMatchInlineSnapshot(`
Array [
- "search",
Object {
"body": Object {
"aggs": Object {
@@ -803,7 +800,7 @@ describe('monitor availability', () => {
});
it('does not overwrite filters', async () => {
- const [callES, esMock] = setupMockEsCompositeQuery<
+ const esMock = setupMockEsCompositeQuery<
AvailabilityKey,
GetMonitorAvailabilityResult,
AvailabilityDoc
@@ -816,14 +813,14 @@ describe('monitor availability', () => {
genBucketItem
);
await getMonitorAvailability({
- callES,
+ callES: esMock,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
range: 3,
rangeUnit: 's',
threshold: '99',
filters: JSON.stringify({ bool: { filter: [{ term: { 'monitor.id': 'foo' } }] } }),
});
- const [, params] = esMock.callAsCurrentUser.mock.calls[0];
+ const [params] = esMock.search.mock.calls[0];
expect(params).toMatchInlineSnapshot(`
Object {
"body": Object {
diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts
index 2ebe670bc43c1..9edd3e2e160d2 100644
--- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts
@@ -8,37 +8,37 @@ import { set } from '@elastic/safer-lodash-set';
import mockChartsData from './monitor_charts_mock.json';
import { getMonitorDurationChart } from '../get_monitor_duration';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants';
+import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks';
describe('ElasticsearchMonitorsAdapter', () => {
it('getMonitorChartsData will provide expected filters', async () => {
expect.assertions(2);
- const searchMock = jest.fn();
- const search = searchMock.bind({});
+ const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
await getMonitorDurationChart({
- callES: search,
+ callES: mockEsClient,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
monitorId: 'fooID',
dateStart: 'now-15m',
dateEnd: 'now',
});
- expect(searchMock).toHaveBeenCalledTimes(1);
+ expect(mockEsClient.search).toHaveBeenCalledTimes(1);
// protect against possible rounding errors polluting the snapshot comparison
set(
- searchMock.mock.calls[0][1],
+ mockEsClient.search.mock.calls[0],
'body.aggs.timeseries.date_histogram.fixed_interval',
'36000ms'
);
- expect(searchMock.mock.calls[0]).toMatchSnapshot();
+ expect(mockEsClient.search.mock.calls[0]).toMatchSnapshot();
});
it('inserts empty buckets for missing data', async () => {
- const searchMock = jest.fn();
- searchMock.mockReturnValue(mockChartsData);
- const search = searchMock.bind({});
+ const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
+ mockEsClient.search.mockResolvedValueOnce(mockChartsData as any);
+
expect(
await getMonitorDurationChart({
- callES: search,
+ callES: mockEsClient,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
monitorId: 'id',
dateStart: 'now-15m',
diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts
index e61d736e37106..949bc39f07259 100644
--- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts
@@ -42,7 +42,7 @@ const genBucketItem = ({
describe('getMonitorStatus', () => {
it('applies bool filters to params', async () => {
- const [callES, esMock] = setupMockEsCompositeQuery(
+ const esMock = setupMockEsCompositeQuery(
[],
genBucketItem
);
@@ -78,7 +78,7 @@ describe('getMonitorStatus', () => {
},
};
await getMonitorStatus({
- callES,
+ callES: esMock,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
filters: exampleFilter,
locations: [],
@@ -88,9 +88,8 @@ describe('getMonitorStatus', () => {
to: 'now-1m',
},
});
- expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(1);
- const [method, params] = esMock.callAsCurrentUser.mock.calls[0];
- expect(method).toEqual('search');
+ expect(esMock.search).toHaveBeenCalledTimes(1);
+ const [params] = esMock.search.mock.calls[0];
expect(params).toMatchInlineSnapshot(`
Object {
"body": Object {
@@ -190,12 +189,12 @@ describe('getMonitorStatus', () => {
});
it('applies locations to params', async () => {
- const [callES, esMock] = setupMockEsCompositeQuery(
+ const esMock = setupMockEsCompositeQuery(
[],
genBucketItem
);
await getMonitorStatus({
- callES,
+ callES: esMock,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
locations: ['fairbanks', 'harrisburg'],
numTimes: 1,
@@ -204,9 +203,8 @@ describe('getMonitorStatus', () => {
to: 'now',
},
});
- expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(1);
- const [method, params] = esMock.callAsCurrentUser.mock.calls[0];
- expect(method).toEqual('search');
+ expect(esMock.search).toHaveBeenCalledTimes(1);
+ const [params] = esMock.search.mock.calls[0];
expect(params).toMatchInlineSnapshot(`
Object {
"body": Object {
@@ -291,7 +289,7 @@ describe('getMonitorStatus', () => {
});
it('properly assigns filters for complex kuery filters', async () => {
- const [callES, esMock] = setupMockEsCompositeQuery(
+ const esMock = setupMockEsCompositeQuery(
[{ bucketCriteria: [] }],
genBucketItem
);
@@ -353,12 +351,12 @@ describe('getMonitorStatus', () => {
},
};
await getMonitorStatus({
- callES,
+ callES: esMock,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
...clientParameters,
});
- expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(1);
- const [, params] = esMock.callAsCurrentUser.mock.calls[0];
+ expect(esMock.search).toHaveBeenCalledTimes(1);
+ const [params] = esMock.search.mock.calls[0];
expect(params).toMatchInlineSnapshot(`
Object {
"body": Object {
@@ -476,7 +474,7 @@ describe('getMonitorStatus', () => {
});
it('properly assigns filters for complex kuery filters object', async () => {
- const [callES, esMock] = setupMockEsCompositeQuery(
+ const esMock = setupMockEsCompositeQuery(
[{ bucketCriteria: [] }],
genBucketItem
);
@@ -498,12 +496,12 @@ describe('getMonitorStatus', () => {
},
};
await getMonitorStatus({
- callES,
+ callES: esMock,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
...clientParameters,
});
- expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(1);
- const [, params] = esMock.callAsCurrentUser.mock.calls[0];
+ expect(esMock.search).toHaveBeenCalledTimes(1);
+ const [params] = esMock.search.mock.calls[0];
expect(params).toMatchInlineSnapshot(`
Object {
"body": Object {
@@ -581,7 +579,7 @@ describe('getMonitorStatus', () => {
});
it('fetches single page of results', async () => {
- const [callES, esMock] = setupMockEsCompositeQuery(
+ const esMock = setupMockEsCompositeQuery(
[
{
bucketCriteria: [
@@ -618,13 +616,12 @@ describe('getMonitorStatus', () => {
},
};
const result = await getMonitorStatus({
- callES,
+ callES: esMock,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
...clientParameters,
});
- expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(1);
- const [method, params] = esMock.callAsCurrentUser.mock.calls[0];
- expect(method).toEqual('search');
+ expect(esMock.search).toHaveBeenCalledTimes(1);
+ const [params] = esMock.search.mock.calls[0];
expect(params).toMatchInlineSnapshot(`
Object {
"body": Object {
@@ -792,12 +789,12 @@ describe('getMonitorStatus', () => {
],
},
];
- const [callES] = setupMockEsCompositeQuery(
+ const esMock = setupMockEsCompositeQuery(
criteria,
genBucketItem
);
const result = await getMonitorStatus({
- callES,
+ callES: esMock,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
locations: [],
numTimes: 5,
diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts
index ac940ffb6676f..86e5f2876ca28 100644
--- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts
@@ -6,6 +6,7 @@
import { getPingHistogram } from '../get_ping_histogram';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants';
+import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks';
describe('getPingHistogram', () => {
const standardMockResponse: any = {
@@ -37,25 +38,28 @@ describe('getPingHistogram', () => {
it.skip('returns a single bucket if array has 1', async () => {
expect.assertions(2);
- const mockEsClient = jest.fn();
- mockEsClient.mockReturnValue({
- aggregations: {
- timeseries: {
- buckets: [
- {
- key: 1,
- up: {
- doc_count: 2,
+ const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
+
+ mockEsClient.search.mockResolvedValueOnce({
+ body: {
+ aggregations: {
+ timeseries: {
+ buckets: [
+ {
+ key: 1,
+ up: {
+ doc_count: 2,
+ },
+ down: {
+ doc_count: 1,
+ },
},
- down: {
- doc_count: 1,
- },
- },
- ],
- interval: '10s',
+ ],
+ interval: '10s',
+ },
},
},
- });
+ } as any);
const result = await getPingHistogram({
callES: mockEsClient,
@@ -64,16 +68,20 @@ describe('getPingHistogram', () => {
to: 'now',
});
- expect(mockEsClient).toHaveBeenCalledTimes(1);
+ expect(mockEsClient.search).toHaveBeenCalledTimes(1);
expect(result).toMatchSnapshot();
});
it('returns expected result for no status filter', async () => {
expect.assertions(2);
- const mockEsClient = jest.fn();
+
+ const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
standardMockResponse.aggregations.timeseries.interval = '1m';
- mockEsClient.mockReturnValue(standardMockResponse);
+
+ mockEsClient.search.mockResolvedValueOnce({
+ body: standardMockResponse,
+ } as any);
const result = await getPingHistogram({
callES: mockEsClient,
@@ -83,50 +91,53 @@ describe('getPingHistogram', () => {
filters: '',
});
- expect(mockEsClient).toHaveBeenCalledTimes(1);
+ expect(mockEsClient.search).toHaveBeenCalledTimes(1);
expect(result).toMatchSnapshot();
});
it('handles status + additional user queries', async () => {
expect.assertions(2);
- const mockEsClient = jest.fn();
-
- mockEsClient.mockReturnValue({
- aggregations: {
- timeseries: {
- buckets: [
- {
- key: 1,
- up: {
- doc_count: 2,
- },
- down: {
- doc_count: 1,
- },
- },
- {
- key: 2,
- up: {
- doc_count: 2,
- },
- down: {
- doc_count: 2,
+
+ const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
+
+ mockEsClient.search.mockResolvedValueOnce({
+ body: {
+ aggregations: {
+ timeseries: {
+ buckets: [
+ {
+ key: 1,
+ up: {
+ doc_count: 2,
+ },
+ down: {
+ doc_count: 1,
+ },
},
- },
- {
- key: 3,
- up: {
- doc_count: 3,
+ {
+ key: 2,
+ up: {
+ doc_count: 2,
+ },
+ down: {
+ doc_count: 2,
+ },
},
- down: {
- doc_count: 1,
+ {
+ key: 3,
+ up: {
+ doc_count: 3,
+ },
+ down: {
+ doc_count: 1,
+ },
},
- },
- ],
- interval: '1h',
+ ],
+ interval: '1h',
+ },
},
},
- });
+ } as any);
const searchFilter = {
bool: {
@@ -146,50 +157,52 @@ describe('getPingHistogram', () => {
monitorId: undefined,
});
- expect(mockEsClient).toHaveBeenCalledTimes(1);
+ expect(mockEsClient.search).toHaveBeenCalledTimes(1);
expect(result).toMatchSnapshot();
});
it('handles simple_text_query without issues', async () => {
expect.assertions(2);
- const mockEsClient = jest.fn();
-
- mockEsClient.mockReturnValue({
- aggregations: {
- timeseries: {
- buckets: [
- {
- key: 1,
- up: {
- doc_count: 2,
+ const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
+
+ mockEsClient.search.mockResolvedValueOnce({
+ body: {
+ aggregations: {
+ timeseries: {
+ buckets: [
+ {
+ key: 1,
+ up: {
+ doc_count: 2,
+ },
+ down: {
+ doc_count: 1,
+ },
},
- down: {
- doc_count: 1,
+ {
+ key: 2,
+ up: {
+ doc_count: 1,
+ },
+ down: {
+ doc_count: 2,
+ },
},
- },
- {
- key: 2,
- up: {
- doc_count: 1,
- },
- down: {
- doc_count: 2,
+ {
+ key: 3,
+ up: {
+ doc_count: 3,
+ },
+ down: {
+ doc_count: 1,
+ },
},
- },
- {
- key: 3,
- up: {
- doc_count: 3,
- },
- down: {
- doc_count: 1,
- },
- },
- ],
- interval: '1m',
+ ],
+ interval: '1m',
+ },
},
},
- });
+ } as any);
const filters = `{"bool":{"must":[{"simple_query_string":{"query":"http"}}]}}`;
const result = await getPingHistogram({
@@ -200,7 +213,7 @@ describe('getPingHistogram', () => {
filters,
});
- expect(mockEsClient).toHaveBeenCalledTimes(1);
+ expect(mockEsClient.search).toHaveBeenCalledTimes(1);
expect(result).toMatchSnapshot();
});
});
diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts
index cb84cc2eb05b6..f313cce9f758b 100644
--- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts
@@ -7,6 +7,7 @@
import { getPings } from '../get_pings';
import { set } from '@elastic/safer-lodash-set';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants';
+import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks';
describe('getAll', () => {
let mockEsSearchResult: any;
@@ -49,15 +50,17 @@ describe('getAll', () => {
},
];
mockEsSearchResult = {
- hits: {
- total: {
- value: mockHits.length,
+ body: {
+ hits: {
+ total: {
+ value: mockHits.length,
+ },
+ hits: mockHits,
},
- hits: mockHits,
- },
- aggregations: {
- locations: {
- buckets: [{ key: 'foo' }],
+ aggregations: {
+ locations: {
+ buckets: [{ key: 'foo' }],
+ },
},
},
};
@@ -84,8 +87,9 @@ describe('getAll', () => {
});
it('returns data in the appropriate shape', async () => {
- const mockEsClient = jest.fn();
- mockEsClient.mockReturnValue(mockEsSearchResult);
+ const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
+
+ mockEsClient.search.mockResolvedValueOnce(mockEsSearchResult);
const result = await getPings({
callES: mockEsClient,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
@@ -102,12 +106,12 @@ describe('getAll', () => {
expect(pings[0].timestamp).toBe('2018-10-30T18:51:59.792Z');
expect(pings[1].timestamp).toBe('2018-10-30T18:53:59.792Z');
expect(pings[2].timestamp).toBe('2018-10-30T18:55:59.792Z');
- expect(mockEsClient).toHaveBeenCalledTimes(1);
+ expect(mockEsClient.search).toHaveBeenCalledTimes(1);
});
it('creates appropriate sort and size parameters', async () => {
- const mockEsClient = jest.fn();
- mockEsClient.mockReturnValue(mockEsSearchResult);
+ const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
+ mockEsClient.search.mockResolvedValueOnce(mockEsSearchResult);
await getPings({
callES: mockEsClient,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
@@ -117,10 +121,9 @@ describe('getAll', () => {
});
set(expectedGetAllParams, 'body.sort[0]', { timestamp: { order: 'asc' } });
- expect(mockEsClient).toHaveBeenCalledTimes(1);
- expect(mockEsClient.mock.calls[0]).toMatchInlineSnapshot(`
+ expect(mockEsClient.search).toHaveBeenCalledTimes(1);
+ expect(mockEsClient.search.mock.calls[0]).toMatchInlineSnapshot(`
Array [
- "search",
Object {
"body": Object {
"aggregations": Object {
@@ -186,8 +189,8 @@ describe('getAll', () => {
});
it('omits the sort param when no sort passed', async () => {
- const mockEsClient = jest.fn();
- mockEsClient.mockReturnValue(mockEsSearchResult);
+ const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
+ mockEsClient.search.mockResolvedValueOnce(mockEsSearchResult);
await getPings({
callES: mockEsClient,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
@@ -195,10 +198,9 @@ describe('getAll', () => {
size: 12,
});
- expect(mockEsClient).toHaveBeenCalledTimes(1);
- expect(mockEsClient.mock.calls[0]).toMatchInlineSnapshot(`
+ expect(mockEsClient.search).toHaveBeenCalledTimes(1);
+ expect(mockEsClient.search.mock.calls[0]).toMatchInlineSnapshot(`
Array [
- "search",
Object {
"body": Object {
"aggregations": Object {
@@ -264,8 +266,8 @@ describe('getAll', () => {
});
it('omits the size param when no size passed', async () => {
- const mockEsClient = jest.fn();
- mockEsClient.mockReturnValue(mockEsSearchResult);
+ const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
+ mockEsClient.search.mockResolvedValueOnce(mockEsSearchResult);
await getPings({
callES: mockEsClient,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
@@ -273,10 +275,9 @@ describe('getAll', () => {
sort: 'desc',
});
- expect(mockEsClient).toHaveBeenCalledTimes(1);
- expect(mockEsClient.mock.calls[0]).toMatchInlineSnapshot(`
+ expect(mockEsClient.search).toHaveBeenCalledTimes(1);
+ expect(mockEsClient.search.mock.calls[0]).toMatchInlineSnapshot(`
Array [
- "search",
Object {
"body": Object {
"aggregations": Object {
@@ -342,8 +343,8 @@ describe('getAll', () => {
});
it('adds a filter for monitor ID', async () => {
- const mockEsClient = jest.fn();
- mockEsClient.mockReturnValue(mockEsSearchResult);
+ const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
+ mockEsClient.search.mockResolvedValueOnce(mockEsSearchResult);
await getPings({
callES: mockEsClient,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
@@ -351,10 +352,9 @@ describe('getAll', () => {
monitorId: 'testmonitorid',
});
- expect(mockEsClient).toHaveBeenCalledTimes(1);
- expect(mockEsClient.mock.calls[0]).toMatchInlineSnapshot(`
+ expect(mockEsClient.search).toHaveBeenCalledTimes(1);
+ expect(mockEsClient.search.mock.calls[0]).toMatchInlineSnapshot(`
Array [
- "search",
Object {
"body": Object {
"aggregations": Object {
@@ -425,8 +425,8 @@ describe('getAll', () => {
});
it('adds a filter for monitor status', async () => {
- const mockEsClient = jest.fn();
- mockEsClient.mockReturnValue(mockEsSearchResult);
+ const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
+ mockEsClient.search.mockResolvedValueOnce(mockEsSearchResult);
await getPings({
callES: mockEsClient,
dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS,
@@ -434,10 +434,9 @@ describe('getAll', () => {
status: 'down',
});
- expect(mockEsClient).toHaveBeenCalledTimes(1);
- expect(mockEsClient.mock.calls[0]).toMatchInlineSnapshot(`
+ expect(mockEsClient.search).toHaveBeenCalledTimes(1);
+ expect(mockEsClient.search.mock.calls[0]).toMatchInlineSnapshot(`
Array [
- "search",
Object {
"body": Object {
"aggregations": Object {
diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/helper.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/helper.ts
index 878569b5d390f..4ebc9b2da7855 100644
--- a/x-pack/plugins/uptime/server/lib/requests/__tests__/helper.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/helper.ts
@@ -4,16 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { LegacyScopedClusterClient } from 'src/core/server';
import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { ElasticsearchClientMock } from '../../../../../../../src/core/server/elasticsearch/client/mocks';
+
export interface MultiPageCriteria {
after_key?: K;
bucketCriteria: T[];
}
-export type MockCallES = (method: any, params: any) => Promise;
-
/**
* This utility function will set up a mock ES client, and store subsequent calls. It is designed
* to let callers easily simulate an arbitrary series of chained composite aggregation calls by supplying
@@ -30,8 +30,8 @@ export type MockCallES = (method: any, params: any) => Promise;
export const setupMockEsCompositeQuery = (
criteria: Array>,
genBucketItem: (criteria: C) => I
-): [MockCallES, jest.Mocked>] => {
- const esMock = elasticsearchServiceMock.createLegacyScopedClusterClient();
+): ElasticsearchClientMock => {
+ const esMock = elasticsearchServiceMock.createElasticsearchClient();
// eslint-disable-next-line @typescript-eslint/naming-convention
criteria.forEach(({ after_key, bucketCriteria }) => {
@@ -43,8 +43,14 @@ export const setupMockEsCompositeQuery = (
},
},
};
- esMock.callAsCurrentUser.mockResolvedValueOnce(mockResponse);
+ esMock.search.mockResolvedValueOnce({
+ body: mockResponse,
+ statusCode: 200,
+ headers: {},
+ warnings: [],
+ meta: {} as any,
+ });
});
- return [(method: any, params: any) => esMock.callAsCurrentUser(method, params), esMock];
+ return esMock;
};
diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/monitor_charts_mock.json b/x-pack/plugins/uptime/server/lib/requests/__tests__/monitor_charts_mock.json
index c62e862a9af89..9fbfdb98d7fa4 100644
--- a/x-pack/plugins/uptime/server/lib/requests/__tests__/monitor_charts_mock.json
+++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/monitor_charts_mock.json
@@ -1,146 +1,318 @@
{
- "took": 40,
- "timed_out": false,
- "_shards": {
- "total": 1,
- "successful": 1,
- "skipped": 0,
- "failed": 0
- },
- "aggregations": {
- "timeseries": {
- "buckets": [
- {
- "key": 1568411568000,
- "doc_count": 4,
- "location": {
- "buckets": [
- { "key": "us-east-2", "duration": { "avg": 4658759 } },
- { "key": "us-west-4", "duration": { "avg": 8678399.5 } }
- ]
+ "body": {
+ "took": 40,
+ "timed_out": false,
+ "_shards": {
+ "total": 1,
+ "successful": 1,
+ "skipped": 0,
+ "failed": 0
+ },
+ "aggregations": {
+ "timeseries": {
+ "buckets": [
+ {
+ "key": 1568411568000,
+ "doc_count": 4,
+ "location": {
+ "buckets": [
+ {
+ "key": "us-east-2",
+ "duration": {
+ "avg": 4658759
+ }
+ },
+ {
+ "key": "us-west-4",
+ "duration": {
+ "avg": 8678399.5
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": 1568411604000,
+ "doc_count": 0,
+ "location": {
+ "buckets": []
+ }
+ },
+ {
+ "key": 1568411640000,
+ "doc_count": 8,
+ "location": {
+ "buckets": [
+ {
+ "key": "us-east-2",
+ "duration": {
+ "avg": 481780
+ }
+ },
+ {
+ "key": "us-west-4",
+ "duration": {
+ "avg": 685056.5
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": 1568411784000,
+ "doc_count": 8,
+ "location": {
+ "buckets": [
+ {
+ "key": "us-east-2",
+ "duration": {
+ "avg": 469206.5
+ }
+ },
+ {
+ "key": "us-west-4",
+ "duration": {
+ "avg": 261406.5
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": 1568411820000,
+ "doc_count": 0,
+ "location": {
+ "buckets": []
+ }
+ },
+ {
+ "key": 1568411856000,
+ "doc_count": 0,
+ "location": {
+ "buckets": []
+ }
+ },
+ {
+ "key": 1568411892000,
+ "doc_count": 0,
+ "location": {
+ "buckets": []
+ }
+ },
+ {
+ "key": 1568411928000,
+ "doc_count": 4,
+ "location": {
+ "buckets": [
+ {
+ "key": "us-west-4",
+ "duration": {
+ "avg": 1999309.6666667
+ }
+ },
+ {
+ "key": "us-east-2",
+ "duration": {
+ "avg": 645563
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": 1568411964000,
+ "doc_count": 7,
+ "location": {
+ "buckets": [
+ {
+ "key": "us-west-4",
+ "duration": {
+ "avg": 2499799.25
+ }
+ },
+ {
+ "key": "us-east-2",
+ "duration": {
+ "avg": 1513896.6666667
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": 1568412036000,
+ "doc_count": 5,
+ "location": {
+ "buckets": [
+ {
+ "key": "us-west-4",
+ "duration": {
+ "avg": 1876155.3333333
+ }
+ },
+ {
+ "key": "us-east-2",
+ "duration": {
+ "avg": 1511409
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": 1568412072000,
+ "doc_count": 4,
+ "location": {
+ "buckets": [
+ {
+ "key": "us-west-4",
+ "duration": {
+ "avg": 1490845.75
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": 1568412108000,
+ "doc_count": 3,
+ "location": {
+ "buckets": [
+ {
+ "key": "us-west-4",
+ "duration": {
+ "avg": 2365962.6666667
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": 1568412144000,
+ "doc_count": 4,
+ "location": {
+ "buckets": [
+ {
+ "key": "us-west-4",
+ "duration": {
+ "avg": 1788901.25
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": 1568412180000,
+ "doc_count": 4,
+ "location": {
+ "buckets": [
+ {
+ "key": "us-west-4",
+ "duration": {
+ "avg": 1773177.5
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": 1568412216000,
+ "doc_count": 3,
+ "location": {
+ "buckets": [
+ {
+ "key": "us-west-4",
+ "duration": {
+ "avg": 3086220.3333333
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": 1568412252000,
+ "doc_count": 1,
+ "location": {
+ "buckets": [
+ {
+ "key": "us-west-4",
+ "duration": {
+ "avg": 1020528
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": 1568412288000,
+ "doc_count": 3,
+ "location": {
+ "buckets": [
+ {
+ "key": "us-west-4",
+ "duration": {
+ "avg": 1643963.3333333
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": 1568412324000,
+ "doc_count": 8,
+ "location": {
+ "buckets": [
+ {
+ "key": "us-east-2",
+ "duration": {
+ "avg": 1804116
+ }
+ },
+ {
+ "key": "us-west-4",
+ "duration": {
+ "avg": 1799630
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": 1568412432000,
+ "doc_count": 8,
+ "location": {
+ "buckets": [
+ {
+ "key": "us-east-2",
+ "duration": {
+ "avg": 1972483.25
+ }
+ },
+ {
+ "key": "us-west-4",
+ "duration": {
+ "avg": 1543307.5
+ }
+ }
+ ]
+ }
+ },
+ {
+ "key": 1568412468000,
+ "doc_count": 1,
+ "location": {
+ "buckets": [
+ {
+ "key": "us-east-2",
+ "duration": {
+ "avg": 1020490
+ }
+ }
+ ]
+ }
}
- },
- { "key": 1568411604000, "doc_count": 0, "location": { "buckets": [] } },
- {
- "key": 1568411640000,
- "doc_count": 8,
- "location": {
- "buckets": [
- { "key": "us-east-2", "duration": { "avg": 481780 } },
- { "key": "us-west-4", "duration": { "avg": 685056.5 } }
- ]
- }
- },
- {
- "key": 1568411784000,
- "doc_count": 8,
- "location": {
- "buckets": [
- { "key": "us-east-2", "duration": { "avg": 469206.5 } },
- { "key": "us-west-4", "duration": { "avg": 261406.5 } }
- ]
- }
- },
- { "key": 1568411820000, "doc_count": 0, "location": { "buckets": [] } },
- { "key": 1568411856000, "doc_count": 0, "location": { "buckets": [] } },
- { "key": 1568411892000, "doc_count": 0, "location": { "buckets": [] } },
- {
- "key": 1568411928000,
- "doc_count": 4,
- "location": {
- "buckets": [
- { "key": "us-west-4", "duration": { "avg": 1999309.6666667 } },
- { "key": "us-east-2", "duration": { "avg": 645563 } }
- ]
- }
- },
- {
- "key": 1568411964000,
- "doc_count": 7,
- "location": {
- "buckets": [
- { "key": "us-west-4", "duration": { "avg": 2499799.25 } },
- { "key": "us-east-2", "duration": { "avg": 1513896.6666667 } }
- ]
- }
- },
- {
- "key": 1568412036000,
- "doc_count": 5,
- "location": {
- "buckets": [
- { "key": "us-west-4", "duration": { "avg": 1876155.3333333 } },
- { "key": "us-east-2", "duration": { "avg": 1511409 } }
- ]
- }
- },
- {
- "key": 1568412072000,
- "doc_count": 4,
- "location": { "buckets": [{ "key": "us-west-4", "duration": { "avg": 1490845.75 } }] }
- },
- {
- "key": 1568412108000,
- "doc_count": 3,
- "location": {
- "buckets": [{ "key": "us-west-4", "duration": { "avg": 2365962.6666667 } }]
- }
- },
- {
- "key": 1568412144000,
- "doc_count": 4,
- "location": { "buckets": [{ "key": "us-west-4", "duration": { "avg": 1788901.25 } }] }
- },
- {
- "key": 1568412180000,
- "doc_count": 4,
- "location": { "buckets": [{ "key": "us-west-4", "duration": { "avg": 1773177.5 } }] }
- },
- {
- "key": 1568412216000,
- "doc_count": 3,
- "location": {
- "buckets": [{ "key": "us-west-4", "duration": { "avg": 3086220.3333333 } }]
- }
- },
- {
- "key": 1568412252000,
- "doc_count": 1,
- "location": { "buckets": [{ "key": "us-west-4", "duration": { "avg": 1020528 } }] }
- },
- {
- "key": 1568412288000,
- "doc_count": 3,
- "location": {
- "buckets": [{ "key": "us-west-4", "duration": { "avg": 1643963.3333333 } }]
- }
- },
- {
- "key": 1568412324000,
- "doc_count": 8,
- "location": {
- "buckets": [
- { "key": "us-east-2", "duration": { "avg": 1804116 } },
- { "key": "us-west-4", "duration": { "avg": 1799630 } }
- ]
- }
- },
- {
- "key": 1568412432000,
- "doc_count": 8,
- "location": {
- "buckets": [
- { "key": "us-east-2", "duration": { "avg": 1972483.25 } },
- { "key": "us-west-4", "duration": { "avg": 1543307.5 } }
- ]
- }
- },
- {
- "key": 1568412468000,
- "doc_count": 1,
- "location": { "buckets": [{ "key": "us-east-2", "duration": { "avg": 1020490 } }] }
- }
- ]
+ ]
+ }
}
}
}
diff --git a/x-pack/plugins/uptime/server/lib/requests/get_certs.ts b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts
index 4793d420cbfd8..0836cb039b215 100644
--- a/x-pack/plugins/uptime/server/lib/requests/get_certs.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts
@@ -145,7 +145,7 @@ export const getCerts: UMElasticsearchQueryFn = asyn
}
// console.log(JSON.stringify(params, null, 2));
- const result = await callES('search', params);
+ const { body: result } = await callES.search(params);
const certs = (result?.hits?.hits ?? []).map((hit: any) => {
const {
diff --git a/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts b/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts
index e89b457eccf32..c3295d6dd9c8f 100644
--- a/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts
@@ -93,6 +93,8 @@ export const getFilterBar: UMElasticsearchQueryFn = async ({ esClient, dynamicSettings }) => {
+export const getUptimeIndexPattern = async ({
+ esClient,
+ dynamicSettings,
+}: {
+ esClient: ElasticsearchClient;
+ dynamicSettings: DynamicSettings;
+}): Promise => {
const indexPatternsFetcher = new IndexPatternsFetcher(esClient);
// Since `getDynamicIndexPattern` is called in setup_request (and thus by every endpoint)
@@ -28,12 +31,10 @@ export const getUptimeIndexPattern: UMElasticsearchQueryFn<
pattern: dynamicSettings.heartbeatIndices,
});
- const indexPattern: IndexPatternTitleAndFields = {
+ return {
fields,
title: dynamicSettings.heartbeatIndices,
};
-
- return indexPattern;
} catch (e) {
const notExists = e.output?.statusCode === 404;
if (notExists) {
diff --git a/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts
index 7688f04f1acd9..061d002b010de 100644
--- a/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts
@@ -12,9 +12,11 @@ export const getIndexStatus: UMElasticsearchQueryFn<{}, StatesIndexStatus> = asy
dynamicSettings,
}) => {
const {
- _shards: { total },
- count,
- } = await callES('count', { index: dynamicSettings.heartbeatIndices });
+ body: {
+ _shards: { total },
+ count,
+ },
+ } = await callES.count({ index: dynamicSettings.heartbeatIndices });
return {
indexExists: total > 0,
docCount: count,
diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts
index f726ef47915b8..bff3aaf1176df 100644
--- a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts
@@ -42,7 +42,7 @@ export const getJourneyScreenshot: UMElasticsearchQueryFn<
_source: ['synthetics.blob'],
},
};
- const result = await callES('search', params);
+ const { body: result } = await callES.search(params);
if (!Array.isArray(result?.hits?.hits) || result.hits.hits.length < 1) {
return null;
}
diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts
index 9c139b2ce8588..f36815a747db3 100644
--- a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts
@@ -42,7 +42,7 @@ export const getJourneySteps: UMElasticsearchQueryFn h?._source?.synthetics?.type === 'step/screenshot')
.map((h: any) => h?._source?.synthetics?.step?.index);
diff --git a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts
index d32b78bdc7139..f6562eaa42e90 100644
--- a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts
@@ -57,7 +57,7 @@ export const getLatestMonitor: UMElasticsearchQueryFn {
+const getMonitorAlerts = async ({
+ callES,
+ dynamicSettings,
+ alertsClient,
+ monitorId,
+}: {
+ callES: ElasticsearchClient;
+ dynamicSettings: any;
+ alertsClient: any;
+ monitorId: string;
+}) => {
const options: any = {
page: 1,
perPage: 500,
@@ -70,13 +73,12 @@ const getMonitorAlerts = async (
const parsedFilters = await formatFilterString(
dynamicSettings,
callES,
- esClient,
currAlert.params.filters,
currAlert.params.search
);
esParams.body.query.bool = Object.assign({}, esParams.body.query.bool, parsedFilters?.bool);
- const result = await callES('search', esParams);
+ const { body: result } = await callES.search(esParams);
if (result.hits.total.value > 0) {
monitorAlerts.push(currAlert);
@@ -88,7 +90,7 @@ const getMonitorAlerts = async (
export const getMonitorDetails: UMElasticsearchQueryFn<
GetMonitorDetailsParams,
MonitorDetails
-> = async ({ callES, esClient, dynamicSettings, monitorId, dateStart, dateEnd, alertsClient }) => {
+> = async ({ callES, dynamicSettings, monitorId, dateStart, dateEnd, alertsClient }) => {
const queryFilters: any = [
{
range: {
@@ -132,19 +134,19 @@ export const getMonitorDetails: UMElasticsearchQueryFn<
},
};
- const result = await callES('search', params);
+ const { body: result } = await callES.search(params);
const data = result.hits.hits[0]?._source;
const monitorError: MonitorError | undefined = data?.error;
const errorTimestamp: string | undefined = data?.['@timestamp'];
- const monAlerts = await getMonitorAlerts(
+ const monAlerts = await getMonitorAlerts({
callES,
- esClient,
dynamicSettings,
alertsClient,
- monitorId
- );
+ monitorId,
+ });
+
return {
monitorId,
error: monitorError,
diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts
index 00ca1b5878329..77ae7570a96a8 100644
--- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts
@@ -59,7 +59,7 @@ export const getMonitorDurationChart: UMElasticsearchQueryFn<
},
};
- const result = await callES('search', params);
+ const { body: result } = await callES.search(params);
const dateHistogramBuckets: any[] = result?.aggregations?.timeseries?.buckets ?? [];
diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts
index f52e965d488ea..b5183ca9ffb9f 100644
--- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts
@@ -88,7 +88,7 @@ export const getMonitorLocations: UMElasticsearchQueryFn<
},
};
- const result = await callES('search', params);
+ const { body: result } = await callES.search(params);
const locations = result?.aggregations?.location?.buckets ?? [];
const getGeo = (locGeo: { name: string; location?: string }) => {
diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts
index 3e49a32881f54..020fcf5331188 100644
--- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts
@@ -153,7 +153,7 @@ export const getHistogramForMonitors = async (
};
const result = await queryContext.search(params);
- const histoBuckets: any[] = result.aggregations.histogram.buckets;
+ const histoBuckets: any[] = result.aggregations?.histogram.buckets ?? [];
const simplified = histoBuckets.map((histoBucket: any): { timestamp: number; byId: any } => {
const byId: { [key: string]: number } = {};
histoBucket.by_id.buckets.forEach((idBucket: any) => {
diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts
index caf505610e991..06648d68969c1 100644
--- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts
@@ -133,7 +133,7 @@ export const getMonitorStatus: UMElasticsearchQueryFn<
esParams.body.aggs.monitors.composite.after = afterKey;
}
- const result = await callES('search', esParams);
+ const { body: result } = await callES.search(esParams);
afterKey = result?.aggregations?.monitors?.after_key;
monitors = monitors.concat(result?.aggregations?.monitors?.buckets || []);
diff --git a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts
index 5d8706e2fc5f1..4eb2d862cb702 100644
--- a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts
@@ -76,7 +76,7 @@ export const getPingHistogram: UMElasticsearchQueryFn<
},
};
- const result = await callES('search', params);
+ const { body: result } = await callES.search(params);
const buckets: HistogramQueryResult[] = result?.aggregations?.timeseries?.buckets ?? [];
const histogram = buckets.map((bucket) => {
const x: number = bucket.key;
diff --git a/x-pack/plugins/uptime/server/lib/requests/get_pings.ts b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts
index 03ec2d7343c9a..e72b16de3d66f 100644
--- a/x-pack/plugins/uptime/server/lib/requests/get_pings.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts
@@ -108,9 +108,11 @@ export const getPings: UMElasticsearchQueryFn = a
}
const {
- hits: { hits, total },
- aggregations: aggs,
- } = await callES('search', params);
+ body: {
+ hits: { hits, total },
+ aggregations: aggs,
+ },
+ } = await callES.search(params);
const locations = aggs?.locations ?? { buckets: [{ key: 'N/A', doc_count: 0 }] };
diff --git a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts
index 92295a38cffb4..ac36585ff0939 100644
--- a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts
@@ -39,7 +39,7 @@ export const getSnapshotCount: UMElasticsearchQueryFn => {
- const res = await context.search({
+ const { body: res } = await context.search({
index: context.heartbeatIndices,
body: statusCountBody(await context.dateAndCustomFilters()),
});
diff --git a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts
index 6c229cf30e165..38e7dabb19941 100644
--- a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts
@@ -19,7 +19,7 @@ export const findPotentialMatches = async (
searchAfter: any,
size: number
) => {
- const queryResult = await query(queryContext, searchAfter, size);
+ const { body: queryResult } = await query(queryContext, searchAfter, size);
const monitorIds: string[] = [];
get(queryResult, 'aggregations.monitors.buckets', []).forEach((b: any) => {
const monitorId = b.key.monitor_id;
diff --git a/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts
index 5d97e635f3e7d..96df8ea651c44 100644
--- a/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts
@@ -5,13 +5,13 @@
*/
import moment from 'moment';
-import { LegacyAPICaller } from 'src/core/server';
+import { ElasticsearchClient } from 'kibana/server';
import { CursorPagination } from './types';
import { parseRelativeDate } from '../../helper';
import { CursorDirection, SortOrder } from '../../../../common/runtime_types';
export class QueryContext {
- callES: LegacyAPICaller;
+ callES: ElasticsearchClient;
heartbeatIndices: string;
dateRangeStart: string;
dateRangeEnd: string;
@@ -43,12 +43,12 @@ export class QueryContext {
async search(params: any): Promise {
params.index = this.heartbeatIndices;
- return this.callES('search', params);
+ return this.callES.search(params);
}
async count(params: any): Promise {
params.index = this.heartbeatIndices;
- return this.callES('count', params);
+ return this.callES.count(params);
}
async dateAndCustomFilters(): Promise {
diff --git a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts
index a864bfa591424..6be9f813016f8 100644
--- a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts
@@ -23,7 +23,7 @@ export const refinePotentialMatches = async (
return [];
}
- const queryResult = await query(queryContext, potentialMatchMonitorIDs);
+ const { body: queryResult } = await query(queryContext, potentialMatchMonitorIDs);
return await fullyMatchingIds(queryResult, queryContext.statusFilter);
};
diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts
index baf999158a29e..418cde9e701d5 100644
--- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts
+++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts
@@ -17,8 +17,7 @@ export const createGetIndexPatternRoute: UMRestApiRouteFactory = (libs: UMServer
return response.ok({
body: {
...(await libs.requests.getIndexPattern({
- callES,
- esClient: _context.core.elasticsearch.client.asCurrentUser,
+ esClient: callES,
dynamicSettings,
})),
},
diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts
index 0e2c8c180e0e0..7b461060bf4bc 100644
--- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts
+++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts
@@ -25,44 +25,51 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({
tags: ['access:uptime-read'],
},
handler: async ({ callES, dynamicSettings }, _context, request, response): Promise => {
- const {
- dateRangeStart,
- dateRangeEnd,
- filters,
- pagination,
- statusFilter,
- pageSize,
- } = request.query;
-
- const decodedPagination = pagination
- ? JSON.parse(decodeURIComponent(pagination))
- : CONTEXT_DEFAULTS.CURSOR_PAGINATION;
- const [indexStatus, { summaries, nextPagePagination, prevPagePagination }] = await Promise.all([
- libs.requests.getIndexStatus({ callES, dynamicSettings }),
- libs.requests.getMonitorStates({
- callES,
- dynamicSettings,
+ try {
+ const {
dateRangeStart,
dateRangeEnd,
- pagination: decodedPagination,
- pageSize,
filters,
- // this is added to make typescript happy,
- // this sort of reassignment used to be further downstream but I've moved it here
- // because this code is going to be decomissioned soon
- statusFilter: statusFilter || undefined,
- }),
- ]);
+ pagination,
+ statusFilter,
+ pageSize,
+ } = request.query;
+
+ const decodedPagination = pagination
+ ? JSON.parse(decodeURIComponent(pagination))
+ : CONTEXT_DEFAULTS.CURSOR_PAGINATION;
+ const [
+ indexStatus,
+ { summaries, nextPagePagination, prevPagePagination },
+ ] = await Promise.all([
+ libs.requests.getIndexStatus({ callES, dynamicSettings }),
+ libs.requests.getMonitorStates({
+ callES,
+ dynamicSettings,
+ dateRangeStart,
+ dateRangeEnd,
+ pagination: decodedPagination,
+ pageSize,
+ filters,
+ // this is added to make typescript happy,
+ // this sort of reassignment used to be further downstream but I've moved it here
+ // because this code is going to be decomissioned soon
+ statusFilter: statusFilter || undefined,
+ }),
+ ]);
- const totalSummaryCount = indexStatus?.docCount ?? 0;
+ const totalSummaryCount = indexStatus?.docCount ?? 0;
- return response.ok({
- body: {
- summaries,
- nextPagePagination,
- prevPagePagination,
- totalSummaryCount,
- },
- });
+ return response.ok({
+ body: {
+ summaries,
+ nextPagePagination,
+ prevPagePagination,
+ totalSummaryCount,
+ },
+ });
+ } catch (e) {
+ return response.internalError({ body: { message: e.message } });
+ }
},
});
diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts
index 8bbb4fcb5575c..bb54effc0d57e 100644
--- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts
+++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts
@@ -28,7 +28,6 @@ export const createGetMonitorDetailsRoute: UMRestApiRouteFactory = (libs: UMServ
body: {
...(await libs.requests.getMonitorDetails({
callES,
- esClient: context.core.elasticsearch.client.asCurrentUser,
dynamicSettings,
monitorId,
dateStart,
diff --git a/x-pack/plugins/uptime/server/rest_api/types.ts b/x-pack/plugins/uptime/server/rest_api/types.ts
index 589cb82d550f6..5e5f4a2a991cf 100644
--- a/x-pack/plugins/uptime/server/rest_api/types.ts
+++ b/x-pack/plugins/uptime/server/rest_api/types.ts
@@ -9,12 +9,13 @@ import {
RequestHandler,
RouteConfig,
RouteMethod,
- LegacyCallAPIOptions,
SavedObjectsClientContract,
RequestHandlerContext,
KibanaRequest,
KibanaResponseFactory,
IKibanaResponse,
+ IScopedClusterClient,
+ ElasticsearchClient,
} from 'kibana/server';
import { DynamicSettings } from '../../common/runtime_types';
import { UMServerLibs } from '../lib/lib';
@@ -63,11 +64,8 @@ export type UMKibanaRouteWrapper = (uptimeRoute: UptimeRoute) => UMKibanaRoute;
* This type can store custom parameters used by the internal Uptime route handlers.
*/
export interface UMRouteParams {
- callES: (
- endpoint: string,
- clientParams?: Record,
- options?: LegacyCallAPIOptions | undefined
- ) => Promise;
+ callES: ElasticsearchClient;
+ esClient: IScopedClusterClient;
dynamicSettings: DynamicSettings;
savedObjectsClient: SavedObjectsClientContract;
}
diff --git a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts
index 84a85a54afe13..b2f1c7d6424e6 100644
--- a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts
+++ b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts
@@ -13,11 +13,11 @@ export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute) => ({
tags: ['access:uptime-read', ...(uptimeRoute?.writeAccess ? ['access:uptime-write'] : [])],
},
handler: async (context, request, response) => {
- const { callAsCurrentUser: callES } = context.core.elasticsearch.legacy.client;
+ const { client: esClient } = context.core.elasticsearch;
const { client: savedObjectsClient } = context.core.savedObjects;
const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient);
return uptimeRoute.handler(
- { callES, savedObjectsClient, dynamicSettings },
+ { callES: esClient.asCurrentUser, esClient, savedObjectsClient, dynamicSettings },
context,
request,
response
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/execution_status.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/execution_status.ts
index 8fb89042e4a90..4058b71356280 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/execution_status.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/execution_status.ts
@@ -5,6 +5,7 @@
*/
import expect from '@kbn/expect';
+import { AlertExecutionStatusErrorReasons } from '../../../../../plugins/alerts/common';
import { Spaces } from '../../scenarios';
import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
@@ -49,7 +50,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
executionStatus = await waitForStatus(alertId, new Set(['error']));
expect(executionStatus.error).to.be.ok();
- expect(executionStatus.error.reason).to.be('decrypt');
+ expect(executionStatus.error.reason).to.be(AlertExecutionStatusErrorReasons.Decrypt);
expect(executionStatus.error.message).to.be('Unable to decrypt attribute "apiKey"');
});
});
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts
index 92db0458c0639..c05fa6cf051ff 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts
@@ -15,6 +15,7 @@ import {
ObjectRemover,
} from '../../../../../common/lib';
import { createEsDocuments } from './create_test_data';
+import { getAlertType } from '../../../../../../../plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/';
const ALERT_TYPE_ID = '.index-threshold';
const ACTION_TYPE_ID = '.index';
@@ -26,6 +27,8 @@ const ALERT_INTERVALS_TO_WRITE = 5;
const ALERT_INTERVAL_SECONDS = 3;
const ALERT_INTERVAL_MILLIS = ALERT_INTERVAL_SECONDS * 1000;
+const DefaultActionMessage = getAlertType().defaultActionMessage;
+
// eslint-disable-next-line import/no-default-export
export default function alertTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
@@ -62,6 +65,10 @@ export default function alertTests({ getService }: FtrProviderContext) {
await esTestIndexToolOutput.destroy();
});
+ it('has a default action message', () => {
+ expect(DefaultActionMessage).to.be.ok();
+ });
+
// The tests below create two alerts, one that will fire, one that will
// never fire; the tests ensure the ones that should fire, do fire, and
// those that shouldn't fire, do not fire.
@@ -85,7 +92,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
const docs = await waitForDocs(2);
for (const doc of docs) {
const { group } = doc._source;
- const { name, value, title, message } = doc._source.params;
+ const { name, title, message } = doc._source.params;
expect(name).to.be('always fire');
expect(group).to.be('all documents');
@@ -93,9 +100,8 @@ export default function alertTests({ getService }: FtrProviderContext) {
// we'll check title and message in this test, but not subsequent ones
expect(title).to.be('alert always fire group all documents exceeded threshold');
- const expectedPrefix = `alert always fire group all documents value ${value} exceeded threshold count > -1 over`;
- const messagePrefix = message.substr(0, expectedPrefix.length);
- expect(messagePrefix).to.be(expectedPrefix);
+ const messagePattern = /alert always fire group all documents value \d+ exceeded threshold count > -1 over 15s on \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
}
});
@@ -128,10 +134,13 @@ export default function alertTests({ getService }: FtrProviderContext) {
for (const doc of docs) {
const { group } = doc._source;
- const { name } = doc._source.params;
+ const { name, message } = doc._source.params;
expect(name).to.be('always fire');
if (group === 'group-0') inGroup0++;
+
+ const messagePattern = /alert always fire group group-\d value \d+ exceeded threshold count .+ over 15s on \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
}
// there should be 2 docs in group-0, rando split between others
@@ -163,9 +172,12 @@ export default function alertTests({ getService }: FtrProviderContext) {
const docs = await waitForDocs(2);
for (const doc of docs) {
- const { name } = doc._source.params;
+ const { name, message } = doc._source.params;
expect(name).to.be('always fire');
+
+ const messagePattern = /alert always fire group all documents value \d+ exceeded threshold sum\(testedValue\) between 0,1000000 over 15s on \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
}
});
@@ -195,9 +207,12 @@ export default function alertTests({ getService }: FtrProviderContext) {
const docs = await waitForDocs(4);
for (const doc of docs) {
- const { name } = doc._source.params;
+ const { name, message } = doc._source.params;
expect(name).to.be('always fire');
+
+ const messagePattern = /alert always fire group all documents value .+ exceeded threshold avg\(testedValue\) .+ 0 over 15s on \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
}
});
@@ -232,10 +247,13 @@ export default function alertTests({ getService }: FtrProviderContext) {
for (const doc of docs) {
const { group } = doc._source;
- const { name } = doc._source.params;
+ const { name, message } = doc._source.params;
expect(name).to.be('always fire');
if (group === 'group-2') inGroup2++;
+
+ const messagePattern = /alert always fire group group-. value \d+ exceeded threshold max\(testedValue\) .* 0 over 15s on \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
}
// there should be 2 docs in group-2, rando split between others
@@ -274,10 +292,13 @@ export default function alertTests({ getService }: FtrProviderContext) {
for (const doc of docs) {
const { group } = doc._source;
- const { name } = doc._source.params;
+ const { name, message } = doc._source.params;
expect(name).to.be('always fire');
if (group === 'group-0') inGroup0++;
+
+ const messagePattern = /alert always fire group group-. value \d+ exceeded threshold min\(testedValue\) .* 0 over 15s on \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
}
// there should be 2 docs in group-0, rando split between others
@@ -329,7 +350,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
name: '{{{alertName}}}',
value: '{{{context.value}}}',
title: '{{{context.title}}}',
- message: '{{{context.message}}}',
+ message: DefaultActionMessage,
},
date: '{{{context.date}}}',
// TODO: I wanted to write the alert value here, but how?
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts
index 7d99d3635106d..0f6da936f8644 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts
@@ -55,6 +55,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await nameInput.click();
}
+ async function defineAlwaysFiringAlert(alertName: string) {
+ await pageObjects.triggersActionsUI.clickCreateAlertButton();
+ await testSubjects.setValue('alertNameInput', alertName);
+ await testSubjects.click('test.always-firing-SelectOption');
+ }
+
describe('create alert', function () {
before(async () => {
await pageObjects.common.navigateToApp('triggersActions');
@@ -73,10 +79,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)');
const createdConnectorToastTitle = await pageObjects.common.closeToast();
expect(createdConnectorToastTitle).to.eql(`Created '${slackConnectorName}'`);
+ const messageTextArea = await find.byCssSelector('[data-test-subj="messageTextArea"]');
+ expect(await messageTextArea.getAttribute('value')).to.eql(
+ 'alert {{alertName}} group {{context.group}} value {{context.value}} exceeded threshold {{context.function}} over {{params.timeWindowSize}}{{params.timeWindowUnit}} on {{context.date}}'
+ );
await testSubjects.setValue('messageTextArea', 'test message ');
await testSubjects.click('messageAddVariableButton');
await testSubjects.click('variableMenuButton-0');
- const messageTextArea = await find.byCssSelector('[data-test-subj="messageTextArea"]');
expect(await messageTextArea.getAttribute('value')).to.eql('test message {{alertId}}');
await messageTextArea.type(' some additional text ');
@@ -106,6 +115,57 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id));
});
+ it('should create an alert with actions in multiple groups', async () => {
+ const alertName = generateUniqueKey();
+ await defineAlwaysFiringAlert(alertName);
+
+ // create Slack connector and attach an action using it
+ await testSubjects.click('.slack-ActionTypeSelectOption');
+ await testSubjects.click('addNewActionConnectorButton-.slack');
+ const slackConnectorName = generateUniqueKey();
+ await testSubjects.setValue('nameInput', slackConnectorName);
+ await testSubjects.setValue('slackWebhookUrlInput', 'https://test');
+ await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)');
+ const createdConnectorToastTitle = await pageObjects.common.closeToast();
+ expect(createdConnectorToastTitle).to.eql(`Created '${slackConnectorName}'`);
+ await testSubjects.setValue('messageTextArea', 'test message ');
+ await (
+ await find.byCssSelector(
+ '[data-test-subj="alertActionAccordion-0"] [data-test-subj="messageTextArea"]'
+ )
+ ).type('some text ');
+
+ await testSubjects.click('addAlertActionButton');
+ await testSubjects.click('.slack-ActionTypeSelectOption');
+ await testSubjects.setValue('messageTextArea', 'test message ');
+ await (
+ await find.byCssSelector(
+ '[data-test-subj="alertActionAccordion-1"] [data-test-subj="messageTextArea"]'
+ )
+ ).type('some text ');
+
+ await testSubjects.click('addNewActionConnectorActionGroup-1');
+ await testSubjects.click('addNewActionConnectorActionGroup-1-option-other');
+
+ await testSubjects.click('saveAlertButton');
+ const toastTitle = await pageObjects.common.closeToast();
+ expect(toastTitle).to.eql(`Created alert "${alertName}"`);
+ await pageObjects.triggersActionsUI.searchAlerts(alertName);
+ const searchResultsAfterSave = await pageObjects.triggersActionsUI.getAlertsList();
+ expect(searchResultsAfterSave).to.eql([
+ {
+ name: alertName,
+ tagsText: '',
+ alertType: 'Always Firing',
+ interval: '1m',
+ },
+ ]);
+
+ // clean up created alert
+ const alertsToDelete = await getAlertsByName(alertName);
+ await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id));
+ });
+
it('should show save confirmation before creating alert with no actions', async () => {
const alertName = generateUniqueKey();
await defineAlert(alertName);
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts
index 9e4006681dc8d..1d86d95b7a796 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts
@@ -306,7 +306,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
});
- describe('Alert Instances', function () {
+ // FLAKY: https://github.com/elastic/kibana/issues/57426
+ describe.skip('Alert Instances', function () {
const testRunUuid = uuid.v4();
let alert: any;
diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts
index e3927f6bfffb9..6f9d010378624 100644
--- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts
+++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts
@@ -78,6 +78,7 @@ function createAlwaysFiringAlertType(alerts: AlertingSetup) {
{ id: 'default', name: 'Default' },
{ id: 'other', name: 'Other' },
],
+ defaultActionGroupId: 'default',
producer: 'alerts',
async executor(alertExecutorOptions: any) {
const { services, state, params } = alertExecutorOptions;
diff --git a/yarn.lock b/yarn.lock
index 833e8bffcfc80..0b429c96c1847 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10370,17 +10370,6 @@ cross-fetch@2.2.2:
node-fetch "2.1.2"
whatwg-fetch "2.0.4"
-cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5:
- version "6.0.5"
- resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
- integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
- dependencies:
- nice-try "^1.0.4"
- path-key "^2.0.1"
- semver "^5.5.0"
- shebang-command "^1.2.0"
- which "^1.2.9"
-
cross-spawn@7.0.1, cross-spawn@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14"
@@ -10398,6 +10387,17 @@ cross-spawn@^3.0.0:
lru-cache "^4.0.1"
which "^1.2.9"
+cross-spawn@^6.0.0, cross-spawn@^6.0.5:
+ version "6.0.5"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+ integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+ dependencies:
+ nice-try "^1.0.4"
+ path-key "^2.0.1"
+ semver "^5.5.0"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
crypt@~0.0.1:
version "0.0.2"
resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
@@ -12245,16 +12245,7 @@ endent@^2.0.1:
fast-json-parse "^1.0.3"
objectorarray "^1.0.4"
-enhanced-resolve@4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f"
- integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==
- dependencies:
- graceful-fs "^4.1.2"
- memory-fs "^0.4.0"
- tapable "^1.0.0"
-
-enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0, enhanced-resolve@^4.3.0:
+enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0, enhanced-resolve@^4.1.1, enhanced-resolve@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126"
integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==
@@ -13749,16 +13740,6 @@ find@^0.3.0:
dependencies:
traverse-chain "~0.1.0"
-findup-sync@3.0.0, findup-sync@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1"
- integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==
- dependencies:
- detect-file "^1.0.0"
- is-glob "^4.0.0"
- micromatch "^3.0.4"
- resolve-dir "^1.0.1"
-
findup-sync@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc"
@@ -13769,6 +13750,16 @@ findup-sync@^2.0.0:
micromatch "^3.0.4"
resolve-dir "^1.0.1"
+findup-sync@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1"
+ integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==
+ dependencies:
+ detect-file "^1.0.0"
+ is-glob "^4.0.0"
+ micromatch "^3.0.4"
+ resolve-dir "^1.0.1"
+
findup-sync@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.3.0.tgz#37930aa5d816b777c03445e1966cc6790a4c0b16"
@@ -14637,7 +14628,7 @@ global-dirs@^2.0.1:
dependencies:
ini "^1.3.5"
-global-modules@2.0.0:
+global-modules@2.0.0, global-modules@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780"
integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==
@@ -16195,7 +16186,7 @@ import-lazy@^2.1.0:
resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=
-import-local@2.0.0, import-local@^2.0.0:
+import-local@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==
@@ -16430,10 +16421,10 @@ internal-slot@^1.0.2:
has "^1.0.3"
side-channel "^1.0.2"
-interpret@1.2.0, interpret@^1.0.0, interpret@^1.1.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
- integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
+interpret@^1.0.0, interpret@^1.1.0, interpret@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
+ integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
interpret@^2.0.0:
version "2.2.0"
@@ -16481,11 +16472,6 @@ invert-kv@^1.0.0:
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY=
-invert-kv@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
- integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
-
io-ts@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.0.5.tgz#e6e3db9df8b047f9cbd6b69e7d2ad3e6437a0b13"
@@ -18624,13 +18610,6 @@ lcid@^1.0.0:
dependencies:
invert-kv "^1.0.0"
-lcid@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf"
- integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==
- dependencies:
- invert-kv "^2.0.0"
-
lead@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lead/-/lead-1.0.0.tgz#6f14f99a37be3a9dd784f5495690e5903466ee42"
@@ -18916,6 +18895,15 @@ loader-utils@2.0.0, loader-utils@^2.0.0:
emojis-list "^3.0.0"
json5 "^2.1.2"
+loader-utils@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
+ integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
+ dependencies:
+ big.js "^5.2.2"
+ emojis-list "^3.0.0"
+ json5 "^1.0.1"
+
locate-path@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
@@ -19449,13 +19437,6 @@ makeerror@1.0.x:
dependencies:
tmpl "1.0.x"
-map-age-cleaner@^0.1.1:
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.2.tgz#098fb15538fd3dbe461f12745b0ca8568d4e3f74"
- integrity sha512-UN1dNocxQq44IhJyMI4TU8phc2m9BddacHRPRjKGLYaF0jqd3xLz0jS0skpAU9WgYyoR4gHtUpzytNBS385FWQ==
- dependencies:
- p-defer "^1.0.0"
-
map-cache@^0.2.0, map-cache@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
@@ -19689,15 +19670,6 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
-mem@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/mem/-/mem-4.0.0.tgz#6437690d9471678f6cc83659c00cbafcd6b0cdaf"
- integrity sha512-WQxG/5xYc3tMbYLXoXPm81ET2WDULiU5FxbuIoNbJqLOOI8zehXFdZuiUEgfdrU2mVB1pxBZUGlYORSrpuJreA==
- dependencies:
- map-age-cleaner "^0.1.1"
- mimic-fn "^1.0.0"
- p-is-promise "^1.1.0"
-
"memoize-one@>=3.1.1 <6", memoize-one@^5.0.0, memoize-one@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
@@ -19729,7 +19701,7 @@ memory-fs@^0.2.0:
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290"
integrity sha1-8rslNovBIeORwlIN6Slpyu4KApA=
-memory-fs@^0.4.0, memory-fs@^0.4.1:
+memory-fs@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=
@@ -20620,11 +20592,6 @@ node-fetch@2.1.2, node-fetch@^1.0.1, node-fetch@^2.3.0, node-fetch@^2.6.0, node-
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
-node-forge@0.9.0:
- version "0.9.0"
- resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
- integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==
-
node-forge@^0.10.0, node-forge@^0.7.6:
version "0.10.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
@@ -21408,15 +21375,6 @@ os-locale@^1.4.0:
dependencies:
lcid "^1.0.0"
-os-locale@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
- integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==
- dependencies:
- execa "^1.0.0"
- lcid "^2.0.0"
- mem "^4.0.0"
-
os-name@^3.0.0, os-name@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801"
@@ -21477,11 +21435,6 @@ p-cancelable@^2.0.0:
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e"
integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==
-p-defer@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
- integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=
-
p-each-series@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48"
@@ -21506,11 +21459,6 @@ p-finally@^1.0.0:
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
-p-is-promise@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e"
- integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=
-
p-limit@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
@@ -25133,11 +25081,11 @@ selenium-webdriver@^4.0.0-alpha.7:
tmp "0.0.30"
selfsigned@^1.10.7:
- version "1.10.7"
- resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.7.tgz#da5819fd049d5574f28e88a9bcc6dbc6e6f3906b"
- integrity sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==
+ version "1.10.8"
+ resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30"
+ integrity sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==
dependencies:
- node-forge "0.9.0"
+ node-forge "^0.10.0"
semver-diff@^2.0.0:
version "2.1.0"
@@ -26572,13 +26520,6 @@ supports-color@6.0.0:
dependencies:
has-flag "^3.0.0"
-supports-color@6.1.0, supports-color@^6.1.0:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
- integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
- dependencies:
- has-flag "^3.0.0"
-
supports-color@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a"
@@ -26596,6 +26537,13 @@ supports-color@^5.0.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-co
dependencies:
has-flag "^3.0.0"
+supports-color@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
+ integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
+ dependencies:
+ has-flag "^3.0.0"
+
supports-color@^7.0.0, supports-color@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
@@ -28406,10 +28354,10 @@ uuid@^8.0.0, uuid@^8.3.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea"
integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==
-v8-compile-cache@2.0.3, v8-compile-cache@^2.0.3:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe"
- integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==
+v8-compile-cache@^2.0.3, v8-compile-cache@^2.1.1:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132"
+ integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==
v8-to-istanbul@^5.0.1:
version "5.0.1"
@@ -29156,22 +29104,22 @@ webidl-conversions@^6.1.0:
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
-webpack-cli@^3.3.10:
- version "3.3.10"
- resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.10.tgz#17b279267e9b4fb549023fae170da8e6e766da13"
- integrity sha512-u1dgND9+MXaEt74sJR4PR7qkPxXUSQ0RXYq8x1L6Jg1MYVEmGPrH6Ah6C4arD4r0J1P5HKjRqpab36k0eIzPqg==
+webpack-cli@^3.3.12:
+ version "3.3.12"
+ resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.12.tgz#94e9ada081453cd0aa609c99e500012fd3ad2d4a"
+ integrity sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag==
dependencies:
- chalk "2.4.2"
- cross-spawn "6.0.5"
- enhanced-resolve "4.1.0"
- findup-sync "3.0.0"
- global-modules "2.0.0"
- import-local "2.0.0"
- interpret "1.2.0"
- loader-utils "1.2.3"
- supports-color "6.1.0"
- v8-compile-cache "2.0.3"
- yargs "13.2.4"
+ chalk "^2.4.2"
+ cross-spawn "^6.0.5"
+ enhanced-resolve "^4.1.1"
+ findup-sync "^3.0.0"
+ global-modules "^2.0.0"
+ import-local "^2.0.0"
+ interpret "^1.4.0"
+ loader-utils "^1.4.0"
+ supports-color "^6.1.0"
+ v8-compile-cache "^2.1.1"
+ yargs "^13.3.2"
webpack-dev-middleware@^3.7.0, webpack-dev-middleware@^3.7.2:
version "3.7.2"
@@ -29184,7 +29132,7 @@ webpack-dev-middleware@^3.7.0, webpack-dev-middleware@^3.7.2:
range-parser "^1.2.1"
webpack-log "^2.0.0"
-webpack-dev-server@^3.8.2:
+webpack-dev-server@^3.11.0:
version "3.11.0"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz#8f154a3bce1bcfd1cc618ef4e703278855e7ff8c"
integrity sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==
@@ -29806,7 +29754,7 @@ yaml@^1.7.2:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
-yargs-parser@13.1.2, yargs-parser@^13.1.0, yargs-parser@^13.1.2:
+yargs-parser@13.1.2, yargs-parser@^13.1.2:
version "13.1.2"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
@@ -29844,23 +29792,6 @@ yargs-unparser@1.6.0:
lodash "^4.17.15"
yargs "^13.3.0"
-yargs@13.2.4:
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83"
- integrity sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==
- dependencies:
- cliui "^5.0.0"
- find-up "^3.0.0"
- get-caller-file "^2.0.1"
- os-locale "^3.1.0"
- require-directory "^2.1.1"
- require-main-filename "^2.0.0"
- set-blocking "^2.0.0"
- string-width "^3.0.0"
- which-module "^2.0.0"
- y18n "^4.0.0"
- yargs-parser "^13.1.0"
-
yargs@13.3.2, yargs@^13.2.2, yargs@^13.3.0, yargs@^13.3.2:
version "13.3.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"