diff --git a/docs/development/core/server/kibana-plugin-server.irouter.delete.md b/docs/development/core/server/kibana-plugin-server.irouter.delete.md
index 9124b4a1b21c4..5202e0cfd5ebb 100644
--- a/docs/development/core/server/kibana-plugin-server.irouter.delete.md
+++ b/docs/development/core/server/kibana-plugin-server.irouter.delete.md
@@ -9,5 +9,5 @@ Register a route handler for `DELETE` request.
Signature:
```typescript
-delete:
(route: RouteConfig
, handler: RequestHandler
) => void;
+delete: RouteRegistrar;
```
diff --git a/docs/development/core/server/kibana-plugin-server.irouter.get.md b/docs/development/core/server/kibana-plugin-server.irouter.get.md
index 0291906c6fc6b..32552a49cb999 100644
--- a/docs/development/core/server/kibana-plugin-server.irouter.get.md
+++ b/docs/development/core/server/kibana-plugin-server.irouter.get.md
@@ -9,5 +9,5 @@ Register a route handler for `GET` request.
Signature:
```typescript
-get:
(route: RouteConfig
, handler: RequestHandler
) => void;
+get: RouteRegistrar;
```
diff --git a/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md b/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md
new file mode 100644
index 0000000000000..2367420068064
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRouter](./kibana-plugin-server.irouter.md) > [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md)
+
+## IRouter.handleLegacyErrors property
+
+Wrap a router handler to catch and converts legacy boom errors to proper custom errors.
+
+Signature:
+
+```typescript
+handleLegacyErrors:
(handler: RequestHandler
) => RequestHandler
;
+```
diff --git a/docs/development/core/server/kibana-plugin-server.irouter.md b/docs/development/core/server/kibana-plugin-server.irouter.md
index bbffe1e42f229..b5d3c893d745d 100644
--- a/docs/development/core/server/kibana-plugin-server.irouter.md
+++ b/docs/development/core/server/kibana-plugin-server.irouter.md
@@ -16,9 +16,10 @@ export interface IRouter
| Property | Type | Description |
| --- | --- | --- |
-| [delete](./kibana-plugin-server.irouter.delete.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void
| Register a route handler for DELETE
request. |
-| [get](./kibana-plugin-server.irouter.get.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void
| Register a route handler for GET
request. |
-| [post](./kibana-plugin-server.irouter.post.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void
| Register a route handler for POST
request. |
-| [put](./kibana-plugin-server.irouter.put.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void
| Register a route handler for PUT
request. |
+| [delete](./kibana-plugin-server.irouter.delete.md) | RouteRegistrar
| Register a route handler for DELETE
request. |
+| [get](./kibana-plugin-server.irouter.get.md) | RouteRegistrar
| Register a route handler for GET
request. |
+| [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B>
| Wrap a router handler to catch and converts legacy boom errors to proper custom errors. |
+| [post](./kibana-plugin-server.irouter.post.md) | RouteRegistrar
| Register a route handler for POST
request. |
+| [put](./kibana-plugin-server.irouter.put.md) | RouteRegistrar
| Register a route handler for PUT
request. |
| [routerPath](./kibana-plugin-server.irouter.routerpath.md) | string
| Resulted path |
diff --git a/docs/development/core/server/kibana-plugin-server.irouter.post.md b/docs/development/core/server/kibana-plugin-server.irouter.post.md
index e97a32e433ce9..cd655c9ce0dc8 100644
--- a/docs/development/core/server/kibana-plugin-server.irouter.post.md
+++ b/docs/development/core/server/kibana-plugin-server.irouter.post.md
@@ -9,5 +9,5 @@ Register a route handler for `POST` request.
Signature:
```typescript
-post:
(route: RouteConfig
, handler: RequestHandler
) => void;
+post: RouteRegistrar;
```
diff --git a/docs/development/core/server/kibana-plugin-server.irouter.put.md b/docs/development/core/server/kibana-plugin-server.irouter.put.md
index 25db91e389939..e553d4b79dd2b 100644
--- a/docs/development/core/server/kibana-plugin-server.irouter.put.md
+++ b/docs/development/core/server/kibana-plugin-server.irouter.put.md
@@ -9,5 +9,5 @@ Register a route handler for `PUT` request.
Signature:
```typescript
-put:
(route: RouteConfig
, handler: RequestHandler
) => void;
+put: RouteRegistrar;
```
diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md
index c6ab8502acbd2..360675b3490c2 100644
--- a/docs/development/core/server/kibana-plugin-server.md
+++ b/docs/development/core/server/kibana-plugin-server.md
@@ -170,6 +170,7 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [ResponseErrorAttributes](./kibana-plugin-server.responseerrorattributes.md) | Additional data to provide error details. |
| [ResponseHeaders](./kibana-plugin-server.responseheaders.md) | Http response headers to set. |
| [RouteMethod](./kibana-plugin-server.routemethod.md) | The set of common HTTP methods supported by Kibana routing. |
+| [RouteRegistrar](./kibana-plugin-server.routeregistrar.md) | Handler to declare a route. |
| [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | Type definition for a Saved Object attribute value |
| [SavedObjectAttributeSingle](./kibana-plugin-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) |
| [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError()
helpers exposed at SavedObjectsErrorHelpers
should be used to understand and manage error responses from the SavedObjectsClient
.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type
or doing substring checks on error.body.error.reason
, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index
setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) |
diff --git a/docs/development/core/server/kibana-plugin-server.routeregistrar.md b/docs/development/core/server/kibana-plugin-server.routeregistrar.md
new file mode 100644
index 0000000000000..535927dc73743
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.routeregistrar.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteRegistrar](./kibana-plugin-server.routeregistrar.md)
+
+## RouteRegistrar type
+
+Handler to declare a route.
+
+Signature:
+
+```typescript
+export declare type RouteRegistrar =
(route: RouteConfig
, handler: RequestHandler
) => void;
+```
diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts
index 00c9aedc42cfb..e9a2571382edc 100644
--- a/src/core/server/http/http_service.mock.ts
+++ b/src/core/server/http/http_service.mock.ts
@@ -45,6 +45,7 @@ const createRouterMock = (): jest.Mocked => ({
put: jest.fn(),
delete: jest.fn(),
getRoutes: jest.fn(),
+ handleLegacyErrors: jest.fn().mockImplementation(handler => handler),
});
const createSetupContractMock = () => {
diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts
index ff1ff3acfae3d..2fa67750f6406 100644
--- a/src/core/server/http/index.ts
+++ b/src/core/server/http/index.ts
@@ -45,6 +45,7 @@ export {
IRouter,
RouteMethod,
RouteConfigOptions,
+ RouteRegistrar,
} from './router';
export { BasePathProxyServer } from './base_path_proxy_server';
export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth';
diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts
index 70d7ae00f917e..481d8e1bbf49b 100644
--- a/src/core/server/http/integration_tests/router.test.ts
+++ b/src/core/server/http/integration_tests/router.test.ts
@@ -164,6 +164,53 @@ describe('Handler', () => {
});
});
+describe('handleLegacyErrors', () => {
+ it('properly convert Boom errors', async () => {
+ const { server: innerServer, createRouter } = await server.setup(setupDeps);
+ const router = createRouter('/');
+
+ router.get(
+ { path: '/', validate: false },
+ router.handleLegacyErrors((context, req, res) => {
+ throw Boom.notFound();
+ })
+ );
+ await server.start();
+
+ const result = await supertest(innerServer.listener)
+ .get('/')
+ .expect(404);
+
+ expect(result.body.message).toBe('Not Found');
+ });
+
+ it('returns default error when non-Boom errors are thrown', async () => {
+ const { server: innerServer, createRouter } = await server.setup(setupDeps);
+ const router = createRouter('/');
+
+ router.get(
+ {
+ path: '/',
+ validate: false,
+ },
+ router.handleLegacyErrors((context, req, res) => {
+ throw new Error('Unexpected');
+ })
+ );
+ await server.start();
+
+ const result = await supertest(innerServer.listener)
+ .get('/')
+ .expect(500);
+
+ expect(result.body).toEqual({
+ error: 'Internal Server Error',
+ message: 'An internal server error occurred.',
+ statusCode: 500,
+ });
+ });
+});
+
describe('Response factory', () => {
describe('Success', () => {
it('supports answering with json object', async () => {
diff --git a/src/core/server/http/router/error_wrapper.test.ts b/src/core/server/http/router/error_wrapper.test.ts
new file mode 100644
index 0000000000000..aa20b49dc9c91
--- /dev/null
+++ b/src/core/server/http/router/error_wrapper.test.ts
@@ -0,0 +1,80 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import Boom from 'boom';
+import { KibanaResponse, KibanaResponseFactory, kibanaResponseFactory } from './response';
+import { wrapErrors } from './error_wrapper';
+import { KibanaRequest, RequestHandler, RequestHandlerContext } from 'kibana/server';
+
+const createHandler = (handler: () => any): RequestHandler => () => {
+ return handler();
+};
+
+describe('wrapErrors', () => {
+ let context: RequestHandlerContext;
+ let request: KibanaRequest;
+ let response: KibanaResponseFactory;
+
+ beforeEach(() => {
+ context = {} as any;
+ request = {} as any;
+ response = kibanaResponseFactory;
+ });
+
+ it('should pass-though call parameters to the handler', async () => {
+ const handler = jest.fn();
+ const wrapped = wrapErrors(handler);
+ await wrapped(context, request, response);
+ expect(handler).toHaveBeenCalledWith(context, request, response);
+ });
+
+ it('should pass-though result from the handler', async () => {
+ const handler = createHandler(() => {
+ return 'handler-response';
+ });
+ const wrapped = wrapErrors(handler);
+ const result = await wrapped(context, request, response);
+ expect(result).toBe('handler-response');
+ });
+
+ it('should intercept and convert thrown Boom errors', async () => {
+ const handler = createHandler(() => {
+ throw Boom.notFound('not there');
+ });
+ const wrapped = wrapErrors(handler);
+ const result = await wrapped(context, request, response);
+ expect(result).toBeInstanceOf(KibanaResponse);
+ expect(result.status).toBe(404);
+ expect(result.payload).toEqual({
+ error: 'Not Found',
+ message: 'not there',
+ statusCode: 404,
+ });
+ });
+
+ it('should re-throw non-Boom errors', async () => {
+ const handler = createHandler(() => {
+ throw new Error('something went bad');
+ });
+ const wrapped = wrapErrors(handler);
+ await expect(wrapped(context, request, response)).rejects.toMatchInlineSnapshot(
+ `[Error: something went bad]`
+ );
+ });
+});
diff --git a/src/core/server/http/router/error_wrapper.ts b/src/core/server/http/router/error_wrapper.ts
new file mode 100644
index 0000000000000..706a9fe3b8887
--- /dev/null
+++ b/src/core/server/http/router/error_wrapper.ts
@@ -0,0 +1,48 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import Boom from 'boom';
+import { ObjectType, TypeOf } from '@kbn/config-schema';
+import { KibanaRequest } from './request';
+import { KibanaResponseFactory } from './response';
+import { RequestHandler } from './router';
+import { RequestHandlerContext } from '../../../server';
+
+export const wrapErrors = (
+ handler: RequestHandler
+): RequestHandler
=> {
+ return async (
+ context: RequestHandlerContext,
+ request: KibanaRequest, TypeOf, TypeOf>,
+ response: KibanaResponseFactory
+ ) => {
+ try {
+ return await handler(context, request, response);
+ } catch (e) {
+ if (Boom.isBoom(e)) {
+ return response.customError({
+ body: e.output.payload,
+ statusCode: e.output.statusCode,
+ headers: e.output.headers,
+ });
+ }
+ throw e;
+ }
+ };
+};
diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts
index 56ed9ca11edc1..f07ad3cfe85c0 100644
--- a/src/core/server/http/router/index.ts
+++ b/src/core/server/http/router/index.ts
@@ -18,7 +18,7 @@
*/
export { Headers, filterHeaders, ResponseHeaders, KnownHeaders } from './headers';
-export { Router, RequestHandler, IRouter } from './router';
+export { Router, RequestHandler, IRouter, RouteRegistrar } from './router';
export {
KibanaRequest,
KibanaRequestRoute,
diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts
index 6b7e2e3ad14cd..a13eae51a19a6 100644
--- a/src/core/server/http/router/router.ts
+++ b/src/core/server/http/router/router.ts
@@ -27,6 +27,7 @@ import { KibanaResponseFactory, kibanaResponseFactory, IKibanaResponse } from '.
import { RouteConfig, RouteConfigOptions, RouteMethod, RouteSchemas } from './route';
import { HapiResponseAdapter } from './response_adapter';
import { RequestHandlerContext } from '../../../server';
+import { wrapErrors } from './error_wrapper';
interface RouterRoute {
method: RouteMethod;
@@ -35,6 +36,15 @@ interface RouterRoute {
handler: (req: Request, responseToolkit: ResponseToolkit) => Promise>;
}
+/**
+ * Handler to declare a route.
+ * @public
+ */
+export type RouteRegistrar = (
+ route: RouteConfig
,
+ handler: RequestHandler
+) => void;
+
/**
* Registers route handlers for specified resource path and method.
* See {@link RouteConfig} and {@link RequestHandler} for more information about arguments to route registrations.
@@ -52,40 +62,36 @@ export interface IRouter {
* @param route {@link RouteConfig} - a route configuration.
* @param handler {@link RequestHandler} - a function to call to respond to an incoming request
*/
- get:
(
- route: RouteConfig
,
- handler: RequestHandler
- ) => void;
+ get: RouteRegistrar;
/**
* Register a route handler for `POST` request.
* @param route {@link RouteConfig} - a route configuration.
* @param handler {@link RequestHandler} - a function to call to respond to an incoming request
*/
- post:
(
- route: RouteConfig
,
- handler: RequestHandler
- ) => void;
+ post: RouteRegistrar;
/**
* Register a route handler for `PUT` request.
* @param route {@link RouteConfig} - a route configuration.
* @param handler {@link RequestHandler} - a function to call to respond to an incoming request
*/
- put:
(
- route: RouteConfig
,
- handler: RequestHandler
- ) => void;
+ put: RouteRegistrar;
/**
* Register a route handler for `DELETE` request.
* @param route {@link RouteConfig} - a route configuration.
* @param handler {@link RequestHandler} - a function to call to respond to an incoming request
*/
- delete:
(
- route: RouteConfig
,
+ delete: RouteRegistrar;
+
+ /**
+ * Wrap a router handler to catch and converts legacy boom errors to proper custom errors.
+ * @param handler {@link RequestHandler} - a route handler to wrap
+ */
+ handleLegacyErrors:
(
handler: RequestHandler
- ) => void;
+ ) => RequestHandler
;
/**
* Returns all routes registered with the this router.
@@ -188,6 +194,12 @@ export class Router implements IRouter {
return [...this.routes];
}
+ public handleLegacyErrors
(
+ handler: RequestHandler
+ ): RequestHandler
{
+ return wrapErrors(handler);
+ }
+
private async handle
({
routeSchemas,
request,
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index 987e4e64f9d5b..31dec2c9b96ff 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -114,6 +114,7 @@ export {
IRouter,
RouteMethod,
RouteConfigOptions,
+ RouteRegistrar,
SessionStorage,
SessionStorageCookieOptions,
SessionStorageFactory,
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 066f79bfd38f3..d6cfa54397565 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -714,14 +714,15 @@ export interface IndexSettingsDeprecationInfo {
// @public
export interface IRouter {
- delete:
(route: RouteConfig
, handler: RequestHandler
) => void;
- get:
(route: RouteConfig
, handler: RequestHandler
) => void;
+ delete: RouteRegistrar;
+ get: RouteRegistrar;
// Warning: (ae-forgotten-export) The symbol "RouterRoute" needs to be exported by the entry point index.d.ts
//
// @internal
getRoutes: () => RouterRoute[];
- post:
(route: RouteConfig
, handler: RequestHandler
) => void;
- put:
(route: RouteConfig
, handler: RequestHandler
) => void;
+ handleLegacyErrors:
(handler: RequestHandler
) => RequestHandler
;
+ post: RouteRegistrar;
+ put: RouteRegistrar;
routerPath: string;
}
@@ -1099,6 +1100,9 @@ export interface RouteConfigOptions {
// @public
export type RouteMethod = 'get' | 'post' | 'put' | 'delete';
+// @public
+export type RouteRegistrar =
(route: RouteConfig
, handler: RequestHandler
) => void;
+
// @public (undocumented)
export interface SavedObject {
attributes: T;
diff --git a/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js b/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js
index e6cae3f1710bc..0c00b2f93ee6f 100644
--- a/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js
+++ b/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js
@@ -21,7 +21,7 @@ import { ListComponent } from './list_component';
export class TemplateAutocompleteComponent extends ListComponent {
constructor(name, parent) {
- super(name, mappings.getTemplates, parent);
+ super(name, mappings.getTemplates, parent, true, true);
}
getContextKey() {
return 'template';
diff --git a/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js b/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js
index e54bc2476698a..c2596dc4258da 100644
--- a/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js
+++ b/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js
@@ -352,6 +352,13 @@ const rules = {
},
missing: '',
},
+ cumulative_cardinality: {
+ __template: {
+ buckets_path: '',
+ },
+ buckets_path: '',
+ format: '',
+ },
scripted_metric: {
__template: {
init_script: '',
diff --git a/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json b/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json
index cc7218be2e48d..c19836e2f9eb0 100644
--- a/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json
+++ b/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json
@@ -1,10 +1,16 @@
{
"indices.put_template": {
"data_autocomplete_rules": {
- "template": "index*",
- "warmers": { "__scope_link": "_warmer" },
+ "index_patterns": [],
"mappings": { "__scope_link": "put_mapping" },
- "settings": { "__scope_link": "put_settings" }
+ "settings": { "__scope_link": "put_settings" },
+ "version": 0,
+ "order": 0,
+ "aliases": {
+ "__template": {
+ "NAME": {}
+ }
+ }
},
"patterns": [
"_template/{template}"
diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts
index 38684cb22e237..378e32cb3582c 100644
--- a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts
+++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts
@@ -5,3 +5,4 @@
*/
export * from './results';
+export * from './validation';
diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts
new file mode 100644
index 0000000000000..727faca69298e
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/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 * from './indices';
diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts
new file mode 100644
index 0000000000000..62d81dc136853
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts
@@ -0,0 +1,51 @@
+/*
+ * 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 * as rt from 'io-ts';
+
+export const LOG_ANALYSIS_VALIDATION_INDICES_PATH = '/api/infra/log_analysis/validation/indices';
+
+/**
+ * Request types
+ */
+export const validationIndicesRequestPayloadRT = rt.type({
+ data: rt.type({
+ timestampField: rt.string,
+ indices: rt.array(rt.string),
+ }),
+});
+
+export type ValidationIndicesRequestPayload = rt.TypeOf;
+
+/**
+ * Response types
+ * */
+export const validationIndicesErrorRT = rt.union([
+ rt.type({
+ error: rt.literal('INDEX_NOT_FOUND'),
+ index: rt.string,
+ }),
+ rt.type({
+ error: rt.literal('FIELD_NOT_FOUND'),
+ index: rt.string,
+ field: rt.string,
+ }),
+ rt.type({
+ error: rt.literal('FIELD_NOT_VALID'),
+ index: rt.string,
+ field: rt.string,
+ }),
+]);
+
+export type ValidationIndicesError = rt.TypeOf;
+
+export const validationIndicesResponsePayloadRT = rt.type({
+ data: rt.type({
+ errors: rt.array(validationIndicesErrorRT),
+ }),
+});
+
+export type ValidationIndicesResponsePayload = rt.TypeOf;
diff --git a/x-pack/legacy/plugins/infra/package.json b/x-pack/legacy/plugins/infra/package.json
index 63812bb2da513..7aa8cb9b5269a 100644
--- a/x-pack/legacy/plugins/infra/package.json
+++ b/x-pack/legacy/plugins/infra/package.json
@@ -16,4 +16,4 @@
"boom": "7.3.0",
"lodash": "^4.17.15"
}
-}
\ No newline at end of file
+}
diff --git a/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx b/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx
index a99b265fc3ea9..5df1fc07e83b9 100644
--- a/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx
+++ b/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx
@@ -40,4 +40,5 @@ const OverlayDiv = euiStyled.div`
position: absolute;
top: 0;
width: 100%;
+ z-index: ${props => props.theme.eui.euiZLevel1};
`;
diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts
new file mode 100644
index 0000000000000..440ee10e4223d
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 { fold } from 'fp-ts/lib/Either';
+import { pipe } from 'fp-ts/lib/pipeable';
+import { identity } from 'fp-ts/lib/function';
+import { kfetch } from 'ui/kfetch';
+
+import {
+ LOG_ANALYSIS_VALIDATION_INDICES_PATH,
+ validationIndicesRequestPayloadRT,
+ validationIndicesResponsePayloadRT,
+} from '../../../../../common/http_api';
+
+import { throwErrors, createPlainError } from '../../../../../common/runtime_types';
+
+export const callIndexPatternsValidate = async (timestampField: string, indices: string[]) => {
+ const response = await kfetch({
+ method: 'POST',
+ pathname: LOG_ANALYSIS_VALIDATION_INDICES_PATH,
+ body: JSON.stringify(
+ validationIndicesRequestPayloadRT.encode({ data: { timestampField, indices } })
+ ),
+ });
+
+ return pipe(
+ validationIndicesResponsePayloadRT.decode(response),
+ fold(throwErrors(createPlainError), identity)
+ );
+};
diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx
index 163f0e39d1228..0f386f416b866 100644
--- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx
+++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx
@@ -94,8 +94,8 @@ export const useLogAnalysisJobs = ({
dispatch({ type: 'fetchingJobStatuses' });
return await callJobsSummaryAPI(spaceId, sourceId);
},
- onResolve: response => {
- dispatch({ type: 'fetchedJobStatuses', payload: response, spaceId, sourceId });
+ onResolve: jobResponse => {
+ dispatch({ type: 'fetchedJobStatuses', payload: jobResponse, spaceId, sourceId });
},
onReject: err => {
dispatch({ type: 'failedFetchingJobStatuses' });
@@ -158,6 +158,7 @@ export const useLogAnalysisJobs = ({
setup: setupMlModule,
setupMlModuleRequest,
setupStatus: statusState.setupStatus,
+ timestampField: timeField,
viewSetupForReconfiguration,
viewSetupForUpdate,
viewResults,
diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx
index 7942657018455..c965c50bedccc 100644
--- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx
+++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx
@@ -4,9 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { useState, useCallback, useMemo } from 'react';
+import { useState, useCallback, useMemo, useEffect } from 'react';
import { isExampleDataIndex } from '../../../../common/log_analysis';
+import {
+ ValidationIndicesError,
+ ValidationIndicesResponsePayload,
+} from '../../../../common/http_api';
+import { useTrackedPromise } from '../../../utils/use_tracked_promise';
+import { callIndexPatternsValidate } from './api/index_patterns_validate';
type SetupHandler = (
indices: string[],
@@ -14,42 +20,75 @@ type SetupHandler = (
endTime: number | undefined
) => void;
+export type ValidationIndicesUIError =
+ | ValidationIndicesError
+ | { error: 'NETWORK_ERROR' }
+ | { error: 'TOO_FEW_SELECTED_INDICES' };
+
+export interface ValidatedIndex {
+ index: string;
+ errors: ValidationIndicesError[];
+ isSelected: boolean;
+}
+
interface AnalysisSetupStateArguments {
availableIndices: string[];
cleanupAndSetupModule: SetupHandler;
setupModule: SetupHandler;
+ timestampField: string;
}
-type IndicesSelection = Record;
-
-type ValidationErrors = 'TOO_FEW_SELECTED_INDICES';
-
const fourWeeksInMs = 86400000 * 7 * 4;
export const useAnalysisSetupState = ({
availableIndices,
cleanupAndSetupModule,
setupModule,
+ timestampField,
}: AnalysisSetupStateArguments) => {
const [startTime, setStartTime] = useState(Date.now() - fourWeeksInMs);
const [endTime, setEndTime] = useState(undefined);
- const [selectedIndices, setSelectedIndices] = useState(
- availableIndices.reduce(
- (indexMap, indexName) => ({
- ...indexMap,
- [indexName]: !(availableIndices.length > 1 && isExampleDataIndex(indexName)),
- }),
- {}
- )
+ // Prepare the validation
+ const [validatedIndices, setValidatedIndices] = useState(
+ availableIndices.map(index => ({
+ index,
+ errors: [],
+ isSelected: false,
+ }))
+ );
+ const [validateIndicesRequest, validateIndices] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'resolution',
+ createPromise: async () => {
+ return await callIndexPatternsValidate(timestampField, availableIndices);
+ },
+ onResolve: ({ data }: ValidationIndicesResponsePayload) => {
+ setValidatedIndices(
+ availableIndices.map(index => {
+ const errors = data.errors.filter(error => error.index === index);
+ return {
+ index,
+ errors,
+ isSelected: errors.length === 0 && !isExampleDataIndex(index),
+ };
+ })
+ );
+ },
+ onReject: () => {
+ setValidatedIndices([]);
+ },
+ },
+ [availableIndices, timestampField]
);
+ useEffect(() => {
+ validateIndices();
+ }, [validateIndices]);
+
const selectedIndexNames = useMemo(
- () =>
- Object.entries(selectedIndices)
- .filter(([_indexName, isSelected]) => isSelected)
- .map(([indexName]) => indexName),
- [selectedIndices]
+ () => validatedIndices.filter(i => i.isSelected).map(i => i.index),
+ [validatedIndices]
);
const setup = useCallback(() => {
@@ -60,24 +99,42 @@ export const useAnalysisSetupState = ({
return cleanupAndSetupModule(selectedIndexNames, startTime, endTime);
}, [cleanupAndSetupModule, selectedIndexNames, startTime, endTime]);
- const validationErrors: ValidationErrors[] = useMemo(
+ const isValidating = useMemo(
() =>
- Object.values(selectedIndices).some(isSelected => isSelected)
- ? []
- : ['TOO_FEW_SELECTED_INDICES' as const],
- [selectedIndices]
+ validateIndicesRequest.state === 'pending' ||
+ validateIndicesRequest.state === 'uninitialized',
+ [validateIndicesRequest.state]
);
+ const validationErrors = useMemo(() => {
+ if (isValidating) {
+ return [];
+ }
+
+ if (validateIndicesRequest.state === 'rejected') {
+ return [{ error: 'NETWORK_ERROR' }];
+ }
+
+ if (selectedIndexNames.length === 0) {
+ return [{ error: 'TOO_FEW_SELECTED_INDICES' }];
+ }
+
+ return validatedIndices.reduce((errors, index) => {
+ return selectedIndexNames.includes(index.index) ? errors.concat(index.errors) : errors;
+ }, []);
+ }, [selectedIndexNames, validatedIndices, validateIndicesRequest.state]);
+
return {
cleanupAndSetup,
endTime,
+ isValidating,
selectedIndexNames,
- selectedIndices,
setEndTime,
- setSelectedIndices,
setStartTime,
setup,
startTime,
+ validatedIndices,
+ setValidatedIndices,
validationErrors,
};
};
diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx
index 04d7520c0ca88..f0a26eae25ecb 100644
--- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx
+++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx
@@ -27,6 +27,7 @@ export const AnalysisPageContent = () => {
lastSetupErrorMessages,
setup,
setupStatus,
+ timestampField,
viewResults,
} = useContext(LogAnalysisJobs.Context);
@@ -61,6 +62,7 @@ export const AnalysisPageContent = () => {
errorMessages={lastSetupErrorMessages}
setup={setup}
setupStatus={setupStatus}
+ timestampField={timestampField}
viewResults={viewResults}
/>
);
diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx
index 097cccf5dca33..7ae174c4a7899 100644
--- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx
+++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx
@@ -34,6 +34,7 @@ interface AnalysisSetupContentProps {
errorMessages: string[];
setup: SetupHandler;
setupStatus: SetupStatus;
+ timestampField: string;
viewResults: () => void;
}
@@ -43,6 +44,7 @@ export const AnalysisSetupContent: React.FunctionComponent {
useTrackPageview({ app: 'infra_logs', path: 'analysis_setup' });
@@ -82,6 +84,7 @@ export const AnalysisSetupContent: React.FunctionComponent
diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx
index defcefd69a7ab..585a65b9ad1c8 100644
--- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx
+++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx
@@ -4,37 +4,58 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiCheckboxGroup, EuiCode, EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui';
+import { EuiCode, EuiDescribedFormGroup, EuiFormRow, EuiCheckbox, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useCallback, useMemo } from 'react';
-
-export type IndicesSelection = Record;
-
-export type IndicesValidationError = 'TOO_FEW_SELECTED_INDICES';
+import {
+ ValidatedIndex,
+ ValidationIndicesUIError,
+} from '../../../../../containers/logs/log_analysis/log_analysis_setup_state';
+import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper';
export const AnalysisSetupIndicesForm: React.FunctionComponent<{
- indices: IndicesSelection;
- onChangeSelectedIndices: (selectedIndices: IndicesSelection) => void;
- validationErrors?: IndicesValidationError[];
-}> = ({ indices, onChangeSelectedIndices, validationErrors = [] }) => {
+ indices: ValidatedIndex[];
+ isValidating: boolean;
+ onChangeSelectedIndices: (selectedIndices: ValidatedIndex[]) => void;
+ valid: boolean;
+}> = ({ indices, isValidating, onChangeSelectedIndices, valid }) => {
+ const handleCheckboxChange = useCallback(
+ (event: React.ChangeEvent) => {
+ onChangeSelectedIndices(
+ indices.map(index => {
+ const checkbox = event.currentTarget;
+ return index.index === checkbox.id ? { ...index, isSelected: checkbox.checked } : index;
+ })
+ );
+ },
+ [indices, onChangeSelectedIndices]
+ );
+
const choices = useMemo(
() =>
- Object.keys(indices).map(indexName => ({
- id: indexName,
- label: {indexName},
- })),
- [indices]
- );
+ indices.map(index => {
+ const validIndex = index.errors.length === 0;
+ const checkbox = (
+ {index.index}}
+ onChange={handleCheckboxChange}
+ checked={index.isSelected}
+ disabled={!validIndex}
+ />
+ );
- const handleCheckboxGroupChange = useCallback(
- indexName => {
- onChangeSelectedIndices({
- ...indices,
- [indexName]: !indices[indexName],
- });
- },
- [indices, onChangeSelectedIndices]
+ return validIndex ? (
+ checkbox
+ ) : (
+
+ {checkbox}
+
+ );
+ }),
+ [indices]
);
return (
@@ -53,20 +74,17 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{
/>
}
>
- 0}
- label={indicesSelectionLabel}
- labelType="legend"
- >
-
-
+
+
+ <>{choices}>
+
+
);
};
@@ -75,14 +93,50 @@ const indicesSelectionLabel = i18n.translate('xpack.infra.analysisSetup.indicesS
defaultMessage: 'Indices',
});
-const formatValidationError = (validationError: IndicesValidationError) => {
- switch (validationError) {
- case 'TOO_FEW_SELECTED_INDICES':
- return i18n.translate(
- 'xpack.infra.analysisSetup.indicesSelectionTooFewSelectedIndicesDescription',
- {
- defaultMessage: 'Select at least one index name.',
- }
- );
- }
+const formatValidationError = (errors: ValidationIndicesUIError[]): React.ReactNode => {
+ return errors.map(error => {
+ switch (error.error) {
+ case 'INDEX_NOT_FOUND':
+ return (
+
+ {error.index} }}
+ />
+
+ );
+
+ case 'FIELD_NOT_FOUND':
+ return (
+
+ {error.index},
+ field: {error.field},
+ }}
+ />
+
+ );
+
+ case 'FIELD_NOT_VALID':
+ return (
+
+ {error.index},
+ field: {error.field},
+ }}
+ />
+
+ );
+
+ default:
+ return '';
+ }
+ });
};
diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx
index 929fba26f2323..3b5497fb91864 100644
--- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx
+++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx
@@ -4,24 +4,27 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiSpacer, EuiForm } from '@elastic/eui';
-import React, { useMemo } from 'react';
+import { EuiSpacer, EuiForm, EuiCallOut } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import React from 'react';
-import {
- AnalysisSetupIndicesForm,
- IndicesSelection,
- IndicesValidationError,
-} from './analysis_setup_indices_form';
+import { AnalysisSetupIndicesForm } from './analysis_setup_indices_form';
import { AnalysisSetupTimerangeForm } from './analysis_setup_timerange_form';
+import {
+ ValidatedIndex,
+ ValidationIndicesUIError,
+} from '../../../../../containers/logs/log_analysis/log_analysis_setup_state';
interface InitialConfigurationStepProps {
setStartTime: (startTime: number | undefined) => void;
setEndTime: (endTime: number | undefined) => void;
startTime: number | undefined;
endTime: number | undefined;
- selectedIndices: IndicesSelection;
- setSelectedIndices: (selectedIndices: IndicesSelection) => void;
- validationErrors?: IndicesValidationError[];
+ isValidating: boolean;
+ validatedIndices: ValidatedIndex[];
+ setValidatedIndices: (selectedIndices: ValidatedIndex[]) => void;
+ validationErrors?: ValidationIndicesUIError[];
}
export const InitialConfigurationStep: React.FunctionComponent = ({
@@ -29,16 +32,11 @@ export const InitialConfigurationStep: React.FunctionComponent {
- const indicesFormValidationErrors = useMemo(
- () =>
- validationErrors.filter(validationError => validationError === 'TOO_FEW_SELECTED_INDICES'),
- [validationErrors]
- );
-
return (
<>
@@ -50,11 +48,63 @@ export const InitialConfigurationStep: React.FunctionComponent
+
+
>
);
};
+
+const errorCalloutTitle = i18n.translate(
+ 'xpack.infra.analysisSetup.steps.initialConfigurationStep.errorCalloutTitle',
+ {
+ defaultMessage: 'Your index configuration is not valid',
+ }
+);
+
+const ValidationErrors: React.FC<{ errors: ValidationIndicesUIError[] }> = ({ errors }) => {
+ if (errors.length === 0) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+ {errors.map((error, i) => (
+ - {formatValidationError(error)}
+ ))}
+
+
+
+ >
+ );
+};
+
+const formatValidationError = (error: ValidationIndicesUIError): React.ReactNode => {
+ switch (error.error) {
+ case 'NETWORK_ERROR':
+ return (
+
+ );
+
+ case 'TOO_FEW_SELECTED_INDICES':
+ return (
+
+ );
+
+ default:
+ return '';
+ }
+};
diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx
index 27fc8a83bc086..978e45e26b733 100644
--- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx
+++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx
@@ -60,8 +60,8 @@ export const ProcessStep: React.FunctionComponent = ({
defaultMessage="Something went wrong creating the necessary ML jobs. Please ensure all selected log indices exist."
/>
- {errorMessages.map(errorMessage => (
-
+ {errorMessages.map((errorMessage, i) => (
+
{errorMessage}
))}
diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx
index aebb44d4c9372..4643516e73fac 100644
--- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx
+++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx
@@ -25,6 +25,7 @@ interface AnalysisSetupStepsProps {
errorMessages: string[];
setup: SetupHandler;
setupStatus: SetupStatus;
+ timestampField: string;
viewResults: () => void;
}
@@ -34,6 +35,7 @@ export const AnalysisSetupSteps: React.FunctionComponent {
const {
@@ -43,13 +45,15 @@ export const AnalysisSetupSteps: React.FunctionComponent
),
diff --git a/x-pack/legacy/plugins/infra/server/infra_server.ts b/x-pack/legacy/plugins/infra/server/infra_server.ts
index 98536f4c85d36..0093a6c21af57 100644
--- a/x-pack/legacy/plugins/infra/server/infra_server.ts
+++ b/x-pack/legacy/plugins/infra/server/infra_server.ts
@@ -13,7 +13,10 @@ import { createSnapshotResolvers } from './graphql/snapshot';
import { createSourceStatusResolvers } from './graphql/source_status';
import { createSourcesResolvers } from './graphql/sources';
import { InfraBackendLibs } from './lib/infra_types';
-import { initLogAnalysisGetLogEntryRateRoute } from './routes/log_analysis';
+import {
+ initLogAnalysisGetLogEntryRateRoute,
+ initIndexPatternsValidateRoute,
+} from './routes/log_analysis';
import { initMetricExplorerRoute } from './routes/metrics_explorer';
import { initMetadataRoute } from './routes/metadata';
@@ -33,6 +36,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => {
initIpToHostName(libs);
initLogAnalysisGetLogEntryRateRoute(libs);
+ initIndexPatternsValidateRoute(libs);
initMetricExplorerRoute(libs);
initMetadataRoute(libs);
};
diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts
index 1ff9b7ab62039..c7b29447ce22c 100644
--- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts
+++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts
@@ -45,7 +45,12 @@ export interface InfraBackendFrameworkAdapter {
): Promise;
callWithRequest(
req: InfraFrameworkRequest,
- method: 'indices.getAlias' | 'indices.get',
+ method: 'indices.getAlias',
+ options?: object
+ ): Promise;
+ callWithRequest(
+ req: InfraFrameworkRequest,
+ method: 'indices.get',
options?: object
): Promise;
callWithRequest(
@@ -134,14 +139,32 @@ export interface InfraDatabaseMultiResponse extends InfraDatab
}
export interface InfraDatabaseFieldCapsResponse extends InfraDatabaseResponse {
+ indices: string[];
fields: InfraFieldsResponse;
}
+export interface InfraDatabaseGetIndicesAliasResponse {
+ [indexName: string]: {
+ aliases: {
+ [aliasName: string]: any;
+ };
+ };
+}
+
export interface InfraDatabaseGetIndicesResponse {
[indexName: string]: {
aliases: {
[aliasName: string]: any;
};
+ mappings: {
+ _meta: object;
+ dynamic_templates: any[];
+ date_detection: boolean;
+ properties: {
+ [fieldName: string]: any;
+ };
+ };
+ settings: { index: object };
};
}
diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts
index 38684cb22e237..7364d167efe47 100644
--- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts
+++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts
@@ -5,3 +5,4 @@
*/
export * from './results';
+export * from './index_patterns';
diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts
new file mode 100644
index 0000000000000..a85e119e7318a
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/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 * from './validate';
diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts
new file mode 100644
index 0000000000000..0a369adb7ca29
--- /dev/null
+++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts
@@ -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 Boom from 'boom';
+
+import { pipe } from 'fp-ts/lib/pipeable';
+import { fold } from 'fp-ts/lib/Either';
+import { identity } from 'fp-ts/lib/function';
+
+import { InfraBackendLibs } from '../../../lib/infra_types';
+import {
+ LOG_ANALYSIS_VALIDATION_INDICES_PATH,
+ validationIndicesRequestPayloadRT,
+ validationIndicesResponsePayloadRT,
+ ValidationIndicesError,
+} from '../../../../common/http_api';
+
+import { throwErrors } from '../../../../common/runtime_types';
+
+const partitionField = 'event.dataset';
+
+export const initIndexPatternsValidateRoute = ({ framework }: InfraBackendLibs) => {
+ framework.registerRoute({
+ method: 'POST',
+ path: LOG_ANALYSIS_VALIDATION_INDICES_PATH,
+ handler: async (req, res) => {
+ const payload = pipe(
+ validationIndicesRequestPayloadRT.decode(req.payload),
+ fold(throwErrors(Boom.badRequest), identity)
+ );
+
+ const { timestampField, indices } = payload.data;
+ const errors: ValidationIndicesError[] = [];
+
+ // Query each pattern individually, to map correctly the errors
+ await Promise.all(
+ indices.map(async index => {
+ const fieldCaps = await framework.callWithRequest(req, 'fieldCaps', {
+ index,
+ fields: `${timestampField},${partitionField}`,
+ });
+
+ if (fieldCaps.indices.length === 0) {
+ errors.push({
+ error: 'INDEX_NOT_FOUND',
+ index,
+ });
+ return;
+ }
+
+ ([
+ [timestampField, 'date'],
+ [partitionField, 'keyword'],
+ ] as const).forEach(([field, fieldType]) => {
+ const fieldMetadata = fieldCaps.fields[field];
+
+ if (fieldMetadata === undefined) {
+ errors.push({
+ error: 'FIELD_NOT_FOUND',
+ index,
+ field,
+ });
+ } else {
+ const fieldTypes = Object.keys(fieldMetadata);
+
+ if (fieldTypes.length > 1 || fieldTypes[0] !== fieldType) {
+ errors.push({
+ error: `FIELD_NOT_VALID`,
+ index,
+ field,
+ });
+ }
+ }
+ });
+ })
+ );
+
+ return res.response(validationIndicesResponsePayloadRT.encode({ data: { errors } }));
+ },
+ });
+};
diff --git a/x-pack/legacy/plugins/maps/common/constants.js b/x-pack/legacy/plugins/maps/common/constants.js
index 691c679e5290b..3b2f887e13c87 100644
--- a/x-pack/legacy/plugins/maps/common/constants.js
+++ b/x-pack/legacy/plugins/maps/common/constants.js
@@ -68,7 +68,7 @@ export const ZOOM_PRECISION = 2;
export const ES_SIZE_LIMIT = 10000;
export const FEATURE_ID_PROPERTY_NAME = '__kbn__feature_id__';
-export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn__isvisible__';
+export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn_isvisibleduetojoin__';
export const MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER = '_';
diff --git a/x-pack/legacy/plugins/maps/public/layers/util/mb_filter_expressions.js b/x-pack/legacy/plugins/maps/public/layers/util/mb_filter_expressions.js
new file mode 100644
index 0000000000000..393c290d69668
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/public/layers/util/mb_filter_expressions.js
@@ -0,0 +1,62 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { GEO_JSON_TYPE, FEATURE_VISIBLE_PROPERTY_NAME } from '../../../common/constants';
+
+const VISIBILITY_FILTER_CLAUSE = ['all',
+ [
+ '==',
+ ['get', FEATURE_VISIBLE_PROPERTY_NAME],
+ true
+ ]
+];
+
+const CLOSED_SHAPE_MB_FILTER = [
+ 'any',
+ ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
+ ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON]
+];
+
+const VISIBLE_CLOSED_SHAPE_MB_FILTER = [
+ ...VISIBILITY_FILTER_CLAUSE,
+ CLOSED_SHAPE_MB_FILTER,
+];
+
+const ALL_SHAPE_MB_FILTER = [
+ 'any',
+ ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
+ ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
+ ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING],
+ ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING]
+];
+
+const VISIBLE_ALL_SHAPE_MB_FILTER = [
+ ...VISIBILITY_FILTER_CLAUSE,
+ ALL_SHAPE_MB_FILTER,
+];
+
+const POINT_MB_FILTER = [
+ 'any',
+ ['==', ['geometry-type'], GEO_JSON_TYPE.POINT],
+ ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT]
+];
+
+const VISIBLE_POINT_MB_FILTER = [
+ ...VISIBILITY_FILTER_CLAUSE,
+ POINT_MB_FILTER,
+];
+
+export function getFillFilterExpression(hasJoins) {
+ return hasJoins ? VISIBLE_CLOSED_SHAPE_MB_FILTER : CLOSED_SHAPE_MB_FILTER;
+}
+
+export function getLineFilterExpression(hasJoins) {
+ return hasJoins ? VISIBLE_ALL_SHAPE_MB_FILTER : ALL_SHAPE_MB_FILTER;
+}
+
+export function getPointFilterExpression(hasJoins) {
+ return hasJoins ? VISIBLE_POINT_MB_FILTER : POINT_MB_FILTER;
+}
diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js
index e6b07b983d898..9b553803606ed 100644
--- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js
+++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js
@@ -10,7 +10,6 @@ import { AbstractLayer } from './layer';
import { VectorStyle } from './styles/vector/vector_style';
import { InnerJoin } from './joins/inner_join';
import {
- GEO_JSON_TYPE,
FEATURE_ID_PROPERTY_NAME,
SOURCE_DATA_ID_ORIGIN,
FEATURE_VISIBLE_PROPERTY_NAME,
@@ -24,41 +23,11 @@ import { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DataRequestAbortError } from './util/data_request';
import { assignFeatureIds } from './util/assign_feature_ids';
-
-const VISIBILITY_FILTER_CLAUSE = ['all',
- [
- '==',
- ['get', FEATURE_VISIBLE_PROPERTY_NAME],
- true
- ]
-];
-
-const FILL_LAYER_MB_FILTER = [
- ...VISIBILITY_FILTER_CLAUSE,
- [
- 'any',
- ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
- ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON]
- ]
-];
-
-const LINE_LAYER_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE,
- [
- 'any',
- ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
- ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
- ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING],
- ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING]
- ]
-];
-
-const POINT_LAYER_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE,
- [
- 'any',
- ['==', ['geometry-type'], GEO_JSON_TYPE.POINT],
- ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT]
- ]
-];
+import {
+ getFillFilterExpression,
+ getLineFilterExpression,
+ getPointFilterExpression,
+} from './util/mb_filter_expressions';
export class VectorLayer extends AbstractLayer {
@@ -107,6 +76,10 @@ export class VectorLayer extends AbstractLayer {
});
}
+ _hasJoins() {
+ return this.getValidJoins().length > 0;
+ }
+
isDataLoaded() {
const sourceDataRequest = this.getSourceDataRequest();
if (!sourceDataRequest || !sourceDataRequest.hasData()) {
@@ -495,12 +468,14 @@ export class VectorLayer extends AbstractLayer {
}
const sourceResult = await this._syncSource(syncContext);
- if (!sourceResult.featureCollection || !sourceResult.featureCollection.features.length) {
+ if (
+ !sourceResult.featureCollection ||
+ !sourceResult.featureCollection.features.length ||
+ !this._hasJoins()) {
return;
}
const joinStates = await this._syncJoins(syncContext);
-
await this._performInnerJoins(sourceResult, joinStates, syncContext.updateSourceData);
}
@@ -571,7 +546,11 @@ export class VectorLayer extends AbstractLayer {
source: sourceId,
paint: {}
});
- mbMap.setFilter(pointLayerId, POINT_LAYER_MB_FILTER);
+ }
+
+ const filterExpr = getPointFilterExpression(this._hasJoins());
+ if (filterExpr !== mbMap.getFilter(pointLayerId)) {
+ mbMap.setFilter(pointLayerId, filterExpr);
}
this._style.setMBPaintPropertiesForPoints({
@@ -592,7 +571,11 @@ export class VectorLayer extends AbstractLayer {
type: 'symbol',
source: sourceId,
});
- mbMap.setFilter(symbolLayerId, POINT_LAYER_MB_FILTER);
+ }
+
+ const filterExpr = getPointFilterExpression(this._hasJoins());
+ if (filterExpr !== mbMap.getFilter(symbolLayerId)) {
+ mbMap.setFilter(symbolLayerId, filterExpr);
}
this._style.setMBSymbolPropertiesForPoints({
@@ -606,6 +589,7 @@ export class VectorLayer extends AbstractLayer {
const sourceId = this.getId();
const fillLayerId = this._getMbPolygonLayerId();
const lineLayerId = this._getMbLineLayerId();
+ const hasJoins = this._hasJoins();
if (!mbMap.getLayer(fillLayerId)) {
mbMap.addLayer({
id: fillLayerId,
@@ -613,7 +597,6 @@ export class VectorLayer extends AbstractLayer {
source: sourceId,
paint: {}
});
- mbMap.setFilter(fillLayerId, FILL_LAYER_MB_FILTER);
}
if (!mbMap.getLayer(lineLayerId)) {
mbMap.addLayer({
@@ -622,7 +605,6 @@ export class VectorLayer extends AbstractLayer {
source: sourceId,
paint: {}
});
- mbMap.setFilter(lineLayerId, LINE_LAYER_MB_FILTER);
}
this._style.setMBPaintProperties({
alpha: this.getAlpha(),
@@ -632,9 +614,18 @@ export class VectorLayer extends AbstractLayer {
});
this.syncVisibilityWithMb(mbMap, fillLayerId);
+ mbMap.setLayerZoomRange(fillLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
+ const fillFilterExpr = getFillFilterExpression(hasJoins);
+ if (fillFilterExpr !== mbMap.getFilter(fillLayerId)) {
+ mbMap.setFilter(fillLayerId, fillFilterExpr);
+ }
+
this.syncVisibilityWithMb(mbMap, lineLayerId);
mbMap.setLayerZoomRange(lineLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
- mbMap.setLayerZoomRange(fillLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
+ const lineFilterExpr = getLineFilterExpression(hasJoins);
+ if (lineFilterExpr !== mbMap.getFilter(lineLayerId)) {
+ mbMap.setFilter(lineLayerId, lineFilterExpr);
+ }
}
_syncStylePropertiesWithMb(mbMap) {
diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts
index 132242606d88c..7a6c7f71bc98c 100644
--- a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts
+++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts
@@ -4,9 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { LOGOUT } from '../urls';
-
export const logout = (): null => {
- cy.visit(`${Cypress.config().baseUrl}${LOGOUT}`);
+ cy.request({
+ method: 'GET',
+ url: `${Cypress.config().baseUrl}/logout`,
+ }).then(response => {
+ expect(response.status).to.eq(200);
+ });
return null;
};
diff --git a/x-pack/legacy/plugins/siem/package.json b/x-pack/legacy/plugins/siem/package.json
index ca5fefe52bcc4..29c26c5f674e3 100644
--- a/x-pack/legacy/plugins/siem/package.json
+++ b/x-pack/legacy/plugins/siem/package.json
@@ -7,7 +7,7 @@
"scripts": {
"build-graphql-types": "node scripts/generate_types_from_graphql.js",
"cypress:open": "../../../node_modules/.bin/cypress open",
- "cypress:run": "../../../node_modules/.bin/cypress run --spec ./cypress/integration/**/*.spec.ts --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./reporter_config.json; ../../../node_modules/.bin/mochawesome-merge --reportDir ../../../../target/kibana-siem/cypress/results > ../../../../target/kibana-siem/cypress/results/output.json; ../../../../node_modules/.bin/marge ../../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../../target/kibana-siem/cypress/results; mkdir -p ../../../../target/junit && cp ../../../../target/kibana-siem/cypress/results/*.xml ../../../../target/junit/"
+ "cypress:run": "../../../node_modules/.bin/cypress run --spec ./cypress/integration/**/*.spec.ts --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./reporter_config.json; status=$?; ../../../node_modules/.bin/mochawesome-merge --reportDir ../../../../target/kibana-siem/cypress/results > ../../../../target/kibana-siem/cypress/results/output.json; ../../../../node_modules/.bin/marge ../../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../../target/kibana-siem/cypress/results; mkdir -p ../../../../target/junit && cp ../../../../target/kibana-siem/cypress/results/*.xml ../../../../target/junit/ && exit $status;"
},
"devDependencies": {
"@types/lodash": "^4.14.110",
diff --git a/x-pack/legacy/plugins/task_manager/index.ts b/x-pack/legacy/plugins/task_manager/index.ts
index 0fda1490de714..0487b003bc1ef 100644
--- a/x-pack/legacy/plugins/task_manager/index.ts
+++ b/x-pack/legacy/plugins/task_manager/index.ts
@@ -72,16 +72,7 @@ export function taskManager(kibana: any) {
}
);
this.kbnServer.afterPluginsInit(() => {
- (async () => {
- // The code block below can't await directly within "afterPluginsInit"
- // callback due to circular dependency. The server isn't "ready" until
- // this code block finishes. Migrations wait for server to be ready before
- // executing. Saved objects repository waits for migrations to finish before
- // finishing the request. To avoid this, we'll await within a separate
- // function block.
- await this.kbnServer.server.kibanaMigrator.runMigrations();
- plugin.start();
- })();
+ plugin.start();
});
server.expose(setupContract);
},
diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js
index 1634bea47a69f..30b957fdf45f4 100644
--- a/x-pack/test/functional/apps/maps/joins.js
+++ b/x-pack/test/functional/apps/maps/joins.js
@@ -102,7 +102,7 @@ export default function ({ getPageObjects, getService }) {
const vectorSource = mapboxStyle.sources[VECTOR_SOURCE_ID];
const visibilitiesOfFeatures = vectorSource.data.features.map(feature => {
- return feature.properties.__kbn__isvisible__;
+ return feature.properties.__kbn_isvisibleduetojoin__;
});
expect(visibilitiesOfFeatures).to.eql([false, true, true, true]);
@@ -166,7 +166,7 @@ export default function ({ getPageObjects, getService }) {
const vectorSource = mapboxStyle.sources[VECTOR_SOURCE_ID];
const visibilitiesOfFeatures = vectorSource.data.features.map(feature => {
- return feature.properties.__kbn__isvisible__;
+ return feature.properties.__kbn_isvisibleduetojoin__;
});
expect(visibilitiesOfFeatures).to.eql([false, true, false, false]);
diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/mapbox_styles.js
index 49519b530337e..bfa4be2b067af 100644
--- a/x-pack/test/functional/apps/maps/mapbox_styles.js
+++ b/x-pack/test/functional/apps/maps/mapbox_styles.js
@@ -15,7 +15,7 @@ export const MAPBOX_STYLES = {
'all',
[
'==',
- ['get', '__kbn__isvisible__'],
+ ['get', '__kbn_isvisibleduetojoin__'],
true
],
[
@@ -89,7 +89,7 @@ export const MAPBOX_STYLES = {
'all',
[
'==',
- ['get', '__kbn__isvisible__'],
+ ['get', '__kbn_isvisibleduetojoin__'],
true
],
[
@@ -160,7 +160,7 @@ export const MAPBOX_STYLES = {
'all',
[
'==',
- ['get', '__kbn__isvisible__'],
+ ['get', '__kbn_isvisibleduetojoin__'],
true
],
[