From 47b2c12b7ec28a5745b13ce6a1054dd810520b3a Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 18 Feb 2021 12:23:30 -0500 Subject: [PATCH 01/43] [backportrc] Adds 7.12 branch and bumps 7.x (#91883) --- .backportrc.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.backportrc.json b/.backportrc.json index 2752768194e0f6..384e221329a4f6 100644 --- a/.backportrc.json +++ b/.backportrc.json @@ -3,6 +3,7 @@ "targetBranchChoices": [ { "name": "master", "checked": true }, { "name": "7.x", "checked": true }, + "7.12", "7.11", "7.10", "7.9", @@ -29,7 +30,7 @@ "targetPRLabels": ["backport"], "branchLabelMapping": { "^v8.0.0$": "master", - "^v7.12.0$": "7.x", + "^v7.13.0$": "7.x", "^v(\\d+).(\\d+).\\d+$": "$1.$2" }, "autoMerge": true, From 4304cb9e625148c1cbe1610d2a91490d25a7b931 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 18 Feb 2021 17:30:53 +0000 Subject: [PATCH 02/43] chore(NA): setup backport tool for new 7.12 branch (#91858) From 5342877a32b213ef18cef2fcf23ea4f549f30e65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 18 Feb 2021 17:31:18 +0000 Subject: [PATCH 03/43] [HTTP] Apply the same behaviour to all 500 errors (except from `custom` responses) (#85541) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...lugin-core-server.kibanaresponsefactory.md | 1 - .../core/server/kibana-plugin-core-server.md | 2 +- ...ibana-plugin-core-server.requesthandler.md | 2 +- .../http/base_path_proxy_server.test.ts | 64 ++++------ src/core/server/http/http_server.mocks.ts | 2 - src/core/server/http/http_server.test.ts | 74 ++++------- .../http/integration_tests/router.test.ts | 6 +- src/core/server/http/router/response.ts | 9 -- src/core/server/http/router/router.ts | 2 + src/core/server/server.api.md | 3 +- .../autocomplete/value_suggestions_route.ts | 14 +- .../errors/handle_es_error.ts | 2 +- .../services/sample_data/routes/install.ts | 6 +- .../vis_type_timelion/server/routes/run.ts | 62 ++++----- .../vis_type_timeseries/server/routes/vis.ts | 18 +-- .../plugins/newsfeed/server/plugin.ts | 2 +- .../plugins/core_plugin_b/server/plugin.ts | 2 +- .../server/routes/tokens/create.ts | 6 +- .../server/routes/catch_error_handler.ts | 2 +- .../server/routes/es_fields/es_fields.test.ts | 6 +- .../register_create_route.ts | 4 +- .../register_delete_route.ts | 2 +- .../register_fetch_route.ts | 2 +- .../auto_follow_pattern/register_get_route.ts | 2 +- .../register_pause_route.ts | 2 +- .../register_resume_route.ts | 2 +- .../register_update_route.ts | 2 +- .../register_permissions_route.ts | 2 +- .../register_stats_route.ts | 2 +- .../follower_index/register_create_route.ts | 2 +- .../follower_index/register_fetch_route.ts | 2 +- .../api/follower_index/register_get_route.ts | 2 +- .../follower_index/register_pause_route.ts | 2 +- .../follower_index/register_resume_route.ts | 2 +- .../follower_index/register_unfollow_route.ts | 2 +- .../follower_index/register_update_route.ts | 2 +- .../enterprise_search/telemetry.test.ts | 29 ++--- .../routes/enterprise_search/telemetry.ts | 27 ++-- x-pack/plugins/graph/server/routes/explore.ts | 6 +- .../routes/api/component_templates/create.ts | 2 +- .../routes/api/component_templates/get.ts | 4 +- .../api/component_templates/privileges.ts | 2 +- .../routes/api/component_templates/update.ts | 2 +- .../api/indices/register_clear_cache_route.ts | 2 +- .../api/indices/register_close_route.ts | 2 +- .../api/indices/register_delete_route.ts | 2 +- .../api/indices/register_flush_route.ts | 2 +- .../api/indices/register_forcemerge_route.ts | 2 +- .../api/indices/register_freeze_route.ts | 2 +- .../routes/api/indices/register_list_route.ts | 2 +- .../routes/api/indices/register_open_route.ts | 2 +- .../api/indices/register_refresh_route.ts | 2 +- .../api/indices/register_reload_route.ts | 2 +- .../api/indices/register_unfreeze_route.ts | 2 +- .../api/mapping/register_mapping_route.ts | 2 +- .../api/settings/register_load_route.ts | 2 +- .../api/settings/register_update_route.ts | 2 +- .../routes/api/stats/register_stats_route.ts | 2 +- .../api/templates/register_create_route.ts | 2 +- .../api/templates/register_get_routes.ts | 2 +- .../api/templates/register_simulate_route.ts | 2 +- .../api/templates/register_update_route.ts | 2 +- .../server/routes/inventory_metadata/index.ts | 48 +++---- .../routes/log_analysis/validation/indices.ts | 82 ++++++------ .../server/routes/log_entries/highlights.ts | 120 +++++++++--------- .../server/routes/log_entries/summary.ts | 52 ++++---- .../routes/log_entries/summary_highlights.ts | 59 ++++----- .../infra/server/routes/metadata/index.ts | 104 +++++++-------- .../infra/server/routes/metrics_api/index.ts | 24 ++-- .../server/routes/metrics_explorer/index.ts | 78 ++++++------ .../infra/server/routes/node_details/index.ts | 54 ++++---- .../infra/server/routes/overview/index.ts | 108 ++++++++-------- .../infra/server/routes/process_list/index.ts | 48 +++---- .../infra/server/routes/snapshot/index.ts | 34 ++--- .../infra/server/routes/source/index.ts | 76 +++++------ .../server/routes/api/create.ts | 2 +- .../server/routes/api/documents.ts | 2 +- .../ingest_pipelines/server/routes/api/get.ts | 4 +- .../server/routes/api/privileges.ts | 34 +++-- .../server/routes/api/simulate.ts | 2 +- .../server/routes/api/update.ts | 2 +- .../lens/server/routes/existing_fields.ts | 6 +- .../plugins/lens/server/routes/field_stats.ts | 7 +- .../plugins/lens/server/routes/telemetry.ts | 7 +- .../api/license/register_license_route.ts | 20 ++- .../api/license/register_permissions_route.ts | 10 +- .../api/license/register_start_basic_route.ts | 18 +-- .../license/register_start_trial_routes.ts | 16 +-- .../logstash/server/routes/cluster/load.ts | 2 +- x-pack/plugins/monitoring/server/plugin.ts | 2 +- .../server/routes/api/add_route.ts | 2 +- .../server/routes/api/delete_route.ts | 4 +- .../server/routes/api/get_route.test.ts | 12 +- .../server/routes/api/get_route.ts | 2 +- .../server/routes/api/update_route.ts | 2 +- .../routes/api/indices/register_get_route.ts | 2 +- .../register_validate_index_pattern_route.ts | 2 +- .../routes/api/jobs/register_create_route.ts | 2 +- .../routes/api/jobs/register_delete_route.ts | 2 +- .../routes/api/jobs/register_get_route.ts | 2 +- .../routes/api/jobs/register_start_route.ts | 2 +- .../routes/api/jobs/register_stop_route.ts | 2 +- .../api/search/register_search_route.ts | 2 +- .../authentication_service.test.ts | 13 +- .../authentication/authentication_service.ts | 8 +- .../routes/authentication/common.test.ts | 16 +-- .../server/routes/authentication/common.ts | 40 +++--- .../server/routes/authentication/saml.test.ts | 6 +- .../server/routes/authentication/saml.ts | 35 +++-- .../routes/session_management/info.test.ts | 6 +- .../server/routes/session_management/info.ts | 35 +++-- .../server/routes/views/access_agreement.ts | 21 ++- .../routes/artifacts/download_artifact.ts | 4 +- .../endpoint/routes/metadata/handlers.ts | 89 ++++++------- .../endpoint/routes/policy/handlers.test.ts | 12 +- .../server/endpoint/routes/policy/handlers.ts | 73 +++++------ .../server/endpoint/routes/policy/index.ts | 2 +- .../server/endpoint/routes/resolver.ts | 9 +- .../server/endpoint/routes/resolver/events.ts | 32 ++--- .../endpoint/routes/resolver/tree/handler.ts | 23 ++-- .../routes/trusted_apps/handlers.test.ts | 68 +++++----- .../endpoint/routes/trusted_apps/handlers.ts | 61 ++------- .../endpoint/routes/trusted_apps/index.ts | 14 +- .../security_solution/server/plugin.ts | 4 +- .../snapshot_restore/server/routes/api/app.ts | 2 +- .../server/routes/api/policy.test.ts | 21 +-- .../server/routes/api/policy.ts | 14 +- .../server/routes/api/repositories.test.ts | 16 +-- .../server/routes/api/repositories.ts | 16 +-- .../server/routes/api/restore.test.ts | 6 +- .../server/routes/api/restore.ts | 4 +- .../server/routes/api/snapshots.test.ts | 6 +- .../server/routes/api/snapshots.ts | 6 +- .../task_manager/server/routes/health.test.ts | 6 +- .../server/routes/cluster_checkup.test.ts | 12 +- .../server/routes/cluster_checkup.ts | 2 +- .../server/routes/deprecation_logging.test.ts | 24 ++-- .../server/routes/deprecation_logging.ts | 20 +-- .../routes/reindex_indices/reindex_indices.ts | 4 +- .../server/routes/telemetry.test.ts | 60 ++++----- .../server/routes/telemetry.ts | 42 +++--- .../server/rest_api/create_route_with_auth.ts | 2 +- .../server/rest_api/uptime_route_wrapper.ts | 42 +++--- .../routes/api/indices/register_get_route.ts | 2 +- .../routes/api/register_list_fields_route.ts | 2 +- .../routes/api/register_load_history_route.ts | 2 +- .../api/settings/register_load_route.ts | 2 +- .../action/register_acknowledge_route.ts | 2 +- .../api/watch/register_activate_route.ts | 2 +- .../api/watch/register_deactivate_route.ts | 2 +- .../routes/api/watch/register_delete_route.ts | 2 +- .../api/watch/register_execute_route.ts | 2 +- .../api/watch/register_history_route.ts | 2 +- .../routes/api/watch/register_load_route.ts | 2 +- .../routes/api/watch/register_save_route.ts | 4 +- .../api/watch/register_visualize_route.ts | 2 +- .../api/watches/register_delete_route.ts | 8 +- .../routes/api/watches/register_list_route.ts | 2 +- .../xpack_legacy/server/routes/settings.ts | 2 +- .../fixtures/plugins/aad/server/plugin.ts | 28 ++-- .../fixtures/plugins/alerts/server/routes.ts | 4 +- .../sample_task_plugin/server/init_routes.ts | 20 ++- 162 files changed, 1024 insertions(+), 1432 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md b/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md index d7eafdce017e43..551cbe3c937504 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md @@ -19,7 +19,6 @@ kibanaResponseFactory: { forbidden: (options?: ErrorHttpResponseOptions) => KibanaResponse; notFound: (options?: ErrorHttpResponseOptions) => KibanaResponse; conflict: (options?: ErrorHttpResponseOptions) => KibanaResponse; - internalError: (options?: ErrorHttpResponseOptions) => KibanaResponse; customError: (options: CustomHttpResponseOptions) => KibanaResponse; redirected: (options: RedirectResponseOptions) => KibanaResponse | Buffer | Stream>; ok: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 3ec63840a67cba..d14e41cfb56ecd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -283,7 +283,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginOpaqueId](./kibana-plugin-core-server.pluginopaqueid.md) | | | [PublicUiSettingsParams](./kibana-plugin-core-server.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) exposed to the client-side. | | [RedirectResponseOptions](./kibana-plugin-core-server.redirectresponseoptions.md) | HTTP response parameters for redirection response | -| [RequestHandler](./kibana-plugin-core-server.requesthandler.md) | A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) functions. | +| [RequestHandler](./kibana-plugin-core-server.requesthandler.md) | A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) functions. If anything else is returned, or an error is thrown, the HTTP service will automatically log the error and respond 500 - Internal Server Error. | | [RequestHandlerContextContainer](./kibana-plugin-core-server.requesthandlercontextcontainer.md) | An object that handles registration of http request context providers. | | [RequestHandlerContextProvider](./kibana-plugin-core-server.requesthandlercontextprovider.md) | Context provider for request handler. Extends request context object with provided functionality or data. | | [RequestHandlerWrapper](./kibana-plugin-core-server.requesthandlerwrapper.md) | Type-safe wrapper for [RequestHandler](./kibana-plugin-core-server.requesthandler.md) function. | diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandler.md b/docs/development/core/server/kibana-plugin-core-server.requesthandler.md index 0032e52a0e9060..d32ac4d80c337b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandler.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandler.md @@ -4,7 +4,7 @@ ## RequestHandler type -A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) functions. +A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) functions. If anything else is returned, or an error is thrown, the HTTP service will automatically log the error and respond `500 - Internal Server Error`. Signature: diff --git a/src/core/server/http/base_path_proxy_server.test.ts b/src/core/server/http/base_path_proxy_server.test.ts index 8f3a63058a8ae0..80c03a2af9031b 100644 --- a/src/core/server/http/base_path_proxy_server.test.ts +++ b/src/core/server/http/base_path_proxy_server.test.ts @@ -705,12 +705,8 @@ describe('BasePathProxyServer', () => { options: { body: { output: 'stream' } }, }, (_, req, res) => { - try { - expect(req.body).toBeInstanceOf(Readable); - return res.ok({ body: req.route.options.body }); - } catch (err) { - return res.internalError({ body: err.message }); - } + expect(req.body).toBeInstanceOf(Readable); + return res.ok({ body: req.route.options.body }); } ); registerRouter(router); @@ -740,15 +736,11 @@ describe('BasePathProxyServer', () => { }, }, (_, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -779,15 +771,11 @@ describe('BasePathProxyServer', () => { }, }, (context, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -815,15 +803,11 @@ describe('BasePathProxyServer', () => { }, }, (_, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -851,15 +835,11 @@ describe('BasePathProxyServer', () => { }, }, (_, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 71452cce246dfa..52dab28accb33b 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -151,7 +151,6 @@ const createResponseFactoryMock = (): jest.Mocked => ({ forbidden: jest.fn(), notFound: jest.fn(), conflict: jest.fn(), - internalError: jest.fn(), customError: jest.fn(), }); @@ -162,7 +161,6 @@ const createLifecycleResponseFactoryMock = (): jest.Mocked { options: { body: { parse: false } }, }, (context, req, res) => { - try { - expect(req.body).toBeInstanceOf(Buffer); - expect(req.body.toString()).toBe(JSON.stringify({ test: 1 })); - return res.ok({ body: req.route.options.body }); - } catch (err) { - return res.internalError({ body: err.message }); - } + expect(req.body).toBeInstanceOf(Buffer); + expect(req.body.toString()).toBe(JSON.stringify({ test: 1 })); + return res.ok({ body: req.route.options.body }); } ); registerRouter(router); @@ -1053,15 +1049,11 @@ describe('timeout options', () => { }, }, (context, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -1091,15 +1083,11 @@ describe('timeout options', () => { }, }, (context, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -1128,15 +1116,11 @@ describe('timeout options', () => { }, }, (context, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -1165,15 +1149,11 @@ describe('timeout options', () => { }, }, (context, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -1294,12 +1274,8 @@ test('should return a stream in the body', async () => { options: { body: { output: 'stream' } }, }, (context, req, res) => { - try { - expect(req.body).toBeInstanceOf(Readable); - return res.ok({ body: req.route.options.body }); - } catch (err) { - return res.internalError({ body: err.message }); - } + expect(req.body).toBeInstanceOf(Readable); + return res.ok({ body: req.route.options.body }); } ); registerRouter(router); diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index c9c4410171b34a..6c54067435405a 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -1672,7 +1672,11 @@ describe('Response factory', () => { const result = await supertest(innerServer.listener).get('/').expect(500); - expect(result.body.message).toBe('reason'); + expect(result.body).toEqual({ + error: 'Internal Server Error', + message: 'reason', + statusCode: 500, + }); expect(loggingSystemMock.collect(logger).error).toHaveLength(0); }); diff --git a/src/core/server/http/router/response.ts b/src/core/server/http/router/response.ts index f4e09fa1cc08ea..e2babf719f67e0 100644 --- a/src/core/server/http/router/response.ts +++ b/src/core/server/http/router/response.ts @@ -177,15 +177,6 @@ const errorResponseFactory = { conflict: (options: ErrorHttpResponseOptions = {}) => new KibanaResponse(409, options.body || 'Conflict', options), - // Server error - /** - * The server encountered an unexpected condition that prevented it from fulfilling the request. - * Status code: `500`. - * @param options - {@link HttpResponseOptions} configures HTTP response headers, error message and other error details to pass to the client - */ - internalError: (options: ErrorHttpResponseOptions = {}) => - new KibanaResponse(500, options.body || 'Internal Error', options), - /** * Creates an error response with defined status code and payload. * @param options - {@link CustomHttpResponseOptions} configures HTTP response headers, error message and other error details to pass to the client diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index acce209751e325..85eab7c0892e88 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -314,6 +314,8 @@ type RequestHandlerEnhanced = WithoutHeadAr /** * A function executed when route path matched requested resource path. * Request handler is expected to return a result of one of {@link KibanaResponseFactory} functions. + * If anything else is returned, or an error is thrown, the HTTP service will automatically log the error + * and respond `500 - Internal Server Error`. * @param context {@link RequestHandlerContext} - the core context exposed for this request. * @param request {@link KibanaRequest} - object containing information about requested resource, * such as path, method, headers, parameters, query, body, etc. diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 377cd2bc2068a9..2177da84b2b53d 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1276,7 +1276,6 @@ export const kibanaResponseFactory: { forbidden: (options?: ErrorHttpResponseOptions) => KibanaResponse; notFound: (options?: ErrorHttpResponseOptions) => KibanaResponse; conflict: (options?: ErrorHttpResponseOptions) => KibanaResponse; - internalError: (options?: ErrorHttpResponseOptions) => KibanaResponse; customError: (options: CustomHttpResponseOptions) => KibanaResponse; redirected: (options: RedirectResponseOptions) => KibanaResponse | Buffer | Stream>; ok: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; @@ -3197,7 +3196,7 @@ export const validBodyOutput: readonly ["data", "stream"]; // Warnings were encountered during analysis: // -// src/core/server/http/router/response.ts:306:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts +// src/core/server/http/router/response.ts:297:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:283:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/server/autocomplete/value_suggestions_route.ts b/src/plugins/data/server/autocomplete/value_suggestions_route.ts index 8a633d8d827f4e..489a23eb83897c 100644 --- a/src/plugins/data/server/autocomplete/value_suggestions_route.ts +++ b/src/plugins/data/server/autocomplete/value_suggestions_route.ts @@ -57,17 +57,13 @@ export function registerValueSuggestionsRoute( const field = indexPattern && getFieldByName(fieldName, indexPattern); const body = await getBody(autocompleteSearchOptions, field || fieldName, query, filters); - try { - const result = await client.callAsCurrentUser('search', { index, body }, { signal }); + const result = await client.callAsCurrentUser('search', { index, body }, { signal }); - const buckets: any[] = - get(result, 'aggregations.suggestions.buckets') || - get(result, 'aggregations.nestedSuggestions.suggestions.buckets'); + const buckets: any[] = + get(result, 'aggregations.suggestions.buckets') || + get(result, 'aggregations.nestedSuggestions.suggestions.buckets'); - return response.ok({ body: map(buckets || [], 'key') }); - } catch (error) { - return response.internalError({ body: error }); - } + return response.ok({ body: map(buckets || [], 'key') }); } ); } diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts index b645b62c863d5d..4a45cff0b96049 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts @@ -38,5 +38,5 @@ export const handleEsError = ({ }); } // Case: default - return response.internalError({ body: error }); + throw error; }; diff --git a/src/plugins/home/server/services/sample_data/routes/install.ts b/src/plugins/home/server/services/sample_data/routes/install.ts index 7c00a46602e26e..a20c3e350222f3 100644 --- a/src/plugins/home/server/services/sample_data/routes/install.ts +++ b/src/plugins/home/server/services/sample_data/routes/install.ts @@ -136,8 +136,7 @@ export function createInstallRoute( (counts as any)[index] = count; } catch (err) { const errMsg = `sample_data install errors while loading data. Error: ${err}`; - logger.warn(errMsg); - return res.internalError({ body: errMsg }); + throw new Error(errMsg); } } @@ -157,8 +156,7 @@ export function createInstallRoute( ); } catch (err) { const errMsg = `bulkCreate failed, error: ${err.message}`; - logger.warn(errMsg); - return res.internalError({ body: errMsg }); + throw new Error(errMsg); } const errors = createResults.saved_objects.filter((savedObjectCreateResult) => { return Boolean(savedObjectCreateResult.error); diff --git a/src/plugins/vis_type_timelion/server/routes/run.ts b/src/plugins/vis_type_timelion/server/routes/run.ts index bae25d6f918e37..b3ab3c61c15d89 100644 --- a/src/plugins/vis_type_timelion/server/routes/run.ts +++ b/src/plugins/vis_type_timelion/server/routes/run.ts @@ -77,46 +77,32 @@ export function runRoute( }, }, router.handleLegacyErrors(async (context, request, response) => { - try { - const [, { data }] = await core.getStartServices(); - const uiSettings = await context.core.uiSettings.client.getAll(); - const indexPatternsService = await data.indexPatterns.indexPatternsServiceFactory( - context.core.savedObjects.client, - context.core.elasticsearch.client.asCurrentUser - ); + const [, { data }] = await core.getStartServices(); + const uiSettings = await context.core.uiSettings.client.getAll(); + const indexPatternsService = await data.indexPatterns.indexPatternsServiceFactory( + context.core.savedObjects.client, + context.core.elasticsearch.client.asCurrentUser + ); - const tlConfig = getTlConfig({ - context, - request, - settings: _.defaults(uiSettings, timelionDefaults), // Just in case they delete some setting. - getFunction, - getIndexPatternsService: () => indexPatternsService, - getStartServices: core.getStartServices, - allowedGraphiteUrls: configManager.getGraphiteUrls(), - esShardTimeout: configManager.getEsShardTimeout(), - }); - const chainRunner = chainRunnerFn(tlConfig); - const sheet = await Bluebird.all(chainRunner.processRequest(request.body)); + const tlConfig = getTlConfig({ + context, + request, + settings: _.defaults(uiSettings, timelionDefaults), // Just in case they delete some setting. + getFunction, + getIndexPatternsService: () => indexPatternsService, + getStartServices: core.getStartServices, + allowedGraphiteUrls: configManager.getGraphiteUrls(), + esShardTimeout: configManager.getEsShardTimeout(), + }); + const chainRunner = chainRunnerFn(tlConfig); + const sheet = await Bluebird.all(chainRunner.processRequest(request.body)); - return response.ok({ - body: { - sheet, - stats: chainRunner.getStats(), - }, - }); - } catch (err) { - logger.error(`${err.toString()}: ${err.stack}`); - // TODO Maybe we should just replace everywhere we throw with Boom? Probably. - if (err.isBoom) { - throw err; - } else { - return response.internalError({ - body: { - message: err.toString(), - }, - }); - } - } + return response.ok({ + body: { + sheet, + stats: chainRunner.getStats(), + }, + }); }) ); } diff --git a/src/plugins/vis_type_timeseries/server/routes/vis.ts b/src/plugins/vis_type_timeseries/server/routes/vis.ts index 7c314228dad246..15890011d75bb9 100644 --- a/src/plugins/vis_type_timeseries/server/routes/vis.ts +++ b/src/plugins/vis_type_timeseries/server/routes/vis.ts @@ -42,18 +42,12 @@ export const visDataRoutes = (router: VisTypeTimeseriesRouter, framework: Framew ); } - try { - const results = await getVisData( - requestContext, - request as KibanaRequest<{}, {}, GetVisDataOptions>, - framework - ); - return response.ok({ body: results }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + const results = await getVisData( + requestContext, + request as KibanaRequest<{}, {}, GetVisDataOptions>, + framework + ); + return response.ok({ body: results }); } ); }; diff --git a/test/common/fixtures/plugins/newsfeed/server/plugin.ts b/test/common/fixtures/plugins/newsfeed/server/plugin.ts index 732bc6c4732436..49ffa464efac92 100644 --- a/test/common/fixtures/plugins/newsfeed/server/plugin.ts +++ b/test/common/fixtures/plugins/newsfeed/server/plugin.ts @@ -34,7 +34,7 @@ export class NewsFeedSimulatorPlugin implements Plugin { options: { authRequired: false }, }, (context, req, res) => { - return res.internalError({ body: new Error('Internal server error') }); + throw new Error('Internal server error'); } ); } diff --git a/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts b/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts index 47644256b05e7c..ae7abdaebba101 100644 --- a/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts +++ b/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts @@ -18,7 +18,7 @@ export class CorePluginBPlugin implements Plugin { public setup(core: CoreSetup, deps: {}) { const router = core.http.createRouter(); router.get({ path: '/core_plugin_b', validate: false }, async (context, req, res) => { - if (!context.pluginA) return res.internalError({ body: 'pluginA is disabled' }); + if (!context.pluginA) throw new Error('pluginA is disabled'); const response = await context.pluginA.ping(); return res.ok({ body: `Pong via plugin A: ${response}` }); }); diff --git a/x-pack/plugins/beats_management/server/routes/tokens/create.ts b/x-pack/plugins/beats_management/server/routes/tokens/create.ts index d61e96900e04b8..c44f9c2dd4e7dc 100644 --- a/x-pack/plugins/beats_management/server/routes/tokens/create.ts +++ b/x-pack/plugins/beats_management/server/routes/tokens/create.ts @@ -50,11 +50,7 @@ export const registerCreateTokenRoute = (router: BeatsManagementRouter) => { }); } catch (err) { beatsManagement.framework.log(err.message); - return response.internalError({ - body: { - message: 'An error occurred, please check your Kibana logs', - }, - }); + throw new Error('An error occurred, please check your Kibana logs'); } } ) diff --git a/x-pack/plugins/canvas/server/routes/catch_error_handler.ts b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts index 2abdda1932535a..b1fe4bc798f6bc 100644 --- a/x-pack/plugins/canvas/server/routes/catch_error_handler.ts +++ b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts @@ -20,7 +20,7 @@ export const catchErrorHandler: ( statusCode: error.output.statusCode, }); } - return response.internalError({ body: error }); + throw error; } }; }; diff --git a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts index 618625cd6cdd43..1e95ee809e7679 100644 --- a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts +++ b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts @@ -147,8 +147,8 @@ describe('Retrieve ES Fields', () => { callAsCurrentUserMock.mockRejectedValueOnce(new Error('Index not found')); - const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); - - expect(response.status).toBe(500); + await expect( + routeHandler(mockRouteContext, request, kibanaResponseFactory) + ).rejects.toThrowError('Index not found'); }); }); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts index 10cb83877f57f3..608e369828de6f 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts @@ -55,7 +55,7 @@ export const registerCreateRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } } @@ -71,7 +71,7 @@ export const registerCreateRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts index bc18d5c4b10f81..868e847bd6bf48 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts @@ -41,7 +41,7 @@ export const registerDeleteRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + return response.customError({ statusCode: 500, body: err }); }; await Promise.all( diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts index 0e6ebc5270986f..632fdb03dd5887 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts @@ -37,7 +37,7 @@ export const registerFetchRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts index cd1cb227a3eba7..3529fe313dbb16 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts @@ -48,7 +48,7 @@ export const registerGetRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts index 3bdcd4824a9d45..a9aa94cdf4f299 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts @@ -40,7 +40,7 @@ export const registerPauseRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + return response.customError({ statusCode: 500, body: err }); }; await Promise.all( diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts index 594ef79eaedc96..1c6396d0b35022 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts @@ -40,7 +40,7 @@ export const registerResumeRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + return response.customError({ statusCode: 500, body: err }); }; await Promise.all( diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts index e4d0b7d9d65149..a3e7c3544ca370 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts @@ -54,7 +54,7 @@ export const registerUpdateRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts index ac956a45b87026..130adb2e0b9892 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts @@ -64,7 +64,7 @@ export const registerPermissionsRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts index f37dafca86c985..6636f6b1c5accc 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts @@ -36,7 +36,7 @@ export const registerStatsRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts index b254606af8a867..f44e5a749baad7 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts @@ -59,7 +59,7 @@ export const registerCreateRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts index ffb560ebd5f2bf..c72706cf5d10dc 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts @@ -56,7 +56,7 @@ export const registerFetchRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts index 36feb2e69a3f5c..cdcfff97e645db 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts @@ -72,7 +72,7 @@ export const registerGetRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts index dd76114d30215a..2e4e71278df0e9 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts @@ -38,7 +38,7 @@ export const registerPauseRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + return response.customError({ statusCode: 500, body: err }); }; await Promise.all( diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts index 22206c70fdbbf8..34f204f3b64b96 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts @@ -38,7 +38,7 @@ export const registerResumeRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + return response.customError({ statusCode: 500, body: err }); }; await Promise.all( diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts index 05bf99e2d8c67f..848408e14662fd 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts @@ -39,7 +39,7 @@ export const registerUnfollowRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + return response.customError({ statusCode: 500, body: err }); }; await Promise.all( diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts index 0b61db42cc7635..933d13a0a223c0 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts @@ -87,7 +87,7 @@ export const registerUpdateRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts index 62f68748fcea19..53ddc21cba3994 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts @@ -7,7 +7,7 @@ import { MockRouter, mockLogger, mockDependencies } from '../../__mocks__'; -import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; +import { savedObjectsServiceMock } from 'src/core/server/mocks'; jest.mock('../../collectors/lib/telemetry', () => ({ incrementUICounter: jest.fn(), @@ -84,17 +84,17 @@ describe('Enterprise Search Telemetry API', () => { it('throws an error when incrementing fails', async () => { (incrementUICounter as jest.Mock).mockImplementation(jest.fn(() => Promise.reject('Failed'))); - await mockRouter.callRoute({ - body: { - product: 'enterprise_search', - action: 'error', - metric: 'error', - }, - }); + await expect( + mockRouter.callRoute({ + body: { + product: 'enterprise_search', + action: 'error', + metric: 'error', + }, + }) + ).rejects.toEqual('Failed'); expect(incrementUICounter).toHaveBeenCalled(); - expect(mockLogger.error).toHaveBeenCalled(); - expect(mockRouter.response.internalError).toHaveBeenCalled(); }); it('throws an error if the Saved Objects service is unavailable', async () => { @@ -104,16 +104,9 @@ describe('Enterprise Search Telemetry API', () => { getSavedObjectsService: null, log: mockLogger, } as any); - await mockRouter.callRoute({}); + await expect(mockRouter.callRoute({})).rejects.toThrow(); expect(incrementUICounter).not.toHaveBeenCalled(); - expect(mockLogger.error).toHaveBeenCalled(); - expect(mockRouter.response.internalError).toHaveBeenCalled(); - expect(loggingSystemMock.collect(mockLogger).error[0][0]).toEqual( - expect.stringContaining( - 'Enterprise Search UI telemetry error: Error: Could not find Saved Objects service' - ) - ); }); describe('validates', () => { diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts index 90afba414c0447..e15be8fcd0d8b8 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts @@ -20,7 +20,7 @@ const productToTelemetryMap = { workplace_search: WS_TELEMETRY_NAME, }; -export function registerTelemetryRoute({ router, getSavedObjectsService, log }: RouteDependencies) { +export function registerTelemetryRoute({ router, getSavedObjectsService }: RouteDependencies) { router.put( { path: '/api/enterprise_search/stats', @@ -43,23 +43,16 @@ export function registerTelemetryRoute({ router, getSavedObjectsService, log }: async (ctx, request, response) => { const { product, action, metric } = request.body; - try { - if (!getSavedObjectsService) throw new Error('Could not find Saved Objects service'); + if (!getSavedObjectsService) throw new Error('Could not find Saved Objects service'); - return response.ok({ - body: await incrementUICounter({ - id: productToTelemetryMap[product], - savedObjects: getSavedObjectsService(), - uiAction: `ui_${action}`, - metric, - }), - }); - } catch (e) { - log.error( - `Enterprise Search UI telemetry error: ${e instanceof Error ? e.stack : e.toString()}` - ); - return response.internalError({ body: 'Enterprise Search UI telemetry failed' }); - } + return response.ok({ + body: await incrementUICounter({ + id: productToTelemetryMap[product], + savedObjects: getSavedObjectsService(), + uiAction: `ui_${action}`, + metric, + }), + }); } ); } diff --git a/x-pack/plugins/graph/server/routes/explore.ts b/x-pack/plugins/graph/server/routes/explore.ts index 95be71812d06c8..9a9a267c40f32a 100644 --- a/x-pack/plugins/graph/server/routes/explore.ts +++ b/x-pack/plugins/graph/server/routes/explore.ts @@ -76,11 +76,7 @@ export function registerExploreRoute({ } } - return response.internalError({ - body: { - message: error.message, - }, - }); + throw error; } } ) diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts index d4d16933c518ce..a6c0592e035e79 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts @@ -71,7 +71,7 @@ export const registerCreateRoute = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts index 28773b6233b552..552aa5a9a2888c 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts @@ -54,7 +54,7 @@ export function registerGetAllRoute({ router, license, lib: { isEsError } }: Rou }); } - return res.internalError({ body: error }); + throw error; } }) ); @@ -94,7 +94,7 @@ export function registerGetAllRoute({ router, license, lib: { isEsError } }: Rou }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts index b7957737d3aae8..1ed6555eb38067 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts @@ -64,7 +64,7 @@ export const registerPrivilegesRoute = ({ license, router, config }: RouteDepend return res.ok({ body: privilegesResult }); } catch (e) { - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts index b0113e8566ae2e..42b53ab6ee25b3 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts @@ -55,7 +55,7 @@ export const registerUpdateRoute = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts index cbb7add344a60e..2f5da4b1d8957a 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts @@ -38,7 +38,7 @@ export function registerClearCacheRoute({ router, license, lib }: RouteDependenc }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts index 5a0d692c743185..1a0babfc3a5b1f 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts @@ -38,7 +38,7 @@ export function registerCloseRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts index dfe571f352296f..9a022d4595d1c0 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts @@ -38,7 +38,7 @@ export function registerDeleteRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts index 8faba8a5d54cd0..b064f3520004a0 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts @@ -38,7 +38,7 @@ export function registerFlushRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts index d8a777196e7ef5..1c14f660b98c67 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts @@ -45,7 +45,7 @@ export function registerForcemergeRoute({ router, license, lib }: RouteDependenc }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts index 4d18650d928265..b669d78f2ba597 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts @@ -40,7 +40,7 @@ export function registerFreezeRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts index b940253635ad0e..0b253b9fe66c97 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts @@ -27,7 +27,7 @@ export function registerListRoute({ router, license, indexDataEnricher, lib }: R }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts index 322eac45d7bd1f..a35ddfcf4d91b2 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts @@ -38,7 +38,7 @@ export function registerOpenRoute({ router, license, lib }: RouteDependencies) { }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts index a070208d30d4cd..f69d2d90a5b8fc 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts @@ -38,7 +38,7 @@ export function registerRefreshRoute({ router, license, lib }: RouteDependencies }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts index b6b8d741c1202c..04b7d760fc1d62 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts @@ -43,7 +43,7 @@ export function registerReloadRoute({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts index 55281951790331..3cda4d6b5f1682 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts @@ -35,7 +35,7 @@ export function registerUnfreezeRoute({ router, license, lib }: RouteDependencie }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts b/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts index 5cc0c92969ab09..f0b62bacdee426 100644 --- a/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts @@ -46,7 +46,7 @@ export function registerMappingRoute({ router, license, lib }: RouteDependencies }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts b/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts index 6d355ced5e9939..7a661a9e9e4f49 100644 --- a/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts @@ -48,7 +48,7 @@ export function registerLoadRoute({ router, license, lib }: RouteDependencies) { }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts b/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts index 1216a9c74e48c4..4c153d6293a79f 100644 --- a/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts @@ -46,7 +46,7 @@ export function registerUpdateRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts b/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts index 45bf114805a787..f8385711b55fe2 100644 --- a/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts @@ -47,7 +47,7 @@ export function registerStatsRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts index 81286b69f2ded8..97e3c380e13ecd 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts @@ -62,7 +62,7 @@ export function registerCreateRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts index 88aa8d3a793501..006532cfd4dbe1 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts @@ -104,7 +104,7 @@ export function registerGetOneRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts index d24837fa01cd50..f4554bd2fb1fa6 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts @@ -40,7 +40,7 @@ export function registerSimulateRoute({ router, license, lib }: RouteDependencie }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts index bf88d63572a823..f0070408768cbf 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts @@ -56,7 +56,7 @@ export function registerUpdateRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts b/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts index 7147f224d09d57..70d29773f76c5d 100644 --- a/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts @@ -33,33 +33,27 @@ export const initInventoryMetaRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const { sourceId, nodeType, currentTime } = pipe( - InventoryMetaRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - - const { configuration } = await libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); - - const awsMetadata = await getCloudMetadata( - framework, - requestContext, - configuration, - nodeType, - currentTime - ); - - return response.ok({ - body: InventoryMetaResponseRT.encode(awsMetadata), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + const { sourceId, nodeType, currentTime } = pipe( + InventoryMetaRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + + const { configuration } = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); + + const awsMetadata = await getCloudMetadata( + framework, + requestContext, + configuration, + nodeType, + currentTime + ); + + return response.ok({ + body: InventoryMetaResponseRT.encode(awsMetadata), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/log_analysis/validation/indices.ts b/x-pack/plugins/infra/server/routes/log_analysis/validation/indices.ts index c49d6034c95c41..463ac77891263c 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/validation/indices.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/validation/indices.ts @@ -31,65 +31,59 @@ export const initValidateLogAnalysisIndicesRoute = ({ framework }: InfraBackendL validate: { body: escapeHatch }, }, async (requestContext, request, response) => { - try { - const payload = pipe( - validationIndicesRequestPayloadRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const payload = pipe( + validationIndicesRequestPayloadRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const { fields, indices } = payload.data; - const errors: ValidationIndicesError[] = []; + const { fields, 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(requestContext, 'fieldCaps', { - allow_no_indices: true, - fields: fields.map((field) => field.name), - ignore_unavailable: true, + // Query each pattern individually, to map correctly the errors + await Promise.all( + indices.map(async (index) => { + const fieldCaps = await framework.callWithRequest(requestContext, 'fieldCaps', { + allow_no_indices: true, + fields: fields.map((field) => field.name), + ignore_unavailable: true, + index, + }); + + if (fieldCaps.indices.length === 0) { + errors.push({ + error: 'INDEX_NOT_FOUND', index, }); + return; + } + + fields.forEach(({ name: fieldName, validTypes }) => { + const fieldMetadata = fieldCaps.fields[fieldName]; - if (fieldCaps.indices.length === 0) { + if (fieldMetadata === undefined) { errors.push({ - error: 'INDEX_NOT_FOUND', + error: 'FIELD_NOT_FOUND', index, + field: fieldName, }); - return; - } - - fields.forEach(({ name: fieldName, validTypes }) => { - const fieldMetadata = fieldCaps.fields[fieldName]; + } else { + const fieldTypes = Object.keys(fieldMetadata); - if (fieldMetadata === undefined) { + if (!fieldTypes.every((fieldType) => validTypes.includes(fieldType))) { errors.push({ - error: 'FIELD_NOT_FOUND', + error: `FIELD_NOT_VALID`, index, field: fieldName, }); - } else { - const fieldTypes = Object.keys(fieldMetadata); - - if (!fieldTypes.every((fieldType) => validTypes.includes(fieldType))) { - errors.push({ - error: `FIELD_NOT_VALID`, - index, - field: fieldName, - }); - } } - }); - }) - ); + } + }); + }) + ); - return response.ok({ - body: validationIndicesResponsePayloadRT.encode({ data: { errors } }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: validationIndicesResponsePayloadRT.encode({ data: { errors } }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/log_entries/highlights.ts b/x-pack/plugins/infra/server/routes/log_entries/highlights.ts index c72590ca01a5ef..bb7c615358c0e1 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/highlights.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/highlights.ts @@ -33,75 +33,69 @@ export const initLogEntriesHighlightsRoute = ({ framework, logEntries }: InfraBa validate: { body: escapeHatch }, }, async (requestContext, request, response) => { - try { - const payload = pipe( - logEntriesHighlightsRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - - const { startTimestamp, endTimestamp, sourceId, query, size, highlightTerms } = payload; + const payload = pipe( + logEntriesHighlightsRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - let entriesPerHighlightTerm; + const { startTimestamp, endTimestamp, sourceId, query, size, highlightTerms } = payload; - if ('center' in payload) { - entriesPerHighlightTerm = await Promise.all( - highlightTerms.map((highlightTerm) => - logEntries.getLogEntriesAround(requestContext, sourceId, { - startTimestamp, - endTimestamp, - query: parseFilterQuery(query), - center: payload.center, - size, - highlightTerm, - }) - ) - ); - } else { - let cursor: LogEntriesParams['cursor']; - if ('before' in payload) { - cursor = { before: payload.before }; - } else if ('after' in payload) { - cursor = { after: payload.after }; - } + let entriesPerHighlightTerm; - entriesPerHighlightTerm = await Promise.all( - highlightTerms.map((highlightTerm) => - logEntries.getLogEntries(requestContext, sourceId, { - startTimestamp, - endTimestamp, - query: parseFilterQuery(query), - cursor, - size, - highlightTerm, - }) - ) - ); + if ('center' in payload) { + entriesPerHighlightTerm = await Promise.all( + highlightTerms.map((highlightTerm) => + logEntries.getLogEntriesAround(requestContext, sourceId, { + startTimestamp, + endTimestamp, + query: parseFilterQuery(query), + center: payload.center, + size, + highlightTerm, + }) + ) + ); + } else { + let cursor: LogEntriesParams['cursor']; + if ('before' in payload) { + cursor = { before: payload.before }; + } else if ('after' in payload) { + cursor = { after: payload.after }; } - return response.ok({ - body: logEntriesHighlightsResponseRT.encode({ - data: entriesPerHighlightTerm.map(({ entries }) => { - if (entries.length > 0) { - return { - entries, - topCursor: entries[0].cursor, - bottomCursor: entries[entries.length - 1].cursor, - }; - } else { - return { - entries, - topCursor: null, - bottomCursor: null, - }; - } - }), - }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); + entriesPerHighlightTerm = await Promise.all( + highlightTerms.map((highlightTerm) => + logEntries.getLogEntries(requestContext, sourceId, { + startTimestamp, + endTimestamp, + query: parseFilterQuery(query), + cursor, + size, + highlightTerm, + }) + ) + ); } + + return response.ok({ + body: logEntriesHighlightsResponseRT.encode({ + data: entriesPerHighlightTerm.map(({ entries }) => { + if (entries.length > 0) { + return { + entries, + topCursor: entries[0].cursor, + bottomCursor: entries[entries.length - 1].cursor, + }; + } else { + return { + entries, + topCursor: null, + bottomCursor: null, + }; + } + }), + }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/log_entries/summary.ts b/x-pack/plugins/infra/server/routes/log_entries/summary.ts index 4849b56b1d579d..3ff0ded8a7c244 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/summary.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/summary.ts @@ -33,38 +33,32 @@ export const initLogEntriesSummaryRoute = ({ framework, logEntries }: InfraBacke validate: { body: escapeHatch }, }, async (requestContext, request, response) => { - try { - const payload = pipe( - logEntriesSummaryRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - const { sourceId, startTimestamp, endTimestamp, bucketSize, query } = payload; + const payload = pipe( + logEntriesSummaryRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + const { sourceId, startTimestamp, endTimestamp, bucketSize, query } = payload; - const buckets = await logEntries.getLogSummaryBucketsBetween( - requestContext, - sourceId, - startTimestamp, - endTimestamp, - bucketSize, - parseFilterQuery(query) - ); + const buckets = await logEntries.getLogSummaryBucketsBetween( + requestContext, + sourceId, + startTimestamp, + endTimestamp, + bucketSize, + parseFilterQuery(query) + ); - UsageCollector.countLogs(); + UsageCollector.countLogs(); - return response.ok({ - body: logEntriesSummaryResponseRT.encode({ - data: { - start: startTimestamp, - end: endTimestamp, - buckets, - }, - }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: logEntriesSummaryResponseRT.encode({ + data: { + start: startTimestamp, + end: endTimestamp, + buckets, + }, + }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/log_entries/summary_highlights.ts b/x-pack/plugins/infra/server/routes/log_entries/summary_highlights.ts index 62a9d15c4e68bc..ca219cac41e2bf 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/summary_highlights.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/summary_highlights.ts @@ -35,44 +35,31 @@ export const initLogEntriesSummaryHighlightsRoute = ({ validate: { body: escapeHatch }, }, async (requestContext, request, response) => { - try { - const payload = pipe( - logEntriesSummaryHighlightsRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - const { - sourceId, - startTimestamp, - endTimestamp, - bucketSize, - query, - highlightTerms, - } = payload; + const payload = pipe( + logEntriesSummaryHighlightsRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + const { sourceId, startTimestamp, endTimestamp, bucketSize, query, highlightTerms } = payload; - const bucketsPerHighlightTerm = await logEntries.getLogSummaryHighlightBucketsBetween( - requestContext, - sourceId, - startTimestamp, - endTimestamp, - bucketSize, - highlightTerms, - parseFilterQuery(query) - ); + const bucketsPerHighlightTerm = await logEntries.getLogSummaryHighlightBucketsBetween( + requestContext, + sourceId, + startTimestamp, + endTimestamp, + bucketSize, + highlightTerms, + parseFilterQuery(query) + ); - return response.ok({ - body: logEntriesSummaryHighlightsResponseRT.encode({ - data: bucketsPerHighlightTerm.map((buckets) => ({ - start: startTimestamp, - end: endTimestamp, - buckets, - })), - }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: logEntriesSummaryHighlightsResponseRT.encode({ + data: bucketsPerHighlightTerm.map((buckets) => ({ + start: startTimestamp, + end: endTimestamp, + buckets, + })), + }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/metadata/index.ts b/x-pack/plugins/infra/server/routes/metadata/index.ts index b2abe1c35a3ff1..cc8888e9bd09d3 100644 --- a/x-pack/plugins/infra/server/routes/metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/metadata/index.ts @@ -37,65 +37,57 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const { nodeId, nodeType, sourceId, timeRange } = pipe( - InfraMetadataRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const { nodeId, nodeType, sourceId, timeRange } = pipe( + InfraMetadataRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const { configuration } = await libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); - const metricsMetadata = await getMetricMetadata( - framework, - requestContext, - configuration, - nodeId, - nodeType, - timeRange - ); - const metricFeatures = pickFeatureName(metricsMetadata.buckets).map( - nameToFeature('metrics') - ); + const { configuration } = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); + const metricsMetadata = await getMetricMetadata( + framework, + requestContext, + configuration, + nodeId, + nodeType, + timeRange + ); + const metricFeatures = pickFeatureName(metricsMetadata.buckets).map(nameToFeature('metrics')); - const info = await getNodeInfo( - framework, - requestContext, - configuration, - nodeId, - nodeType, - timeRange - ); - const cloudInstanceId = get(info, 'cloud.instance.id'); + const info = await getNodeInfo( + framework, + requestContext, + configuration, + nodeId, + nodeType, + timeRange + ); + const cloudInstanceId = get(info, 'cloud.instance.id'); - const cloudMetricsMetadata = cloudInstanceId - ? await getCloudMetricsMetadata( - framework, - requestContext, - configuration, - cloudInstanceId, - timeRange - ) - : { buckets: [] }; - const cloudMetricsFeatures = pickFeatureName(cloudMetricsMetadata.buckets).map( - nameToFeature('metrics') - ); - const id = metricsMetadata.id; - const name = metricsMetadata.name || id; - return response.ok({ - body: InfraMetadataRT.encode({ - id, - name, - features: [...metricFeatures, ...cloudMetricsFeatures], - info, - }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + const cloudMetricsMetadata = cloudInstanceId + ? await getCloudMetricsMetadata( + framework, + requestContext, + configuration, + cloudInstanceId, + timeRange + ) + : { buckets: [] }; + const cloudMetricsFeatures = pickFeatureName(cloudMetricsMetadata.buckets).map( + nameToFeature('metrics') + ); + const id = metricsMetadata.id; + const name = metricsMetadata.name || id; + return response.ok({ + body: InfraMetadataRT.encode({ + id, + name, + features: [...metricFeatures, ...cloudMetricsFeatures], + info, + }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/metrics_api/index.ts b/x-pack/plugins/infra/server/routes/metrics_api/index.ts index 7d616f5b9dfeae..5c0569d6e7a94f 100644 --- a/x-pack/plugins/infra/server/routes/metrics_api/index.ts +++ b/x-pack/plugins/infra/server/routes/metrics_api/index.ts @@ -29,23 +29,17 @@ export const initMetricsAPIRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const options = pipe( - MetricsAPIRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const options = pipe( + MetricsAPIRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = createSearchClient(requestContext, framework); - const metricsApiResponse = await query(client, options); + const client = createSearchClient(requestContext, framework); + const metricsApiResponse = await query(client, options); - return response.ok({ - body: MetricsAPIResponseRT.encode(metricsApiResponse), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: MetricsAPIResponseRT.encode(metricsApiResponse), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts index b8a48df43bc10f..d61dcfad974947 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts @@ -37,55 +37,49 @@ export const initMetricExplorerRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const options = pipe( - metricsExplorerRequestBodyRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const options = pipe( + metricsExplorerRequestBodyRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = createSearchClient(requestContext, framework); - const interval = await findIntervalForMetrics(client, options); + const client = createSearchClient(requestContext, framework); + const interval = await findIntervalForMetrics(client, options); - const optionsWithInterval = options.forceInterval - ? options - : { - ...options, - timerange: { - ...options.timerange, - interval: interval ? `>=${interval}s` : options.timerange.interval, - }, - }; + const optionsWithInterval = options.forceInterval + ? options + : { + ...options, + timerange: { + ...options.timerange, + interval: interval ? `>=${interval}s` : options.timerange.interval, + }, + }; - const metricsApiOptions = convertRequestToMetricsAPIOptions(optionsWithInterval); - const metricsApiResponse = await query(client, metricsApiOptions); - const totalGroupings = await queryTotalGroupings(client, metricsApiOptions); - const hasGroupBy = - Array.isArray(metricsApiOptions.groupBy) && metricsApiOptions.groupBy.length > 0; + const metricsApiOptions = convertRequestToMetricsAPIOptions(optionsWithInterval); + const metricsApiResponse = await query(client, metricsApiOptions); + const totalGroupings = await queryTotalGroupings(client, metricsApiOptions); + const hasGroupBy = + Array.isArray(metricsApiOptions.groupBy) && metricsApiOptions.groupBy.length > 0; - const pageInfo: MetricsExplorerPageInfo = { - total: totalGroupings, - afterKey: null, - }; + const pageInfo: MetricsExplorerPageInfo = { + total: totalGroupings, + afterKey: null, + }; - if (metricsApiResponse.info.afterKey) { - pageInfo.afterKey = metricsApiResponse.info.afterKey; - } + if (metricsApiResponse.info.afterKey) { + pageInfo.afterKey = metricsApiResponse.info.afterKey; + } - // If we have a groupBy but there are ZERO groupings returned then we need to - // return an empty array. Otherwise we transform the series to match the current schema. - const series = - hasGroupBy && totalGroupings === 0 - ? [] - : metricsApiResponse.series.map(transformSeries(hasGroupBy)); + // If we have a groupBy but there are ZERO groupings returned then we need to + // return an empty array. Otherwise we transform the series to match the current schema. + const series = + hasGroupBy && totalGroupings === 0 + ? [] + : metricsApiResponse.series.map(transformSeries(hasGroupBy)); - return response.ok({ - body: metricsExplorerResponseRT.encode({ series, pageInfo }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: metricsExplorerResponseRT.encode({ series, pageInfo }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/node_details/index.ts b/x-pack/plugins/infra/server/routes/node_details/index.ts index d407a9f65f983e..8e305226112bd3 100644 --- a/x-pack/plugins/infra/server/routes/node_details/index.ts +++ b/x-pack/plugins/infra/server/routes/node_details/index.ts @@ -34,38 +34,32 @@ export const initNodeDetailsRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const { nodeId, cloudId, nodeType, metrics, timerange, sourceId } = pipe( - NodeDetailsRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - const source = await libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); + const { nodeId, cloudId, nodeType, metrics, timerange, sourceId } = pipe( + NodeDetailsRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); - UsageCollector.countNode(nodeType); + UsageCollector.countNode(nodeType); - const options: InfraMetricsRequestOptions = { - nodeIds: { - nodeId, - cloudId, - }, - nodeType, - sourceConfiguration: source.configuration, - metrics, - timerange, - }; - return response.ok({ - body: NodeDetailsMetricDataResponseRT.encode({ - metrics: await libs.metrics.getMetrics(requestContext, options, request), - }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + const options: InfraMetricsRequestOptions = { + nodeIds: { + nodeId, + cloudId, + }, + nodeType, + sourceConfiguration: source.configuration, + metrics, + timerange, + }; + return response.ok({ + body: NodeDetailsMetricDataResponseRT.encode({ + metrics: await libs.metrics.getMetrics(requestContext, options, request), + }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/overview/index.ts b/x-pack/plugins/infra/server/routes/overview/index.ts index 4102fd883e9127..fe988abcc2883d 100644 --- a/x-pack/plugins/infra/server/routes/overview/index.ts +++ b/x-pack/plugins/infra/server/routes/overview/index.ts @@ -36,77 +36,71 @@ export const initOverviewRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const overviewRequest = pipe( - OverviewRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const overviewRequest = pipe( + OverviewRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = createSearchClient(requestContext, framework); - const source = await libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - overviewRequest.sourceId - ); + const client = createSearchClient(requestContext, framework); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + overviewRequest.sourceId + ); - const inventoryModelFields = findInventoryFields('host', source.configuration.fields); + const inventoryModelFields = findInventoryFields('host', source.configuration.fields); - const params = { - index: source.configuration.metricAlias, - body: { - query: { - range: { - [source.configuration.fields.timestamp]: { - gte: overviewRequest.timerange.from, - lte: overviewRequest.timerange.to, - format: 'epoch_millis', - }, + const params = { + index: source.configuration.metricAlias, + body: { + query: { + range: { + [source.configuration.fields.timestamp]: { + gte: overviewRequest.timerange.from, + lte: overviewRequest.timerange.to, + format: 'epoch_millis', }, }, - aggs: { - hosts: { - cardinality: { - field: inventoryModelFields.id, - }, + }, + aggs: { + hosts: { + cardinality: { + field: inventoryModelFields.id, }, - cpu: { - avg: { - field: 'system.cpu.total.norm.pct', - }, + }, + cpu: { + avg: { + field: 'system.cpu.total.norm.pct', }, - memory: { - avg: { - field: 'system.memory.actual.used.pct', - }, + }, + memory: { + avg: { + field: 'system.memory.actual.used.pct', }, }, }, - }; + }, + }; - const esResponse = await client<{}, OverviewESAggResponse>(params); + const esResponse = await client<{}, OverviewESAggResponse>(params); - return response.ok({ - body: { - stats: { - hosts: { - type: 'number', - value: esResponse.aggregations?.hosts.value ?? 0, - }, - cpu: { - type: 'percent', - value: esResponse.aggregations?.cpu.value ?? 0, - }, - memory: { - type: 'percent', - value: esResponse.aggregations?.memory.value ?? 0, - }, + return response.ok({ + body: { + stats: { + hosts: { + type: 'number', + value: esResponse.aggregations?.hosts.value ?? 0, + }, + cpu: { + type: 'percent', + value: esResponse.aggregations?.cpu.value ?? 0, + }, + memory: { + type: 'percent', + value: esResponse.aggregations?.memory.value ?? 0, }, }, - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + }, + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/process_list/index.ts b/x-pack/plugins/infra/server/routes/process_list/index.ts index ec4ec21fc16749..f1ba7a7be03600 100644 --- a/x-pack/plugins/infra/server/routes/process_list/index.ts +++ b/x-pack/plugins/infra/server/routes/process_list/index.ts @@ -35,23 +35,17 @@ export const initProcessListRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const options = pipe( - ProcessListAPIRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const options = pipe( + ProcessListAPIRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = createSearchClient(requestContext, framework); - const processListResponse = await getProcessList(client, options); + const client = createSearchClient(requestContext, framework); + const processListResponse = await getProcessList(client, options); - return response.ok({ - body: ProcessListAPIResponseRT.encode(processListResponse), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: ProcessListAPIResponseRT.encode(processListResponse), + }); } ); @@ -64,23 +58,17 @@ export const initProcessListRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const options = pipe( - ProcessListAPIChartRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const options = pipe( + ProcessListAPIChartRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = createSearchClient(requestContext, framework); - const processListResponse = await getProcessListChart(client, options); + const client = createSearchClient(requestContext, framework); + const processListResponse = await getProcessListChart(client, options); - return response.ok({ - body: ProcessListAPIChartResponseRT.encode(processListResponse), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: ProcessListAPIChartResponseRT.encode(processListResponse), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts index 07402a3f3ab5d9..aaf23085d0d600 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts @@ -31,29 +31,23 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const snapshotRequest = pipe( - SnapshotRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const snapshotRequest = pipe( + SnapshotRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const source = await libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - snapshotRequest.sourceId - ); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + snapshotRequest.sourceId + ); - UsageCollector.countNode(snapshotRequest.nodeType); - const client = createSearchClient(requestContext, framework); - const snapshotResponse = await getNodes(client, snapshotRequest, source); + UsageCollector.countNode(snapshotRequest.nodeType); + const client = createSearchClient(requestContext, framework); + const snapshotResponse = await getNodes(client, snapshotRequest, source); - return response.ok({ - body: SnapshotNodeResponseRT.encode(snapshotResponse), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: SnapshotNodeResponseRT.encode(snapshotResponse), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/source/index.ts b/x-pack/plugins/infra/server/routes/source/index.ts index 5c3827e56ce79c..5ab3275f9ea9e0 100644 --- a/x-pack/plugins/infra/server/routes/source/index.ts +++ b/x-pack/plugins/infra/server/routes/source/index.ts @@ -44,34 +44,28 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const { type, sourceId } = request.params; + const { type, sourceId } = request.params; - const [source, logIndexStatus, metricIndicesExist, indexFields] = await Promise.all([ - libs.sources.getSourceConfiguration(requestContext.core.savedObjects.client, sourceId), - libs.sourceStatus.getLogIndexStatus(requestContext, sourceId), - libs.sourceStatus.hasMetricIndices(requestContext, sourceId), - libs.fields.getFields(requestContext, sourceId, typeToInfraIndexType(type)), - ]); + const [source, logIndexStatus, metricIndicesExist, indexFields] = await Promise.all([ + libs.sources.getSourceConfiguration(requestContext.core.savedObjects.client, sourceId), + libs.sourceStatus.getLogIndexStatus(requestContext, sourceId), + libs.sourceStatus.hasMetricIndices(requestContext, sourceId), + libs.fields.getFields(requestContext, sourceId, typeToInfraIndexType(type)), + ]); - if (!source) { - return response.notFound(); - } + if (!source) { + return response.notFound(); + } - const status: InfraSourceStatus = { - logIndicesExist: logIndexStatus !== 'missing', - metricIndicesExist, - indexFields, - }; + const status: InfraSourceStatus = { + logIndicesExist: logIndexStatus !== 'missing', + metricIndicesExist, + indexFields, + }; - return response.ok({ - body: SourceResponseRuntimeType.encode({ source: { ...source, status } }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: SourceResponseRuntimeType.encode({ source: { ...source, status } }), + }); } ); @@ -169,26 +163,20 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const { type, sourceId } = request.params; - - const client = createSearchClient(requestContext, framework); - const source = await libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); - const indexPattern = - type === 'metrics' ? source.configuration.metricAlias : source.configuration.logAlias; - const results = await hasData(indexPattern, client); - - return response.ok({ - body: { hasData: results }, - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + const { type, sourceId } = request.params; + + const client = createSearchClient(requestContext, framework); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); + const indexPattern = + type === 'metrics' ? source.configuration.metricAlias : source.configuration.logAlias; + const results = await hasData(indexPattern, client); + + return response.ok({ + body: { hasData: results }, + }); } ); }; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts index 363254d63a2c7e..afa36e5abe31a1 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts @@ -83,7 +83,7 @@ export const registerCreateRoute = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/documents.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/documents.ts index b23a42b895af92..635ee015be5162 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/documents.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/documents.ts @@ -51,7 +51,7 @@ export const registerDocumentsRoute = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts index 6237f4b6911bdb..3995448d13fbb9 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts @@ -44,7 +44,7 @@ export const registerGetRoutes = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); @@ -78,7 +78,7 @@ export const registerGetRoutes = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts index 52492c3ee6d27a..527b4d4277bf5f 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts @@ -44,28 +44,24 @@ export const registerPrivilegesRoute = ({ license, router, config }: RouteDepend }, } = ctx; - try { - const { has_all_requested: hasAllPrivileges, cluster } = await client.callAsCurrentUser( - 'transport.request', - { - path: '/_security/user/_has_privileges', - method: 'POST', - body: { - cluster: APP_CLUSTER_REQUIRED_PRIVILEGES, - }, - } - ); - - if (!hasAllPrivileges) { - privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster); + const { has_all_requested: hasAllPrivileges, cluster } = await client.callAsCurrentUser( + 'transport.request', + { + path: '/_security/user/_has_privileges', + method: 'POST', + body: { + cluster: APP_CLUSTER_REQUIRED_PRIVILEGES, + }, } + ); - privilegesResult.hasAllPrivileges = hasAllPrivileges; - - return res.ok({ body: privilegesResult }); - } catch (e) { - return res.internalError({ body: e }); + if (!hasAllPrivileges) { + privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster); } + + privilegesResult.hasAllPrivileges = hasAllPrivileges; + + return res.ok({ body: privilegesResult }); }) ); }; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts index efa2a84daca28c..f02aa0a8d5ed6d 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts @@ -52,7 +52,7 @@ export const registerSimulateRoute = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts index 30cfe1b2505c2c..8776aace5ad789 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts @@ -65,7 +65,7 @@ export const registerUpdateRoute = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/lens/server/routes/existing_fields.ts b/x-pack/plugins/lens/server/routes/existing_fields.ts index 11db9360749eaf..8a2db992a839da 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.ts @@ -78,11 +78,9 @@ export async function existingFieldsRoute(setup: CoreSetup, if (e.output.statusCode === 404) { return res.notFound({ body: e.output.payload.message }); } - return res.internalError({ body: e.output.payload.message }); + throw new Error(e.output.payload.message); } else { - return res.internalError({ - body: Boom.internal(e.message || e.name), - }); + throw e; } } } diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts index 9094e5442dc511..57b3e59f4ad5c1 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/plugins/lens/server/routes/field_stats.ts @@ -5,7 +5,6 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { errors } from '@elastic/elasticsearch'; import DateMath from '@elastic/datemath'; import { schema } from '@kbn/config-schema'; @@ -122,11 +121,9 @@ export async function initFieldsRoute(setup: CoreSetup) { if (e.output.statusCode === 404) { return res.notFound(); } - return res.internalError(e.output.message); + throw new Error(e.output.message); } else { - return res.internalError({ - body: Boom.internal(e.message || e.name), - }); + throw e; } } } diff --git a/x-pack/plugins/lens/server/routes/telemetry.ts b/x-pack/plugins/lens/server/routes/telemetry.ts index cb8cf4b15f8d90..efcde9d14ebbe2 100644 --- a/x-pack/plugins/lens/server/routes/telemetry.ts +++ b/x-pack/plugins/lens/server/routes/telemetry.ts @@ -5,7 +5,6 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { errors } from '@elastic/elasticsearch'; import { CoreSetup } from 'src/core/server'; import { schema } from '@kbn/config-schema'; @@ -84,11 +83,9 @@ export async function initLensUsageRoute(setup: CoreSetup) if (e.output.statusCode === 404) { return res.notFound(); } - return res.internalError(e.output.message); + throw new Error(e.output.message); } else { - return res.internalError({ - body: Boom.internal(e.message || e.name), - }); + throw e; } } } diff --git a/x-pack/plugins/license_management/server/routes/api/license/register_license_route.ts b/x-pack/plugins/license_management/server/routes/api/license/register_license_route.ts index 7fcb73ffeb0086..86f87506dfc2c7 100644 --- a/x-pack/plugins/license_management/server/routes/api/license/register_license_route.ts +++ b/x-pack/plugins/license_management/server/routes/api/license/register_license_route.ts @@ -23,18 +23,14 @@ export function registerLicenseRoute({ router, plugins: { licensing } }: RouteDe }, async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - try { - return res.ok({ - body: await putLicense({ - acknowledge: Boolean(req.query.acknowledge), - callAsCurrentUser, - licensing, - license: req.body, - }), - }); - } catch (e) { - return res.internalError({ body: e }); - } + return res.ok({ + body: await putLicense({ + acknowledge: Boolean(req.query.acknowledge), + callAsCurrentUser, + licensing, + license: req.body, + }), + }); } ); } diff --git a/x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.ts b/x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.ts index c5cd11c022cfe6..dd441051872d2b 100644 --- a/x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.ts +++ b/x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.ts @@ -16,12 +16,8 @@ export function registerPermissionsRoute({ router.post({ path: addBasePath('/permissions'), validate: false }, async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - try { - return res.ok({ - body: await getPermissions({ callAsCurrentUser, isSecurityEnabled }), - }); - } catch (e) { - return res.internalError({ body: e }); - } + return res.ok({ + body: await getPermissions({ callAsCurrentUser, isSecurityEnabled }), + }); }); } diff --git a/x-pack/plugins/license_management/server/routes/api/license/register_start_basic_route.ts b/x-pack/plugins/license_management/server/routes/api/license/register_start_basic_route.ts index 820330c6a12044..bc5fb70f7dadd3 100644 --- a/x-pack/plugins/license_management/server/routes/api/license/register_start_basic_route.ts +++ b/x-pack/plugins/license_management/server/routes/api/license/register_start_basic_route.ts @@ -18,17 +18,13 @@ export function registerStartBasicRoute({ router, plugins: { licensing } }: Rout }, async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - try { - return res.ok({ - body: await startBasic({ - acknowledge: Boolean(req.query.acknowledge), - callAsCurrentUser, - licensing, - }), - }); - } catch (e) { - return res.internalError({ body: e }); - } + return res.ok({ + body: await startBasic({ + acknowledge: Boolean(req.query.acknowledge), + callAsCurrentUser, + licensing, + }), + }); } ); } diff --git a/x-pack/plugins/license_management/server/routes/api/license/register_start_trial_routes.ts b/x-pack/plugins/license_management/server/routes/api/license/register_start_trial_routes.ts index 570ae73d8aa613..6986e85e7d280d 100644 --- a/x-pack/plugins/license_management/server/routes/api/license/register_start_trial_routes.ts +++ b/x-pack/plugins/license_management/server/routes/api/license/register_start_trial_routes.ts @@ -12,21 +12,13 @@ import { addBasePath } from '../../helpers'; export function registerStartTrialRoutes({ router, plugins: { licensing } }: RouteDependencies) { router.get({ path: addBasePath('/start_trial'), validate: false }, async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - try { - return res.ok({ body: await canStartTrial(callAsCurrentUser) }); - } catch (e) { - return res.internalError({ body: e }); - } + return res.ok({ body: await canStartTrial(callAsCurrentUser) }); }); router.post({ path: addBasePath('/start_trial'), validate: false }, async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - try { - return res.ok({ - body: await startTrial({ callAsCurrentUser, licensing }), - }); - } catch (e) { - return res.internalError({ body: e }); - } + return res.ok({ + body: await startTrial({ callAsCurrentUser, licensing }), + }); }); } diff --git a/x-pack/plugins/logstash/server/routes/cluster/load.ts b/x-pack/plugins/logstash/server/routes/cluster/load.ts index f820ecdbeb4f34..ac7bc245e51ebc 100644 --- a/x-pack/plugins/logstash/server/routes/cluster/load.ts +++ b/x-pack/plugins/logstash/server/routes/cluster/load.ts @@ -29,7 +29,7 @@ export function registerClusterLoadRoute(router: LogstashPluginRouter) { if (err.status === 403) { return response.ok(); } - return response.internalError(); + throw err; } }) ); diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 654c3de7d81a96..4fada2d17bf5d5 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -368,7 +368,7 @@ export class MonitoringPlugin if (Boom.isBoom(err) || statusCode !== 500) { return res.customError({ statusCode, body: err }); } - return res.internalError(wrapError(err)); + throw wrapError(err).body; } }; diff --git a/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts index 1d9881c400ec8d..685aee16dc665c 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts @@ -88,7 +88,7 @@ export const register = (deps: RouteDependencies): void => { if (isEsError(error)) { return response.customError({ statusCode: error.statusCode, body: error }); } - return response.internalError({ body: error }); + throw error; } }; deps.router.post( diff --git a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts index 3a65bb2c54d952..89df5255d19e2e 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts @@ -95,7 +95,7 @@ export const register = (deps: RouteDependencies): void => { if (isEsError(error)) { return response.customError({ statusCode: error.statusCode, body: error }); } - return response.internalError({ body: error }); + throw error; } }; @@ -132,7 +132,7 @@ export const register = (deps: RouteDependencies): void => { if (isEsError(error)) { return response.customError({ statusCode: error.statusCode, body: error }); } - return response.internalError({ body: error }); + throw error; } }; diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts index 25d17d796b0ee4..cfec01da943ab7 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts @@ -78,10 +78,16 @@ describe('GET remote clusters', () => { const mockContext = xpackMocks.createRequestHandlerContext(); mockContext.core.elasticsearch.legacy.client = mockScopedClusterClient; - const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + if (asserts.statusCode === 500) { + await expect(handler(mockContext, mockRequest, kibanaResponseFactory)).rejects.toThrowError( + asserts.result as Error + ); + } else { + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); - expect(response.status).toBe(asserts.statusCode); - expect(response.payload).toEqual(asserts.result); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + } if (Array.isArray(asserts.apiArguments)) { for (const apiArguments of asserts.apiArguments) { diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts index 1445316cfec37c..fbb345203e48a8 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts @@ -63,7 +63,7 @@ export const register = (deps: RouteDependencies): void => { if (isEsError(error)) { return response.customError({ statusCode: error.statusCode, body: error }); } - return response.internalError({ body: error }); + throw error; } }; diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts index 5e1fdbb2bc0db1..99fb7dd01adb13 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts @@ -100,7 +100,7 @@ export const register = (deps: RouteDependencies): void => { if (isEsError(error)) { return response.customError({ statusCode: error.statusCode, body: error }); } - return response.internalError({ body: error }); + throw error; } }; diff --git a/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts b/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts index cd35552ed5ad71..694ab3c467c1f9 100644 --- a/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts @@ -34,7 +34,7 @@ export const registerGetRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts b/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts index 854f4986e76867..90eabaa88b6410 100644 --- a/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts @@ -136,7 +136,7 @@ export const registerValidateIndexPatternRoute = ({ return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts index 14ff452a4dd546..bcb3a337aa7253 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts @@ -43,7 +43,7 @@ export const registerCreateRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts index e94a1a80ce134d..4bbe73753e96cf 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts @@ -45,7 +45,7 @@ export const registerDeleteRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts index 12b3f96e778358..a9a30c0370c5f3 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts @@ -26,7 +26,7 @@ export const registerGetRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts index c560b41cc4385b..2ebfcc437f41e5 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts @@ -42,7 +42,7 @@ export const registerStartRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts index 87cf2822d4f1b6..faaf377a2d833d 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts @@ -43,7 +43,7 @@ export const registerStopRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts b/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts index 759e05dc2a3343..f77ae7829bb6c5 100644 --- a/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts @@ -41,7 +41,7 @@ export const registerSearchRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/security/server/authentication/authentication_service.test.ts b/x-pack/plugins/security/server/authentication/authentication_service.test.ts index 65dd15b627c7f1..a9c5fa3577476d 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.test.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.test.ts @@ -149,7 +149,6 @@ describe('AuthenticationService', () => { expect(mockAuthToolkit.authenticated).toHaveBeenCalledTimes(1); expect(mockAuthToolkit.authenticated).toHaveBeenCalledWith(); expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); - expect(mockResponse.internalError).not.toHaveBeenCalled(); expect(authenticate).not.toHaveBeenCalled(); }); @@ -172,7 +171,6 @@ describe('AuthenticationService', () => { requestHeaders: mockAuthHeaders, }); expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); - expect(mockResponse.internalError).not.toHaveBeenCalled(); expect(authenticate).toHaveBeenCalledTimes(1); expect(authenticate).toHaveBeenCalledWith(mockRequest); @@ -201,7 +199,6 @@ describe('AuthenticationService', () => { responseHeaders: mockAuthResponseHeaders, }); expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); - expect(mockResponse.internalError).not.toHaveBeenCalled(); expect(authenticate).toHaveBeenCalledTimes(1); expect(authenticate).toHaveBeenCalledWith(mockRequest); @@ -223,7 +220,6 @@ describe('AuthenticationService', () => { 'WWW-Authenticate': 'Negotiate', }); expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled(); - expect(mockResponse.internalError).not.toHaveBeenCalled(); }); it('rejects with `Internal Server Error` and log error when `authenticate` throws unhandled exception', async () => { @@ -231,15 +227,12 @@ describe('AuthenticationService', () => { const failureReason = new Error('something went wrong'); authenticate.mockRejectedValue(failureReason); - await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit); - - expect(mockResponse.internalError).toHaveBeenCalledTimes(1); - const [[error]] = mockResponse.internalError.mock.calls; - expect(error).toBeUndefined(); + await expect( + authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit) + ).rejects.toThrow(failureReason); expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled(); expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); - expect(logger.error).toHaveBeenCalledWith(failureReason); }); it('rejects with original `badRequest` error when `authenticate` fails to authenticate user', async () => { diff --git a/x-pack/plugins/security/server/authentication/authentication_service.ts b/x-pack/plugins/security/server/authentication/authentication_service.ts index 0543e2abd60df3..6848d7a3c7df63 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.ts @@ -93,13 +93,7 @@ export class AuthenticationService { }); } - let authenticationResult; - try { - authenticationResult = await this.authenticator.authenticate(request); - } catch (err) { - this.logger.error(err); - return response.internalError(); - } + const authenticationResult = await this.authenticator.authenticate(request); if (authenticationResult.succeeded()) { return t.authenticated({ diff --git a/x-pack/plugins/security/server/routes/authentication/common.test.ts b/x-pack/plugins/security/server/routes/authentication/common.test.ts index 38f832cc051ddb..654e4fc18f195a 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.test.ts @@ -413,11 +413,9 @@ describe('Common authentication routes', () => { body: { providerType: 'saml', providerName: 'saml1', currentURL: '/some-url' }, }); - await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({ - status: 500, - payload: 'Internal Error', - options: {}, - }); + await expect(routeHandler(mockContext, request, kibanaResponseFactory)).rejects.toThrow( + unhandledException + ); }); it('returns 401 if login fails.', async () => { @@ -683,11 +681,9 @@ describe('Common authentication routes', () => { authc.acknowledgeAccessAgreement.mockRejectedValue(unhandledException); const request = httpServerMock.createKibanaRequest(); - await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({ - status: 500, - payload: 'Internal Error', - options: {}, - }); + await expect(routeHandler(mockContext, request, kibanaResponseFactory)).rejects.toThrowError( + unhandledException + ); }); it('returns 204 if successfully acknowledged.', async () => { diff --git a/x-pack/plugins/security/server/routes/authentication/common.ts b/x-pack/plugins/security/server/routes/authentication/common.ts index 0b0915198f3d40..f1d9aab74548a4 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.ts @@ -142,28 +142,23 @@ export function defineCommonRoutes({ logger.info(`Logging in with provider "${providerName}" (${providerType})`); const redirectURL = parseNext(currentURL, basePath.serverBasePath); - try { - const authenticationResult = await getAuthenticationService().login(request, { - provider: { name: providerName }, - redirectURL, - value: getLoginAttemptForProviderType(providerType, redirectURL, params), - }); - - if (authenticationResult.redirected() || authenticationResult.succeeded()) { - return response.ok({ - body: { location: authenticationResult.redirectURL || redirectURL }, - headers: authenticationResult.authResponseHeaders, - }); - } - - return response.unauthorized({ - body: authenticationResult.error, + const authenticationResult = await getAuthenticationService().login(request, { + provider: { name: providerName }, + redirectURL, + value: getLoginAttemptForProviderType(providerType, redirectURL, params), + }); + + if (authenticationResult.redirected() || authenticationResult.succeeded()) { + return response.ok({ + body: { location: authenticationResult.redirectURL || redirectURL }, headers: authenticationResult.authResponseHeaders, }); - } catch (err) { - logger.error(err); - return response.internalError(); } + + return response.unauthorized({ + body: authenticationResult.error, + headers: authenticationResult.authResponseHeaders, + }); }) ); @@ -178,12 +173,7 @@ export function defineCommonRoutes({ }); } - try { - await getAuthenticationService().acknowledgeAccessAgreement(request); - } catch (err) { - logger.error(err); - return response.internalError(); - } + await getAuthenticationService().acknowledgeAccessAgreement(request); return response.noContent(); }) diff --git a/x-pack/plugins/security/server/routes/authentication/saml.test.ts b/x-pack/plugins/security/server/routes/authentication/saml.test.ts index 5a08c3e1797047..73cba46f46ea70 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.test.ts @@ -76,16 +76,14 @@ describe('SAML authentication routes', () => { const unhandledException = new Error('Something went wrong.'); authc.login.mockRejectedValue(unhandledException); - const internalServerErrorResponse = Symbol('error'); const responseFactory = httpServerMock.createResponseFactory(); - responseFactory.internalError.mockReturnValue(internalServerErrorResponse as any); const request = httpServerMock.createKibanaRequest({ body: { SAMLResponse: 'saml-response' }, }); - await expect(routeHandler({} as any, request, responseFactory)).resolves.toBe( - internalServerErrorResponse + await expect(routeHandler({} as any, request, responseFactory)).rejects.toThrow( + unhandledException ); expect(authc.login).toHaveBeenCalledWith(request, { diff --git a/x-pack/plugins/security/server/routes/authentication/saml.ts b/x-pack/plugins/security/server/routes/authentication/saml.ts index 9fee03cbc614a1..257b95ec707b40 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.ts @@ -30,28 +30,23 @@ export function defineSAMLRoutes({ options: { authRequired: false, xsrfRequired: false }, }, async (context, request, response) => { - try { - // When authenticating using SAML we _expect_ to redirect to the Kibana target location. - const authenticationResult = await getAuthenticationService().login(request, { - provider: { type: SAMLAuthenticationProvider.type }, - value: { - type: SAMLLogin.LoginWithSAMLResponse, - samlResponse: request.body.SAMLResponse, - relayState: request.body.RelayState, - }, - }); - - if (authenticationResult.redirected()) { - return response.redirected({ - headers: { location: authenticationResult.redirectURL! }, - }); - } + // When authenticating using SAML we _expect_ to redirect to the Kibana target location. + const authenticationResult = await getAuthenticationService().login(request, { + provider: { type: SAMLAuthenticationProvider.type }, + value: { + type: SAMLLogin.LoginWithSAMLResponse, + samlResponse: request.body.SAMLResponse, + relayState: request.body.RelayState, + }, + }); - return response.unauthorized({ body: authenticationResult.error }); - } catch (err) { - logger.error(err); - return response.internalError(); + if (authenticationResult.redirected()) { + return response.redirected({ + headers: { location: authenticationResult.redirectURL! }, + }); } + + return response.unauthorized({ body: authenticationResult.error }); } ); } diff --git a/x-pack/plugins/security/server/routes/session_management/info.test.ts b/x-pack/plugins/security/server/routes/session_management/info.test.ts index 6ed50b50c0eb91..84db94f38d5829 100644 --- a/x-pack/plugins/security/server/routes/session_management/info.test.ts +++ b/x-pack/plugins/security/server/routes/session_management/info.test.ts @@ -60,11 +60,7 @@ describe('Info session routes', () => { request, kibanaResponseFactory ) - ).resolves.toEqual({ - status: 500, - options: {}, - payload: 'Internal Error', - }); + ).rejects.toThrowError(unhandledException); expect(session.get).toHaveBeenCalledWith(request); }); diff --git a/x-pack/plugins/security/server/routes/session_management/info.ts b/x-pack/plugins/security/server/routes/session_management/info.ts index f47d896eb55e00..6cab44509f162e 100644 --- a/x-pack/plugins/security/server/routes/session_management/info.ts +++ b/x-pack/plugins/security/server/routes/session_management/info.ts @@ -11,30 +11,25 @@ import { RouteDefinitionParams } from '..'; /** * Defines routes required for the session info. */ -export function defineSessionInfoRoutes({ router, logger, getSession }: RouteDefinitionParams) { +export function defineSessionInfoRoutes({ router, getSession }: RouteDefinitionParams) { router.get( { path: '/internal/security/session', validate: false }, async (_context, request, response) => { - try { - const sessionValue = await getSession().get(request); - if (sessionValue) { - return response.ok({ - body: { - // We can't rely on the client's system clock, so in addition to returning expiration timestamps, we also return - // the current server time -- that way the client can calculate the relative time to expiration. - now: Date.now(), - idleTimeoutExpiration: sessionValue.idleTimeoutExpiration, - lifespanExpiration: sessionValue.lifespanExpiration, - provider: sessionValue.provider, - } as SessionInfo, - }); - } - - return response.noContent(); - } catch (err) { - logger.error(`Error retrieving user session: ${err.message}`); - return response.internalError(); + const sessionValue = await getSession().get(request); + if (sessionValue) { + return response.ok({ + body: { + // We can't rely on the client's system clock, so in addition to returning expiration timestamps, we also return + // the current server time -- that way the client can calculate the relative time to expiration. + now: Date.now(), + idleTimeoutExpiration: sessionValue.idleTimeoutExpiration, + lifespanExpiration: sessionValue.lifespanExpiration, + provider: sessionValue.provider, + } as SessionInfo, + }); } + + return response.noContent(); } ); } diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.ts b/x-pack/plugins/security/server/routes/views/access_agreement.ts index ff67e1af1e7baa..daf697bd23448b 100644 --- a/x-pack/plugins/security/server/routes/views/access_agreement.ts +++ b/x-pack/plugins/security/server/routes/views/access_agreement.ts @@ -46,20 +46,15 @@ export function defineAccessAgreementRoutes({ // It's not guaranteed that we'll have session for the authenticated user (e.g. when user is // authenticated with the help of HTTP authentication), that means we should safely check if // we have it and can get a corresponding configuration. - try { - const sessionValue = await getSession().get(request); - const accessAgreement = - (sessionValue && - config.authc.providers[ - sessionValue.provider.type as keyof ConfigType['authc']['providers'] - ]?.[sessionValue.provider.name]?.accessAgreement?.message) || - ''; + const sessionValue = await getSession().get(request); + const accessAgreement = + (sessionValue && + config.authc.providers[ + sessionValue.provider.type as keyof ConfigType['authc']['providers'] + ]?.[sessionValue.provider.name]?.accessAgreement?.message) || + ''; - return response.ok({ body: { accessAgreement } }); - } catch (err) { - logger.error(err); - return response.internalError(); - } + return response.ok({ body: { accessAgreement } }); }) ); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.ts index 020b70ca0553ca..95070b10b95501 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.ts @@ -78,7 +78,7 @@ export function registerDownloadArtifactRoute( }; if (validateDownload && !downloadArtifactResponseSchema.is(artifact)) { - return res.internalError({ body: 'Artifact failed to validate.' }); + throw new Error('Artifact failed to validate.'); } else { return res.ok(artifact); } @@ -103,7 +103,7 @@ export function registerDownloadArtifactRoute( if (err?.output?.statusCode === 404) { return res.notFound({ body: `No artifact found for ${id}` }); } else { - return res.internalError({ body: err }); + throw err; } }); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index ad5381d2ee36d6..134ce99784bfb6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -62,57 +62,52 @@ export const getMetadataListRequestHandler = function ( SecuritySolutionRequestHandlerContext > { return async (context, request, response) => { - try { - const agentService = endpointAppContext.service.getAgentService(); - if (agentService === undefined) { - throw new Error('agentService not available'); - } + const agentService = endpointAppContext.service.getAgentService(); + if (agentService === undefined) { + throw new Error('agentService not available'); + } - const metadataRequestContext: MetadataRequestContext = { - endpointAppContextService: endpointAppContext.service, - logger, - requestHandlerContext: context, - }; + const metadataRequestContext: MetadataRequestContext = { + endpointAppContextService: endpointAppContext.service, + logger, + requestHandlerContext: context, + }; - const unenrolledAgentIds = await findAllUnenrolledAgentIds( - agentService, - context.core.savedObjects.client, - context.core.elasticsearch.client.asCurrentUser - ); + const unenrolledAgentIds = await findAllUnenrolledAgentIds( + agentService, + context.core.savedObjects.client, + context.core.elasticsearch.client.asCurrentUser + ); - const statusIDs = request?.body?.filters?.host_status?.length - ? await findAgentIDsByStatus( - agentService, - context.core.savedObjects.client, - context.core.elasticsearch.client.asCurrentUser, - request.body?.filters?.host_status - ) - : undefined; + const statusIDs = request?.body?.filters?.host_status?.length + ? await findAgentIDsByStatus( + agentService, + context.core.savedObjects.client, + context.core.elasticsearch.client.asCurrentUser, + request.body?.filters?.host_status + ) + : undefined; - const queryStrategy = await endpointAppContext.service - ?.getMetadataService() - ?.queryStrategy(context.core.savedObjects.client, queryStrategyVersion); + const queryStrategy = await endpointAppContext.service + ?.getMetadataService() + ?.queryStrategy(context.core.savedObjects.client, queryStrategyVersion); - const queryParams = await kibanaRequestToMetadataListESQuery( - request, - endpointAppContext, - queryStrategy!, - { - unenrolledAgentIds: unenrolledAgentIds.concat(IGNORED_ELASTIC_AGENT_IDS), - statusAgentIDs: statusIDs, - } - ); + const queryParams = await kibanaRequestToMetadataListESQuery( + request, + endpointAppContext, + queryStrategy!, + { + unenrolledAgentIds: unenrolledAgentIds.concat(IGNORED_ELASTIC_AGENT_IDS), + statusAgentIDs: statusIDs, + } + ); - const hostListQueryResult = queryStrategy!.queryResponseToHostListResult( - await context.core.elasticsearch.legacy.client.callAsCurrentUser('search', queryParams) - ); - return response.ok({ - body: await mapToHostResultList(queryParams, hostListQueryResult, metadataRequestContext), - }); - } catch (err) { - logger.warn(JSON.stringify(err, null, 2)); - return response.internalError({ body: err }); - } + const hostListQueryResult = queryStrategy!.queryResponseToHostListResult( + await context.core.elasticsearch.legacy.client.callAsCurrentUser('search', queryParams) + ); + return response.ok({ + body: await mapToHostResultList(queryParams, hostListQueryResult, metadataRequestContext), + }); }; }; @@ -129,7 +124,7 @@ export const getMetadataRequestHandler = function ( return async (context, request, response) => { const agentService = endpointAppContext.service.getAgentService(); if (agentService === undefined) { - return response.internalError({ body: 'agentService not available' }); + throw new Error('agentService not available'); } const metadataRequestContext: MetadataRequestContext = { @@ -156,7 +151,7 @@ export const getMetadataRequestHandler = function ( body: { message: err.message }, }); } - return response.internalError({ body: err }); + throw err; } }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts index 132cc6dae58fff..d6b50becc2d029 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts @@ -49,11 +49,7 @@ describe('test policy response handler', () => { it('should return the latest policy response for a host', async () => { const response = createSearchResponse(new EndpointDocGenerator().generatePolicyResponse()); - const hostPolicyResponseHandler = getHostPolicyResponseHandler({ - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - }); + const hostPolicyResponseHandler = getHostPolicyResponseHandler(); mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); const mockRequest = httpServerMock.createKibanaRequest({ @@ -72,11 +68,7 @@ describe('test policy response handler', () => { }); it('should return not found when there is no response policy for host', async () => { - const hostPolicyResponseHandler = getHostPolicyResponseHandler({ - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - }); + const hostPolicyResponseHandler = getHostPolicyResponseHandler(); mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(createSearchResponse()) diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts index 3027892ff37452..ec1fad80701b62 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts @@ -16,25 +16,23 @@ import { EndpointAppContext } from '../../types'; import { getAgentPolicySummary, getPolicyResponseByAgentId } from './service'; import { GetAgentSummaryResponse } from '../../../../common/endpoint/types'; -export const getHostPolicyResponseHandler = function ( - endpointAppContext: EndpointAppContext -): RequestHandler, undefined> { +export const getHostPolicyResponseHandler = function (): RequestHandler< + undefined, + TypeOf, + undefined +> { return async (context, request, response) => { - try { - const doc = await getPolicyResponseByAgentId( - policyIndexPattern, - request.query.agentId, - context.core.elasticsearch.legacy.client - ); - - if (doc) { - return response.ok({ body: doc }); - } + const doc = await getPolicyResponseByAgentId( + policyIndexPattern, + request.query.agentId, + context.core.elasticsearch.legacy.client + ); - return response.notFound({ body: 'Policy Response Not Found' }); - } catch (err) { - return response.internalError({ body: err }); + if (doc) { + return response.ok({ body: doc }); } + + return response.notFound({ body: 'Policy Response Not Found' }); }; }; @@ -42,31 +40,26 @@ export const getAgentPolicySummaryHandler = function ( endpointAppContext: EndpointAppContext ): RequestHandler, undefined> { return async (context, request, response) => { - try { - const result = await getAgentPolicySummary( - endpointAppContext, - context.core.savedObjects.client, - context.core.elasticsearch.client.asCurrentUser, - request.query.package_name, - request.query?.policy_id || undefined - ); - const responseBody = { - package: request.query.package_name, - versions_count: { ...result }, - }; + const result = await getAgentPolicySummary( + endpointAppContext, + context.core.savedObjects.client, + context.core.elasticsearch.client.asCurrentUser, + request.query.package_name, + request.query?.policy_id || undefined + ); + const responseBody = { + package: request.query.package_name, + versions_count: { ...result }, + }; - const body: GetAgentSummaryResponse = { - summary_response: request.query?.policy_id - ? { ...responseBody, ...{ policy_id: request.query?.policy_id } } - : responseBody, - }; + const body: GetAgentSummaryResponse = { + summary_response: request.query?.policy_id + ? { ...responseBody, ...{ policy_id: request.query?.policy_id } } + : responseBody, + }; - return response.ok({ - body, - }); - } catch (err) { - endpointAppContext.logFactory.get('metadata').error(JSON.stringify(err, null, 2)); - return response.internalError({ body: err }); - } + return response.ok({ + body, + }); }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts index 0c199890c205d9..50a68debc11255 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts @@ -26,7 +26,7 @@ export function registerPolicyRoutes(router: IRouter, endpointAppContext: Endpoi validate: GetPolicyResponseSchema, options: { authRequired: true }, }, - getHostPolicyResponseHandler(endpointAppContext) + getHostPolicyResponseHandler() ); router.get( diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts index 398666b40ae380..617a1907cf4bef 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts @@ -6,7 +6,6 @@ */ import { IRouter } from 'kibana/server'; -import { EndpointAppContext } from '../types'; import { validateEvents, validateEntities, @@ -17,16 +16,14 @@ import { handleTree } from './resolver/tree/handler'; import { handleEntities } from './resolver/entity'; import { handleEvents } from './resolver/events'; -export function registerResolverRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { - const log = endpointAppContext.logFactory.get('resolver'); - +export function registerResolverRoutes(router: IRouter) { router.post( { path: '/api/endpoint/resolver/tree', validate: validateTree, options: { authRequired: true }, }, - handleTree(log) + handleTree() ); router.post( @@ -35,7 +32,7 @@ export function registerResolverRoutes(router: IRouter, endpointAppContext: Endp validate: validateEvents, options: { authRequired: true }, }, - handleEvents(log) + handleEvents() ); /** diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts index edbfc4a4423e2d..6b574b3bcbc278 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts @@ -6,7 +6,7 @@ */ import { TypeOf } from '@kbn/config-schema'; -import { RequestHandler, Logger } from 'kibana/server'; +import { RequestHandler } from 'kibana/server'; import { ResolverPaginatedEvents, SafeResolverEvent } from '../../../../common/endpoint/types'; import { validateEvents } from '../../../../common/endpoint/schema/resolver'; import { EventsQuery } from './queries/events'; @@ -28,11 +28,8 @@ function createEvents( /** * This function handles the `/events` api and returns an array of events and a cursor if more events exist than were * requested. - * @param log a logger object */ -export function handleEvents( - log: Logger -): RequestHandler< +export function handleEvents(): RequestHandler< unknown, TypeOf, TypeOf @@ -42,21 +39,16 @@ export function handleEvents( query: { limit, afterEvent }, body, } = req; - try { - const client = context.core.elasticsearch.client; - const query = new EventsQuery({ - pagination: PaginationBuilder.createBuilder(limit, afterEvent), - indexPatterns: body.indexPatterns, - timeRange: body.timeRange, - }); - const results = await query.search(client, body.filter); + const client = context.core.elasticsearch.client; + const query = new EventsQuery({ + pagination: PaginationBuilder.createBuilder(limit, afterEvent), + indexPatterns: body.indexPatterns, + timeRange: body.timeRange, + }); + const results = await query.search(client, body.filter); - return res.ok({ - body: createEvents(results, PaginationBuilder.buildCursorRequestLimit(limit, results)), - }); - } catch (err) { - log.warn(err); - return res.internalError({ body: err }); - } + return res.ok({ + body: createEvents(results, PaginationBuilder.buildCursorRequestLimit(limit, results)), + }); }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/handler.ts index e7b4046b3532ad..675c861b984de2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/handler.ts @@ -5,25 +5,18 @@ * 2.0. */ -import { RequestHandler, Logger } from 'kibana/server'; +import { RequestHandler } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; import { validateTree } from '../../../../../common/endpoint/schema/resolver'; import { Fetcher } from './utils/fetch'; -export function handleTree( - log: Logger -): RequestHandler> { +export function handleTree(): RequestHandler> { return async (context, req, res) => { - try { - const client = context.core.elasticsearch.client; - const fetcher = new Fetcher(client); - const body = await fetcher.tree(req.body); - return res.ok({ - body, - }); - } catch (err) { - log.warn(err); - return res.internalError({ body: 'Error retrieving tree.' }); - } + const client = context.core.elasticsearch.client; + const fetcher = new Fetcher(client); + const body = await fetcher.tree(req.body); + return res.ok({ + body, + }); }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts index 50aff5217a12ea..2179397c237044 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts @@ -112,7 +112,7 @@ describe('handlers', () => { }); describe('getTrustedAppsDeleteRouteHandler', () => { - const deleteTrustedAppHandler = getTrustedAppsDeleteRouteHandler(appContextMock); + const deleteTrustedAppHandler = getTrustedAppsDeleteRouteHandler(); it('should return ok when trusted app deleted', async () => { const mockResponse = httpServerMock.createResponseFactory(); @@ -146,19 +146,18 @@ describe('handlers', () => { exceptionsListClient.deleteExceptionListItem.mockRejectedValue(error); - await deleteTrustedAppHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ params: { id: '123' } }), - mockResponse - ); - - assertResponse(mockResponse, 'internalError', error); - expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error); + await expect( + deleteTrustedAppHandler( + createHandlerContextMock(), + httpServerMock.createKibanaRequest({ params: { id: '123' } }), + mockResponse + ) + ).rejects.toThrowError(error); }); }); describe('getTrustedAppsCreateRouteHandler', () => { - const createTrustedAppHandler = getTrustedAppsCreateRouteHandler(appContextMock); + const createTrustedAppHandler = getTrustedAppsCreateRouteHandler(); it('should return ok with body when trusted app created', async () => { const mockResponse = httpServerMock.createResponseFactory(); @@ -180,19 +179,18 @@ describe('handlers', () => { exceptionsListClient.createExceptionListItem.mockRejectedValue(error); - await createTrustedAppHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ body: NEW_TRUSTED_APP }), - mockResponse - ); - - assertResponse(mockResponse, 'internalError', error); - expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error); + await expect( + createTrustedAppHandler( + createHandlerContextMock(), + httpServerMock.createKibanaRequest({ body: NEW_TRUSTED_APP }), + mockResponse + ) + ).rejects.toThrowError(error); }); }); describe('getTrustedAppsListRouteHandler', () => { - const getTrustedAppsListHandler = getTrustedAppsListRouteHandler(appContextMock); + const getTrustedAppsListHandler = getTrustedAppsListRouteHandler(); it('should return ok with list when no errors', async () => { const mockResponse = httpServerMock.createResponseFactory(); @@ -224,19 +222,18 @@ describe('handlers', () => { exceptionsListClient.findExceptionListItem.mockRejectedValue(error); - await getTrustedAppsListHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ body: NEW_TRUSTED_APP }), - mockResponse - ); - - assertResponse(mockResponse, 'internalError', error); - expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error); + await expect( + getTrustedAppsListHandler( + createHandlerContextMock(), + httpServerMock.createKibanaRequest({ body: NEW_TRUSTED_APP }), + mockResponse + ) + ).rejects.toThrowError(error); }); }); describe('getTrustedAppsSummaryHandler', () => { - const getTrustedAppsSummaryHandler = getTrustedAppsSummaryRouteHandler(appContextMock); + const getTrustedAppsSummaryHandler = getTrustedAppsSummaryRouteHandler(); it('should return ok with list when no errors', async () => { const mockResponse = httpServerMock.createResponseFactory(); @@ -290,14 +287,13 @@ describe('handlers', () => { exceptionsListClient.findExceptionListItem.mockRejectedValue(error); - await getTrustedAppsSummaryHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest(), - mockResponse - ); - - assertResponse(mockResponse, 'internalError', error); - expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error); + await expect( + getTrustedAppsSummaryHandler( + createHandlerContextMock(), + httpServerMock.createKibanaRequest(), + mockResponse + ) + ).rejects.toThrowError(error); }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts index e7ceeee00c3069..fd5160472986f5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts @@ -16,7 +16,6 @@ import { PostTrustedAppCreateRequest, } from '../../../../common/endpoint/types'; -import { EndpointAppContext } from '../../types'; import { createTrustedApp, deleteTrustedApp, @@ -37,16 +36,12 @@ const exceptionListClientFromContext = ( return exceptionLists; }; -export const getTrustedAppsDeleteRouteHandler = ( - endpointAppContext: EndpointAppContext -): RequestHandler< +export const getTrustedAppsDeleteRouteHandler = (): RequestHandler< DeleteTrustedAppsRequestParams, unknown, unknown, SecuritySolutionRequestHandlerContext > => { - const logger = endpointAppContext.logFactory.get('trusted_apps'); - return async (context, req, res) => { try { await deleteTrustedApp(exceptionListClientFromContext(context), req.params); @@ -56,75 +51,47 @@ export const getTrustedAppsDeleteRouteHandler = ( if (error instanceof MissingTrustedAppException) { return res.notFound({ body: `trusted app id [${req.params.id}] not found` }); } else { - logger.error(error); - return res.internalError({ body: error }); + throw error; } } }; }; -export const getTrustedAppsListRouteHandler = ( - endpointAppContext: EndpointAppContext -): RequestHandler< +export const getTrustedAppsListRouteHandler = (): RequestHandler< unknown, GetTrustedAppsListRequest, unknown, SecuritySolutionRequestHandlerContext > => { - const logger = endpointAppContext.logFactory.get('trusted_apps'); - return async (context, req, res) => { - try { - return res.ok({ - body: await getTrustedAppsList(exceptionListClientFromContext(context), req.query), - }); - } catch (error) { - logger.error(error); - return res.internalError({ body: error }); - } + return res.ok({ + body: await getTrustedAppsList(exceptionListClientFromContext(context), req.query), + }); }; }; -export const getTrustedAppsCreateRouteHandler = ( - endpointAppContext: EndpointAppContext -): RequestHandler< +export const getTrustedAppsCreateRouteHandler = (): RequestHandler< unknown, unknown, PostTrustedAppCreateRequest, SecuritySolutionRequestHandlerContext > => { - const logger = endpointAppContext.logFactory.get('trusted_apps'); - return async (context, req, res) => { - try { - return res.ok({ - body: await createTrustedApp(exceptionListClientFromContext(context), req.body), - }); - } catch (error) { - logger.error(error); - return res.internalError({ body: error }); - } + return res.ok({ + body: await createTrustedApp(exceptionListClientFromContext(context), req.body), + }); }; }; -export const getTrustedAppsSummaryRouteHandler = ( - endpointAppContext: EndpointAppContext -): RequestHandler< +export const getTrustedAppsSummaryRouteHandler = (): RequestHandler< unknown, unknown, PostTrustedAppCreateRequest, SecuritySolutionRequestHandlerContext > => { - const logger = endpointAppContext.logFactory.get('trusted_apps'); - return async (context, req, res) => { - try { - return res.ok({ - body: await getTrustedAppsSummary(exceptionListClientFromContext(context)), - }); - } catch (error) { - logger.error(error); - return res.internalError({ body: error }); - } + return res.ok({ + body: await getTrustedAppsSummary(exceptionListClientFromContext(context)), + }); }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts index af23bc7025bf13..4a17b088dc8714 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts @@ -22,13 +22,9 @@ import { getTrustedAppsListRouteHandler, getTrustedAppsSummaryRouteHandler, } from './handlers'; -import { EndpointAppContext } from '../../types'; import { SecuritySolutionPluginRouter } from '../../../types'; -export const registerTrustedAppsRoutes = ( - router: SecuritySolutionPluginRouter, - endpointAppContext: EndpointAppContext -) => { +export const registerTrustedAppsRoutes = (router: SecuritySolutionPluginRouter) => { // DELETE one router.delete( { @@ -36,7 +32,7 @@ export const registerTrustedAppsRoutes = ( validate: DeleteTrustedAppsRequestSchema, options: { authRequired: true }, }, - getTrustedAppsDeleteRouteHandler(endpointAppContext) + getTrustedAppsDeleteRouteHandler() ); // GET list @@ -46,7 +42,7 @@ export const registerTrustedAppsRoutes = ( validate: GetTrustedAppsRequestSchema, options: { authRequired: true }, }, - getTrustedAppsListRouteHandler(endpointAppContext) + getTrustedAppsListRouteHandler() ); // CREATE @@ -56,7 +52,7 @@ export const registerTrustedAppsRoutes = ( validate: PostTrustedAppCreateRequestSchema, options: { authRequired: true }, }, - getTrustedAppsCreateRouteHandler(endpointAppContext) + getTrustedAppsCreateRouteHandler() ); // SUMMARY @@ -66,6 +62,6 @@ export const registerTrustedAppsRoutes = ( validate: false, options: { authRequired: true }, }, - getTrustedAppsSummaryRouteHandler(endpointAppContext) + getTrustedAppsSummaryRouteHandler() ); }; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 5f70b6cdc641e2..b675920977df15 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -198,9 +198,9 @@ export class Plugin implements IPlugin { jest.fn().mockRejectedValueOnce(new Error()), // Call to 'sr.policies' ]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -140,8 +139,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should throw if ES error', async () => { router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -174,8 +172,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should throw if ES error', async () => { router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -286,8 +283,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should throw if ES error', async () => { router.callAsCurrentUserResponses = [{}, jest.fn().mockRejectedValueOnce(new Error())]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -315,8 +311,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should throw if ES error', async () => { router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -372,8 +367,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should throw if ES error', async () => { router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -398,8 +392,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should throw if ES error', async () => { router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); }); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts index 3c656194aa8542..fa127880fd806a 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts @@ -51,7 +51,7 @@ export function registerPolicyRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -92,7 +92,7 @@ export function registerPolicyRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -131,7 +131,7 @@ export function registerPolicyRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -167,7 +167,7 @@ export function registerPolicyRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -222,7 +222,7 @@ export function registerPolicyRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -263,7 +263,7 @@ export function registerPolicyRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -323,7 +323,7 @@ export function registerPolicyRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts index 42c64ff3874feb..35dce2c5d558fa 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts @@ -108,8 +108,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { jest.fn().mockRejectedValueOnce(new Error()), ]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -214,8 +213,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { jest.fn().mockRejectedValueOnce(new Error()), ]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -292,8 +290,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { jest.fn().mockRejectedValueOnce(new Error('Error getting pluggins')), ]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError('Error getting pluggins'); }); }); @@ -328,9 +325,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { const error = new Error('Oh no!'); router.callAsCurrentUserResponses = [{}, jest.fn().mockRejectedValueOnce(error)]; - const response = await router.runRequest(mockRequest); - expect(response.body.message).toEqual(error.message); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(error); }); }); @@ -358,8 +353,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should throw if ES error', async () => { router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts index f8a5f01e4cf3df..c9945bb172e6c3 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts @@ -66,7 +66,7 @@ export function registerRepositoriesRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } // If a managed repository, we also need to check if a policy is associated to it @@ -121,7 +121,7 @@ export function registerRepositoriesRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } const { @@ -203,7 +203,7 @@ export function registerRepositoriesRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -244,7 +244,7 @@ export function registerRepositoriesRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -285,7 +285,7 @@ export function registerRepositoriesRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -329,7 +329,7 @@ export function registerRepositoriesRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -371,7 +371,7 @@ export function registerRepositoriesRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -412,7 +412,7 @@ export function registerRepositoriesRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts index bbf90841e774eb..fe33331522daa8 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts @@ -48,8 +48,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { it('should throw if ES error', async () => { router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -111,8 +110,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { it('should throw if ES error', async () => { router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); }); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts b/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts index df2bc20b026c9b..c4300bafc75fbb 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts @@ -87,7 +87,7 @@ export function registerRestoreRoutes({ router, license, lib: { isEsError } }: R }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -124,7 +124,7 @@ export function registerRestoreRoutes({ router, license, lib: { isEsError } }: R }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts index bcdd27a1f100c3..97eb34b4aaa732 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts @@ -144,8 +144,7 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { jest.fn().mockRejectedValueOnce(new Error('Error getting repository')), ]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -221,8 +220,7 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { mockSnapshotGetEsResponse, ]; - const response = await router.runRequest(mockRequest); - expect(response.status).toBe(500); + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts index 65e941db88df6a..03e3b4ecc08870 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts @@ -61,7 +61,7 @@ export function registerSnapshotsRoutes({ body: e, }); } - return res.internalError({ body: e }); + throw e; } const snapshots: SnapshotDetails[] = []; @@ -176,7 +176,7 @@ export function registerSnapshotsRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); @@ -229,7 +229,7 @@ export function registerSnapshotsRoutes({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/task_manager/server/routes/health.test.ts b/x-pack/plugins/task_manager/server/routes/health.test.ts index dd2a60bedcac6a..dd7ed69aaf27f9 100644 --- a/x-pack/plugins/task_manager/server/routes/health.test.ts +++ b/x-pack/plugins/task_manager/server/routes/health.test.ts @@ -114,7 +114,7 @@ describe('healthRoute', () => { const [, handler] = router.get.mock.calls[0]; - const [context, req, res] = mockHandlerArguments({}, {}, ['ok', 'internalError']); + const [context, req, res] = mockHandlerArguments({}, {}, ['ok']); await sleep(0); @@ -214,7 +214,7 @@ describe('healthRoute', () => { const [, handler] = router.get.mock.calls[0]; - const [context, req, res] = mockHandlerArguments({}, {}, ['ok', 'internalError']); + const [context, req, res] = mockHandlerArguments({}, {}, ['ok']); await sleep(2000); @@ -282,7 +282,7 @@ describe('healthRoute', () => { const [, handler] = router.get.mock.calls[0]; - const [context, req, res] = mockHandlerArguments({}, {}, ['ok', 'internalError']); + const [context, req, res] = mockHandlerArguments({}, {}, ['ok']); expect(await handler(context, req, res)).toMatchObject({ body: { diff --git a/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts index c2bb27ce995ebe..a5da4741b10eb0 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts @@ -96,12 +96,12 @@ describe('cluster checkup API', () => { it('returns an 500 error if it throws', async () => { MigrationApis.getUpgradeAssistantStatus.mockRejectedValue(new Error(`scary error!`)); - const resp = await routeDependencies.router.getHandler({ - method: 'get', - pathPattern: '/api/upgrade_assistant/status', - })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); - - expect(resp.status).toEqual(500); + await expect( + routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/status', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory) + ).rejects.toThrow('scary error!'); }); }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts index be8d1739341825..b4dae6ec385b45 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts @@ -55,7 +55,7 @@ export function registerClusterCheckupRoutes({ cloud, router, licensing, log }: return response.forbidden(e.message); } - return response.internalError({ body: e }); + throw e; } } ) diff --git a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts index 0e0dd075624a68..0b595df0dc8c45 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts @@ -54,12 +54,12 @@ describe('deprecation logging API', () => { it('returns an error if it throws', async () => { (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster .getSettings as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - const resp = await routeDependencies.router.getHandler({ - method: 'get', - pathPattern: '/api/upgrade_assistant/deprecation_logging', - })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); - - expect(resp.status).toEqual(500); + await expect( + routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/deprecation_logging', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory) + ).rejects.toThrow('scary error!'); }); }); @@ -80,12 +80,12 @@ describe('deprecation logging API', () => { it('returns an error if it throws', async () => { (routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster .putSettings as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/deprecation_logging', - })(routeHandlerContextMock, { body: { isEnabled: false } }, kibanaResponseFactory); - - expect(resp.status).toEqual(500); + await expect( + routeDependencies.router.getHandler({ + method: 'put', + pathPattern: '/api/upgrade_assistant/deprecation_logging', + })(routeHandlerContextMock, { body: { isEnabled: false } }, kibanaResponseFactory) + ).rejects.toThrow('scary error!'); }); }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts index 298a87d962ff60..8b427c6443ac88 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts @@ -30,12 +30,8 @@ export function registerDeprecationLoggingRoutes({ router }: RouteDependencies) request, response ) => { - try { - const result = await getDeprecationLoggingStatus(client); - return response.ok({ body: result }); - } catch (e) { - return response.internalError({ body: e }); - } + const result = await getDeprecationLoggingStatus(client); + return response.ok({ body: result }); } ) ); @@ -59,14 +55,10 @@ export function registerDeprecationLoggingRoutes({ router }: RouteDependencies) request, response ) => { - try { - const { isEnabled } = request.body as { isEnabled: boolean }; - return response.ok({ - body: await setDeprecationLogging(client, isEnabled), - }); - } catch (e) { - return response.internalError({ body: e }); - } + const { isEnabled } = request.body as { isEnabled: boolean }; + return response.ok({ + body: await setDeprecationLogging(client, isEnabled), + }); } ) ); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts index aa8e42803121ca..d36f8225ffa3b2 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts @@ -66,7 +66,7 @@ const mapAnyErrorToKibanaHttpResponse = (e: any) => { return kibanaResponseFactory.notFound({ body: e.message }); case CannotCreateIndex: case ReindexTaskCannotBeDeleted: - return kibanaResponseFactory.internalError({ body: e.message }); + throw e; case ReindexTaskFailed: // Bad data return kibanaResponseFactory.customError({ body: e.message, statusCode: 422 }); @@ -78,7 +78,7 @@ const mapAnyErrorToKibanaHttpResponse = (e: any) => { // nothing matched } } - return kibanaResponseFactory.internalError({ body: e }); + throw e; }; export function registerReindexIndicesRoutes( diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts index 96c056ebe5ee8e..05ad542ec9c000 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts @@ -92,20 +92,20 @@ describe('Upgrade Assistant Telemetry API', () => { it('returns an error if it throws', async () => { (upsertUIOpenOption as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_open', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - overview: false, - }, - }), - kibanaResponseFactory - ); - - expect(resp.status).toEqual(500); + await expect( + routeDependencies.router.getHandler({ + method: 'put', + pathPattern: '/api/upgrade_assistant/stats/ui_open', + })( + routeHandlerContextMock, + createRequestMock({ + body: { + overview: false, + }, + }), + kibanaResponseFactory + ) + ).rejects.toThrowError('scary error!'); }); }); @@ -118,7 +118,7 @@ describe('Upgrade Assistant Telemetry API', () => { stop: false, }; - (upsertUIReindexOption as jest.Mock).mockRejectedValue(returnPayload); + (upsertUIReindexOption as jest.Mock).mockResolvedValue(returnPayload); const resp = await routeDependencies.router.getHandler({ method: 'put', @@ -144,7 +144,7 @@ describe('Upgrade Assistant Telemetry API', () => { stop: true, }; - (upsertUIReindexOption as jest.Mock).mockRejectedValue(returnPayload); + (upsertUIReindexOption as jest.Mock).mockResolvedValue(returnPayload); const resp = await routeDependencies.router.getHandler({ method: 'put', @@ -168,20 +168,20 @@ describe('Upgrade Assistant Telemetry API', () => { it('returns an error if it throws', async () => { (upsertUIReindexOption as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_reindex', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - start: false, - }, - }), - kibanaResponseFactory - ); - - expect(resp.status).toEqual(500); + await expect( + routeDependencies.router.getHandler({ + method: 'put', + pathPattern: '/api/upgrade_assistant/stats/ui_reindex', + })( + routeHandlerContextMock, + createRequestMock({ + body: { + start: false, + }, + }), + kibanaResponseFactory + ) + ).rejects.toThrowError('scary error!'); }); }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts index e7c8843c49750b..d24c384f7f0f35 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts @@ -24,18 +24,14 @@ export function registerTelemetryRoutes({ router, getSavedObjectsService }: Rout }, async (ctx, request, response) => { const { cluster, indices, overview } = request.body; - try { - return response.ok({ - body: await upsertUIOpenOption({ - savedObjects: getSavedObjectsService(), - cluster, - indices, - overview, - }), - }); - } catch (e) { - return response.internalError({ body: e }); - } + return response.ok({ + body: await upsertUIOpenOption({ + savedObjects: getSavedObjectsService(), + cluster, + indices, + overview, + }), + }); } ); @@ -53,19 +49,15 @@ export function registerTelemetryRoutes({ router, getSavedObjectsService }: Rout }, async (ctx, request, response) => { const { close, open, start, stop } = request.body; - try { - return response.ok({ - body: await upsertUIReindexOption({ - savedObjects: getSavedObjectsService(), - close, - open, - start, - stop, - }), - }); - } catch (e) { - return response.internalError({ body: e }); - } + return response.ok({ + body: await upsertUIReindexOption({ + savedObjects: getSavedObjectsService(), + close, + open, + start, + stop, + }), + }); } ); } diff --git a/x-pack/plugins/uptime/server/rest_api/create_route_with_auth.ts b/x-pack/plugins/uptime/server/rest_api/create_route_with_auth.ts index 632bf7423f8415..8b6add27f889a7 100644 --- a/x-pack/plugins/uptime/server/rest_api/create_route_with_auth.ts +++ b/x-pack/plugins/uptime/server/rest_api/create_route_with_auth.ts @@ -39,7 +39,7 @@ export const createRouteWithAuth = ( case 403: return response.forbidden({ body: { message } }); default: - return response.internalError(); + throw new Error('Failed to validate the license'); } }; 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 6b291a3be9dcde..24e501a1bddb83 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 @@ -26,34 +26,22 @@ export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute) => ({ esClient: esClient.asCurrentUser, }); - try { - const res = await uptimeRoute.handler({ - uptimeEsClient, - savedObjectsClient, - context, - request, - response, - }); - - if (res instanceof KibanaResponse) { - return res; - } - - return response.ok({ - body: { - ...res, - }, - }); - } catch (e) { - // please don't remove this, this will be really helpful during debugging - /* eslint-disable-next-line no-console */ - console.error(e); + const res = await uptimeRoute.handler({ + uptimeEsClient, + savedObjectsClient, + context, + request, + response, + }); - return response.internalError({ - body: { - message: e.message, - }, - }); + if (res instanceof KibanaResponse) { + return res; } + + return response.ok({ + body: { + ...res, + }, + }); }, }); diff --git a/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts b/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts index d4fb0016aada02..b234bed9f7d4dc 100644 --- a/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts @@ -86,7 +86,7 @@ export function registerGetRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts b/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts index f6803767d89ff1..0882fc3a65027a 100644 --- a/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts @@ -57,7 +57,7 @@ export function registerListFieldsRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts b/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts index 8ab40b346e9794..629f29734c6031 100644 --- a/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts @@ -69,7 +69,7 @@ export function registerLoadHistoryRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts b/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts index 5e289edecefffd..ef5c7c6177ce97 100644 --- a/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts @@ -36,7 +36,7 @@ export function registerLoadRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts index f7ef97c151b2fb..1afec0ada91043 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts @@ -61,7 +61,7 @@ export function registerAcknowledgeRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts index 6d0a2a70850255..85d1d0c51f0b3f 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts @@ -57,7 +57,7 @@ export function registerActivateRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts index 89497bd092f320..071c9d17beee17 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts @@ -57,7 +57,7 @@ export function registerDeactivateRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts index c1e36f5d9c62ff..ebf5b41bc589c4 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts @@ -44,7 +44,7 @@ export function registerDeleteRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts index f8eb3df6126303..e2078ac5cc1d94 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts @@ -74,7 +74,7 @@ export function registerExecuteRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts index ac19d0f71e31c4..cafcf81511a4fa 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts @@ -94,7 +94,7 @@ export function registerHistoryRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts index 350831d32814cf..bba60cf93054cc 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts @@ -61,7 +61,7 @@ export function registerLoadRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts index 9b5d3b4615c07d..b4a219979e6502 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts @@ -62,7 +62,7 @@ export function registerSaveRoute(deps: RouteDependencies) { } catch (e) { const es404 = isEsError(e) && e.statusCode === 404; if (!es404) { - return response.internalError({ body: e }); + throw e; } // Else continue... } @@ -97,7 +97,7 @@ export function registerSaveRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts index 3b2050bff15b59..0310d7eed9d344 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts @@ -62,7 +62,7 @@ export function registerVisualizeRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts b/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts index fad293715b9bb8..631f6fdcb0903d 100644 --- a/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts @@ -51,12 +51,8 @@ export function registerDeleteRoute(deps: RouteDependencies) { }, }, licensePreRoutingFactory(deps, async (ctx, request, response) => { - try { - const results = await deleteWatches(ctx.watcher!.client, request.body.watchIds); - return response.ok({ body: { results } }); - } catch (e) { - return response.internalError({ body: e }); - } + const results = await deleteWatches(ctx.watcher!.client, request.body.watchIds); + return response.ok({ body: { results } }); }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts b/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts index f1119219dae3cd..6a4e85800fa8d5 100644 --- a/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts @@ -75,7 +75,7 @@ export function registerListRoute(deps: RouteDependencies) { } // Case: default - return response.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/xpack_legacy/server/routes/settings.ts b/x-pack/plugins/xpack_legacy/server/routes/settings.ts index b29c52cd011c48..9117637b70bee6 100644 --- a/x-pack/plugins/xpack_legacy/server/routes/settings.ts +++ b/x-pack/plugins/xpack_legacy/server/routes/settings.ts @@ -53,7 +53,7 @@ export function registerSettingsRoute({ | KibanaSettingsCollector | undefined; if (!settingsCollector) { - return res.internalError(); + throw new Error('The settings collector is not registered'); } const settings = diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/server/plugin.ts index 18e813d30659b1..778c87f0ed03ec 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/server/plugin.ts @@ -42,23 +42,19 @@ export class FixturePlugin implements Plugin, res: KibanaResponseFactory ): Promise> { - try { - let namespace: string | undefined; - if (spaces && req.body.spaceId) { - namespace = spaces.spacesService.spaceIdToNamespace(req.body.spaceId); - } - const [, { encryptedSavedObjects }] = await core.getStartServices(); - await encryptedSavedObjects - .getClient({ - includedHiddenTypes: ['alert', 'action'], - }) - .getDecryptedAsInternalUser(req.body.type, req.body.id, { - namespace, - }); - return res.ok({ body: { success: true } }); - } catch (err) { - return res.internalError({ body: err }); + let namespace: string | undefined; + if (spaces && req.body.spaceId) { + namespace = spaces.spacesService.spaceIdToNamespace(req.body.spaceId); } + const [, { encryptedSavedObjects }] = await core.getStartServices(); + await encryptedSavedObjects + .getClient({ + includedHiddenTypes: ['alert', 'action'], + }) + .getDecryptedAsInternalUser(req.body.type, req.body.id, { + namespace, + }); + return res.ok({ body: { success: true } }); } ); } diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts index 7eacc9ba6f0cb3..d9e362a99e6488 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts @@ -66,7 +66,7 @@ export function defineRoutes(core: CoreSetup) { const user = await security.authc.getCurrentUser(req); if (!user) { - return res.internalError({}); + throw new Error('Failed to get the current user'); } // Create an API key using the new grant API - in this case the Kibana system user is creating the @@ -78,7 +78,7 @@ export function defineRoutes(core: CoreSetup) { }); if (!createAPIKeyResult) { - return res.internalError({}); + throw new Error('Failed to grant an API Key'); } const result = await savedObjectsWithAlerts.update( diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts index 57beb40b164592..7213beb2b49a54 100644 --- a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts @@ -71,20 +71,16 @@ export function initRoutes( req: KibanaRequest, res: KibanaResponseFactory ): Promise> { - try { - const taskManager = await taskManagerStart; - const { task: taskFields } = req.body; - const task = { - ...taskFields, - scope: [scope], - }; + const taskManager = await taskManagerStart; + const { task: taskFields } = req.body; + const task = { + ...taskFields, + scope: [scope], + }; - const taskResult = await taskManager.schedule(task, { req }); + const taskResult = await taskManager.schedule(task, { req }); - return res.ok({ body: taskResult }); - } catch (err) { - return res.internalError({ body: err }); - } + return res.ok({ body: taskResult }); } ); From 619a65822766282d7ecb50c106eeb6359b34d44b Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 18 Feb 2021 17:33:16 +0000 Subject: [PATCH 04/43] chore(NA): setup renovate for the new 7.13 (#91859) --- renovate.json5 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/renovate.json5 b/renovate.json5 index 52d7a06c88339a..f8fa3b916946f8 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -38,7 +38,7 @@ packageNames: ['@elastic/charts'], reviewers: ['markov00'], matchBaseBranches: ['master'], - labels: ['release_note:skip', 'v8.0.0', 'v7.12.0'], + labels: ['release_note:skip', 'v8.0.0', 'v7.13.0'], enabled: true, }, { @@ -54,7 +54,7 @@ packageNames: ['@elastic/elasticsearch'], reviewers: ['team:kibana-operations'], matchBaseBranches: ['7.x'], - labels: ['release_note:skip', 'v7.12.0', 'Team:Operations', 'backport:skip'], + labels: ['release_note:skip', 'v7.13.0', 'Team:Operations', 'backport:skip'], enabled: true, }, { From 60e63aa53b55e68af6be31e7a44a0f94f5c2c4e0 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 18 Feb 2021 17:40:25 +0000 Subject: [PATCH 05/43] [ML] Switching to new find file structure endpoint (#91802) * [ML] Switching to new find file structure endpoint * js client change --- package.json | 2 +- x-pack/plugins/ml/server/lib/ml_client/ml_client.ts | 3 --- x-pack/plugins/ml/server/lib/ml_client/types.ts | 1 - .../file_data_visualizer/file_data_visualizer.ts | 8 +++++--- .../plugins/ml/server/routes/file_data_visualizer.ts | 10 +++++----- yarn.lock | 8 ++++---- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 96db1977237d1c..a65c12fce46990 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "@babel/core": "^7.12.10", "@babel/runtime": "^7.12.5", "@elastic/datemath": "link:packages/elastic-datemath", - "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary", + "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.2", "@elastic/ems-client": "7.12.0", "@elastic/eui": "31.7.0", "@elastic/filesaver": "1.1.2", diff --git a/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts b/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts index d075f0cc4e6605..278dd19f74acc3 100644 --- a/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts +++ b/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts @@ -196,9 +196,6 @@ export function getMlClient( await jobIdsCheck('data-frame-analytics', p); return mlClient.explainDataFrameAnalytics(...p); }, - async findFileStructure(...p: Parameters) { - return mlClient.findFileStructure(...p); - }, async flushJob(...p: Parameters) { await jobIdsCheck('anomaly-detector', p); return mlClient.flushJob(...p); diff --git a/x-pack/plugins/ml/server/lib/ml_client/types.ts b/x-pack/plugins/ml/server/lib/ml_client/types.ts index 6618e19dfe2663..7ff1acf4ac0ce2 100644 --- a/x-pack/plugins/ml/server/lib/ml_client/types.ts +++ b/x-pack/plugins/ml/server/lib/ml_client/types.ts @@ -29,7 +29,6 @@ export type MlClientParams = | Parameters | Parameters | Parameters - | Parameters | Parameters | Parameters | Parameters diff --git a/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts b/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts index 2dd3373f2e42f5..6e57e997e5f008 100644 --- a/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts @@ -5,20 +5,22 @@ * 2.0. */ +import { IScopedClusterClient } from 'kibana/server'; import { AnalysisResult, FormattedOverrides, InputOverrides, FindFileStructureResponse, } from '../../../common/types/file_datavisualizer'; -import type { MlClient } from '../../lib/ml_client'; export type InputData = any[]; -export function fileDataVisualizerProvider(mlClient: MlClient) { +export function fileDataVisualizerProvider(client: IScopedClusterClient) { async function analyzeFile(data: InputData, overrides: InputOverrides): Promise { overrides.explain = overrides.explain === undefined ? 'true' : overrides.explain; - const { body } = await mlClient.findFileStructure({ + const { + body, + } = await client.asInternalUser.textStructure.findStructure({ body: data, ...overrides, }); diff --git a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts index fa56a6854de62d..6b200c59f57d5a 100644 --- a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { IScopedClusterClient } from 'kibana/server'; import { schema } from '@kbn/config-schema'; import { MAX_FILE_SIZE_BYTES } from '../../../file_upload/common'; import { InputOverrides } from '../../common/types/file_datavisualizer'; @@ -13,10 +14,9 @@ import { InputData, fileDataVisualizerProvider } from '../models/file_data_visua import { RouteInitialization } from '../types'; import { analyzeFileQuerySchema } from './schemas/file_data_visualizer_schema'; -import type { MlClient } from '../lib/ml_client'; -function analyzeFiles(mlClient: MlClient, data: InputData, overrides: InputOverrides) { - const { analyzeFile } = fileDataVisualizerProvider(mlClient); +function analyzeFiles(client: IScopedClusterClient, data: InputData, overrides: InputOverrides) { + const { analyzeFile } = fileDataVisualizerProvider(client); return analyzeFile(data, overrides); } @@ -48,9 +48,9 @@ export function fileDataVisualizerRoutes({ router, routeGuard }: RouteInitializa tags: ['access:ml:canFindFileStructure'], }, }, - routeGuard.basicLicenseAPIGuard(async ({ mlClient, request, response }) => { + routeGuard.basicLicenseAPIGuard(async ({ client, request, response }) => { try { - const result = await analyzeFiles(mlClient, request.body, request.query); + const result = await analyzeFiles(client, request.body, request.query); return response.ok({ body: result }); } catch (e) { return response.customError(wrapError(e)); diff --git a/yarn.lock b/yarn.lock index 836c067c73324a..4738925e44dfbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2167,10 +2167,10 @@ version "0.0.0" uid "" -"@elastic/elasticsearch@npm:@elastic/elasticsearch-canary@^8.0.0-canary": - version "8.0.0-canary.1" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch-canary/-/elasticsearch-canary-8.0.0-canary.1.tgz#5cd0eda62531b71af66a08da6c3cebc26a73d4c0" - integrity sha512-VhQ42wH+0OGmHSlc4It3bqGTL7mLuC2RIionJZBIuY5P6lwUMz7goelfyfTHoo+LStxz5QQ8Zt2xcnAnShTBJg== +"@elastic/elasticsearch@npm:@elastic/elasticsearch-canary@^8.0.0-canary.2": + version "8.0.0-canary.2" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch-canary/-/elasticsearch-canary-8.0.0-canary.2.tgz#476e22bc90fc4f422f7195f693fdcddb7f8e1897" + integrity sha512-xYdVJ1MCAprVxd0rqmkBVof7I0N+e6VBCcr0UOwEYjvpQJTvu6PPQROBAAmtAAgvIKs4a8HmpArGgu5QJUnNjw== dependencies: debug "^4.1.1" hpagent "^0.1.1" From 18db413083d7f737e60221df6ae3219daad96a47 Mon Sep 17 00:00:00 2001 From: Michail Yasonik Date: Thu, 18 Feb 2021 12:43:24 -0500 Subject: [PATCH 06/43] Adding automated a11y tests for Canvas (#91571) --- x-pack/plugins/canvas/i18n/components.ts | 6 ++ .../confirm_modal/confirm_modal.tsx | 1 + .../workpad_loader/workpad_loader.js | 4 +- .../workpad_manager/workpad_manager.js | 5 +- .../workpad_templates.stories.storyshot | 1 + .../workpad_templates/workpad_templates.tsx | 1 + x-pack/test/accessibility/apps/canvas.ts | 60 +++++++++++++++++++ x-pack/test/accessibility/config.ts | 1 + 8 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 x-pack/test/accessibility/apps/canvas.ts diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index 88ea8700bd464a..afd3d1408e1f1a 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -1752,6 +1752,12 @@ export const ComponentStrings = { description: 'This column in the table contains the date/time the workpad was last updated.', }), + getTableActionsColumnTitle: () => + i18n.translate('xpack.canvas.workpadLoader.table.actionsColumnTitle', { + defaultMessage: 'Actions', + description: + 'This column in the table contains the actions that can be taken on a workpad.', + }), }, WorkpadManager: { getModalTitle: () => diff --git a/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx b/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx index 521ced0d731f2c..3156b14f209f1a 100644 --- a/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx +++ b/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx @@ -49,6 +49,7 @@ export const ConfirmModal: FunctionComponent = (props) => { cancelButtonText={cancelButtonText} defaultFocusedButton="confirm" buttonColor="danger" + data-test-subj="canvasConfirmModal" > {message} diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js index 5a33b25399f77e..25c17fabe9fad8 100644 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js +++ b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js @@ -213,7 +213,7 @@ export class WorkpadLoader extends React.PureComponent { width: '20%', render: (date) => this.props.formatDate(date), }, - { name: '', actions, width: '5%' }, + { name: strings.getTableActionsColumnTitle(), actions, width: '100px' }, ]; const sorting = { @@ -310,6 +310,7 @@ export class WorkpadLoader extends React.PureComponent { onClick={this.openRemoveConfirm} disabled={!canUserWrite} aria-label={strings.getDeleteButtonAriaLabel(selectedWorkpads.length)} + data-test-subj="deleteWorkpadButton" > {strings.getDeleteButtonLabel(selectedWorkpads.length)} @@ -331,6 +332,7 @@ export class WorkpadLoader extends React.PureComponent { display="default" compressed className="canvasWorkpad__upload--compressed" + aria-label={strings.getFilePickerPlaceholder()} initialPromptText={strings.getFilePickerPlaceholder()} onChange={([file]) => uploadWorkpad(file, this.onUpload, this.props.notify)} accept="application/json" diff --git a/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js b/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js index 3f128a1758c3f9..8055be32ac481a 100644 --- a/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js +++ b/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js @@ -37,6 +37,7 @@ export const WorkpadManager = ({ onClose }) => { { id: 'workpadTemplates', name: strings.getWorkpadTemplatesTabLabel(), + 'data-test-subj': 'workpadTemplates', content: ( @@ -50,7 +51,9 @@ export const WorkpadManager = ({ onClose }) => { - {strings.getModalTitle()} + +

{strings.getModalTitle()}

+
diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot index 489827246e998a..e984aae3356366 100644 --- a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot @@ -95,6 +95,7 @@ exports[`Storyshots components/WorkpadTemplates default 1`] = ` />
{rows.length > 0 && ( diff --git a/x-pack/test/accessibility/apps/canvas.ts b/x-pack/test/accessibility/apps/canvas.ts new file mode 100644 index 00000000000000..c802d62b05bf94 --- /dev/null +++ b/x-pack/test/accessibility/apps/canvas.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const a11y = getService('a11y'); + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + const { common } = getPageObjects(['common']); + + describe('Canvas', () => { + before(async () => { + await esArchiver.load('canvas/default'); + await common.navigateToApp('canvas'); + }); + + it('loads workpads', async function () { + await retry.waitFor( + 'canvas workpads visible', + async () => await testSubjects.exists('canvasWorkpadLoaderTable') + ); + await a11y.testAppSnapshot(); + }); + + it('provides bulk actions', async function () { + await testSubjects.click('checkboxSelectAll'); + await retry.waitFor( + 'canvas bulk actions visible', + async () => await testSubjects.exists('deleteWorkpadButton') + ); + await a11y.testAppSnapshot(); + }); + + it('can delete workpads', async function () { + await testSubjects.click('deleteWorkpadButton'); + await retry.waitFor( + 'canvas delete modal visible', + async () => await testSubjects.exists('canvasConfirmModal') + ); + await a11y.testAppSnapshot(); + }); + + it('can navigate to templates', async function () { + await testSubjects.click('confirmModalCancelButton'); // close modal from previous test + + await testSubjects.click('workpadTemplates'); + await retry.waitFor( + 'canvas templates visible', + async () => await testSubjects.exists('canvasTemplatesTable') + ); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index bfd12c4eee6e7d..b014f672ed5a64 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -32,6 +32,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/ml'), require.resolve('./apps/lens'), require.resolve('./apps/upgrade_assistant'), + require.resolve('./apps/canvas'), ], pageObjects, From df8f2b1412f09fd16bea0ea3f3ee34fcf9d8afd6 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Thu, 18 Feb 2021 10:48:42 -0700 Subject: [PATCH 07/43] [Maps] Fix issue preventing WebGL warning message from appearing (#91069) --- x-pack/plugins/maps/public/reducers/store.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/x-pack/plugins/maps/public/reducers/store.js b/x-pack/plugins/maps/public/reducers/store.js index 76199de5b24c92..f1020b2d6cdb69 100644 --- a/x-pack/plugins/maps/public/reducers/store.js +++ b/x-pack/plugins/maps/public/reducers/store.js @@ -10,7 +10,6 @@ import thunk from 'redux-thunk'; import { ui, DEFAULT_MAP_UI_STATE } from './ui'; import { map, DEFAULT_MAP_STATE } from './map'; // eslint-disable-line import/named import { nonSerializableInstances } from './non_serializable_instances'; -import { MAP_DESTROYED } from '../actions'; export const DEFAULT_MAP_STORE_STATE = { ui: { ...DEFAULT_MAP_UI_STATE }, @@ -26,16 +25,7 @@ export function createMapStore() { nonSerializableInstances, }); - const rootReducer = (state, action) => { - // Reset store on map destroyed - if (action.type === MAP_DESTROYED) { - state = undefined; - } - - return combinedReducers(state, action); - }; - const storeConfig = {}; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - return createStore(rootReducer, storeConfig, composeEnhancers(...enhancers)); + return createStore(combinedReducers, storeConfig, composeEnhancers(...enhancers)); } From eddf1c94b1c76492e619bef7557a04e8cbbea7d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 18 Feb 2021 18:00:43 +0000 Subject: [PATCH 08/43] Index pattern field editor (#88995) Index pattern field editor --- .i18nrc.json | 1 + docs/developer/plugin-list.asciidoc | 4 + .../src/painless/diagnostics_adapter.ts | 13 + packages/kbn-monaco/src/painless/index.ts | 10 +- packages/kbn-monaco/src/painless/language.ts | 11 +- packages/kbn-optimizer/limits.yml | 3 +- .../public/doc_links/doc_links_service.ts | 1 + .../data/common/index_patterns/constants.ts | 9 + .../index_pattern_field.test.ts.snap | 4 +- .../fields/index_pattern_field.test.ts | 4 +- .../fields/index_pattern_field.ts | 8 +- .../data/common/index_patterns/index.ts | 1 + .../__snapshots__/index_pattern.test.ts.snap | 6 +- .../index_patterns/index_pattern.ts | 3 +- .../index_patterns/index_patterns.ts | 7 +- .../data/common/index_patterns/types.ts | 7 +- .../forms/hook_form_lib/hooks/use_form.ts | 3 +- .../index_pattern_field_editor/README.md | 69 +++++ .../index_pattern_field_editor/jest.config.js | 13 + .../index_pattern_field_editor/kibana.json | 9 + .../public/assets/icons/LICENSE.txt | 20 ++ .../public/assets/icons/cv.png | Bin 0 -> 802 bytes .../public/assets/icons/de.png | Bin 0 -> 124 bytes .../public/assets/icons/go.png | Bin 0 -> 1938 bytes .../public/assets/icons/ne.png | Bin 0 -> 336 bytes .../public/assets/icons/ni.png | Bin 0 -> 919 bytes .../public/assets/icons/stop.png | Bin 0 -> 1912 bytes .../public/assets/icons/us.png | Bin 0 -> 1074 bytes .../delete_field_provider.tsx | 129 ++++++++ .../get_delete_provider.tsx | 62 ++++ .../components/delete_field_provider/index.ts | 9 + .../advanced_parameters_section.tsx | 44 +++ .../components/field_editor/constants.ts | 37 +++ .../field_editor/field_editor.test.tsx | 212 +++++++++++++ .../components/field_editor/field_editor.tsx | 286 ++++++++++++++++++ .../form_fields/custom_label_field.tsx | 15 + .../field_editor/form_fields/format_field.tsx | 82 +++++ .../field_editor/form_fields/index.ts | 17 ++ .../form_fields/popularity_field.tsx | 21 ++ .../field_editor/form_fields/script_field.tsx | 227 ++++++++++++++ .../field_editor/form_fields/type_field.tsx | 65 ++++ .../components/field_editor/form_row.tsx | 86 ++++++ .../components/field_editor/form_schema.ts | 119 ++++++++ .../public/components/field_editor/index.ts | 9 + .../public/components/field_editor/lib.ts | 60 ++++ .../field_editor/shadowing_field_warning.tsx | 32 ++ .../field_editor_flyout_content.test.ts | 198 ++++++++++++ .../field_editor_flyout_content.tsx | 253 ++++++++++++++++ .../field_editor_flyout_content_container.tsx | 205 +++++++++++++ .../__snapshots__/format_editor.test.tsx.snap | 25 ++ .../bytes/__snapshots__/bytes.test.tsx.snap | 4 +- .../editors/bytes/bytes.test.tsx | 0 .../editors/bytes/bytes.ts | 0 .../editors/bytes/index.ts | 0 .../color/__snapshots__/color.test.tsx.snap | 30 +- .../editors/color/color.test.tsx | 2 +- .../editors/color/color.tsx | 20 +- .../editors/color/index.ts | 0 .../date/__snapshots__/date.test.tsx.snap | 4 +- .../editors/date/date.test.tsx | 0 .../field_format_editor/editors/date/date.tsx | 4 +- .../field_format_editor/editors/date/index.ts | 0 .../__snapshots__/date_nanos.test.tsx.snap | 4 +- .../editors/date_nanos/date_nanos.test.tsx | 2 +- .../editors/date_nanos/date_nanos.tsx | 4 +- .../editors/date_nanos/index.ts | 0 .../__snapshots__/default.test.tsx.snap | 0 .../editors/default/default.test.tsx | 0 .../editors/default/default.tsx | 8 +- .../editors/default/index.ts | 0 .../__snapshots__/duration.test.tsx.snap | 12 +- .../editors/duration/duration.test.tsx | 0 .../editors/duration/duration.tsx | 10 +- .../editors/duration/index.tsx | 0 .../field_format_editor/editors/index.ts | 0 .../number/__snapshots__/number.test.tsx.snap | 4 +- .../editors/number/index.ts | 0 .../editors/number/number.test.tsx | 0 .../editors/number/number.tsx | 4 +- .../__snapshots__/percent.test.tsx.snap | 4 +- .../editors/percent/index.ts | 0 .../editors/percent/percent.test.tsx | 2 +- .../editors/percent/percent.tsx | 0 .../__snapshots__/static_lookup.test.tsx.snap | 16 +- .../editors/static_lookup/index.ts | 0 .../static_lookup/static_lookup.test.tsx | 2 +- .../editors/static_lookup/static_lookup.tsx | 16 +- .../string/__snapshots__/string.test.tsx.snap | 2 +- .../editors/string/index.ts | 0 .../editors/string/string.test.tsx | 0 .../editors/string/string.tsx | 2 +- .../__snapshots__/truncate.test.tsx.snap | 2 +- .../editors/truncate/index.ts | 0 .../editors/truncate/sample.ts | 0 .../editors/truncate/truncate.test.tsx | 0 .../editors/truncate/truncate.tsx | 2 +- .../url/__snapshots__/url.test.tsx.snap | 36 ++- .../field_format_editor/editors/url/index.ts | 0 .../editors/url/url.test.tsx | 36 +-- .../field_format_editor/editors/url/url.tsx | 79 ++--- .../field_format_editor.tsx | 186 ++++++++++++ .../format_editor.test.tsx | 63 ++++ .../field_format_editor/format_editor.tsx | 67 ++++ .../components/field_format_editor/index.ts | 10 + .../__snapshots__/samples.test.tsx.snap | 2 +- .../field_format_editor/samples/index.ts | 0 .../field_format_editor/samples/samples.scss | 0 .../samples/samples.test.tsx | 0 .../field_format_editor/samples/samples.tsx | 8 +- .../components/field_format_editor/types.ts | 14 + .../public/components/index.ts | 20 ++ .../public/constants.ts | 9 + .../public/index.ts | 32 ++ .../public/lib/documentation.ts | 21 ++ .../public/lib/index.ts | 13 + .../lib/runtime_field_validation.test.ts | 165 ++++++++++ .../public/lib/runtime_field_validation.ts | 116 +++++++ .../public/lib/serialization.ts | 28 ++ .../public/mocks.ts | 46 +++ .../public/open_editor.tsx | 119 ++++++++ .../public/plugin.test.tsx | 114 +++++++ .../public/plugin.ts | 60 ++++ .../field_format_editors.ts | 2 +- .../service/field_format_editors/index.ts | 0 .../public/service/format_editor_service.ts | 72 +++++ .../public/service/index.ts | 9 + .../public/shared_imports.ts | 36 +++ .../public/test_utils/helpers.ts | 27 ++ .../public/test_utils/index.ts | 13 + .../public/test_utils/mocks.ts | 24 ++ .../public/test_utils/setup_environment.tsx | 80 +++++ .../public/test_utils/test_utils.ts | 11 + .../public/types.ts | 63 ++++ .../index_pattern_field_editor/tsconfig.json | 20 ++ .../index_pattern_management/kibana.json | 2 +- .../create_index_pattern_wizard.test.tsx.snap | 10 + .../edit_index_pattern/edit_index_pattern.tsx | 19 +- .../indexed_fields_table.test.tsx.snap | 35 ++- .../table/__snapshots__/table.test.tsx.snap | 32 +- .../components/table/table.test.tsx | 65 ++-- .../components/table/table.tsx | 41 ++- .../indexed_fields_table.test.tsx | 13 +- .../indexed_fields_table.tsx | 9 +- .../indexed_fields_table/types.ts | 2 + .../edit_index_pattern/tabs/tabs.tsx | 111 +++++-- .../label_template_flyout.test.tsx.snap | 109 ------- .../url_template_flyout.test.tsx.snap | 114 ------- .../url/label_template_flyout.test.tsx | 24 -- .../editors/url/label_template_flyout.tsx | 142 --------- .../editors/url/url_template_flyout.test.tsx | 24 -- .../editors/url/url_template_flyout.tsx | 112 ------- .../field_format_editor.test.tsx | 2 +- .../field_format_editor.tsx | 2 +- .../components/field_format_editor/index.ts | 1 - .../components/field_editor/field_editor.tsx | 4 +- .../index_pattern_management/public/index.ts | 2 - .../mount_management_section.tsx | 4 +- .../index_pattern_management/public/mocks.ts | 23 +- .../index_pattern_management/public/plugin.ts | 2 + .../index_pattern_management_service.ts | 36 --- .../index_pattern_management/public/types.ts | 3 + .../index_pattern_management/tsconfig.json | 2 + .../management/_handle_version_conflict.js | 6 + .../apps/management/_index_pattern_filter.js | 8 +- .../management/_index_pattern_popularity.js | 8 +- .../management/_index_pattern_results_sort.js | 32 +- test/functional/apps/visualize/_tag_cloud.ts | 6 + test/functional/page_objects/settings_page.ts | 2 +- tsconfig.json | 1 + .../runtime_field_form/runtime_field_form.tsx | 2 +- .../translations/translations/ja-JP.json | 110 +++---- .../translations/translations/zh-CN.json | 110 +++---- .../apps/rollup_job/hybrid_index_pattern.js | 2 +- 173 files changed, 4358 insertions(+), 1034 deletions(-) create mode 100644 src/plugins/data/common/index_patterns/constants.ts create mode 100644 src/plugins/index_pattern_field_editor/README.md create mode 100644 src/plugins/index_pattern_field_editor/jest.config.js create mode 100644 src/plugins/index_pattern_field_editor/kibana.json create mode 100644 src/plugins/index_pattern_field_editor/public/assets/icons/LICENSE.txt create mode 100644 src/plugins/index_pattern_field_editor/public/assets/icons/cv.png create mode 100644 src/plugins/index_pattern_field_editor/public/assets/icons/de.png create mode 100644 src/plugins/index_pattern_field_editor/public/assets/icons/go.png create mode 100644 src/plugins/index_pattern_field_editor/public/assets/icons/ne.png create mode 100644 src/plugins/index_pattern_field_editor/public/assets/icons/ni.png create mode 100644 src/plugins/index_pattern_field_editor/public/assets/icons/stop.png create mode 100644 src/plugins/index_pattern_field_editor/public/assets/icons/us.png create mode 100644 src/plugins/index_pattern_field_editor/public/components/delete_field_provider/delete_field_provider.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/delete_field_provider/get_delete_provider.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/delete_field_provider/index.ts create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/advanced_parameters_section.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/constants.ts create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/custom_label_field.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.ts create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/popularity_field.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/type_field.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/form_row.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.ts create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/index.ts create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor/shadowing_field_warning.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_format_editor/__snapshots__/format_editor.test.tsx.snap rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap (92%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/bytes/bytes.test.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/bytes/bytes.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/bytes/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap (87%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/color/color.test.tsx (96%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/color/color.tsx (89%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/color/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap (93%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/date/date.test.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/date/date.tsx (94%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/date/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap (93%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx (95%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/date_nanos/date_nanos.tsx (94%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/date_nanos/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/default/__snapshots__/default.test.tsx.snap (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/default/default.test.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/default/default.tsx (92%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/default/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap (93%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/duration/duration.test.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/duration/duration.tsx (93%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/duration/index.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap (92%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/number/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/number/number.test.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/number/number.tsx (94%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap (92%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/percent/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/percent/percent.test.tsx (95%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/percent/percent.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap (88%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/static_lookup/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx (96%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/static_lookup/static_lookup.tsx (88%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/string/__snapshots__/string.test.tsx.snap (96%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/string/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/string/string.test.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/string/string.tsx (96%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap (96%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/truncate/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/truncate/sample.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/truncate/truncate.test.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/truncate/truncate.tsx (96%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap (92%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/url/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/url/url.test.tsx (75%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/editors/url/url.tsx (74%) create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.test.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_format_editor/index.ts rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap (96%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/samples/index.ts (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/samples/samples.scss (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/samples/samples.test.tsx (100%) rename src/plugins/{index_pattern_management/public/components/field_editor => index_pattern_field_editor/public}/components/field_format_editor/samples/samples.tsx (87%) create mode 100644 src/plugins/index_pattern_field_editor/public/components/field_format_editor/types.ts create mode 100644 src/plugins/index_pattern_field_editor/public/components/index.ts create mode 100644 src/plugins/index_pattern_field_editor/public/constants.ts create mode 100644 src/plugins/index_pattern_field_editor/public/index.ts create mode 100644 src/plugins/index_pattern_field_editor/public/lib/documentation.ts create mode 100644 src/plugins/index_pattern_field_editor/public/lib/index.ts create mode 100644 src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts create mode 100644 src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts create mode 100644 src/plugins/index_pattern_field_editor/public/lib/serialization.ts create mode 100644 src/plugins/index_pattern_field_editor/public/mocks.ts create mode 100644 src/plugins/index_pattern_field_editor/public/open_editor.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/plugin.test.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/plugin.ts rename src/plugins/{index_pattern_management => index_pattern_field_editor}/public/service/field_format_editors/field_format_editors.ts (89%) rename src/plugins/{index_pattern_management => index_pattern_field_editor}/public/service/field_format_editors/index.ts (100%) create mode 100644 src/plugins/index_pattern_field_editor/public/service/format_editor_service.ts create mode 100644 src/plugins/index_pattern_field_editor/public/service/index.ts create mode 100644 src/plugins/index_pattern_field_editor/public/shared_imports.ts create mode 100644 src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts create mode 100644 src/plugins/index_pattern_field_editor/public/test_utils/index.ts create mode 100644 src/plugins/index_pattern_field_editor/public/test_utils/mocks.ts create mode 100644 src/plugins/index_pattern_field_editor/public/test_utils/setup_environment.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/test_utils/test_utils.ts create mode 100644 src/plugins/index_pattern_field_editor/public/types.ts create mode 100644 src/plugins/index_pattern_field_editor/tsconfig.json delete mode 100644 src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap delete mode 100644 src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap delete mode 100644 src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.tsx delete mode 100644 src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx delete mode 100644 src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.test.tsx delete mode 100644 src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx diff --git a/.i18nrc.json b/.i18nrc.json index 0cdcae08e54e0a..efbb5ecc0194e9 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -29,6 +29,7 @@ "maps_legacy": "src/plugins/maps_legacy", "monaco": "packages/kbn-monaco/src", "presentationUtil": "src/plugins/presentation_util", + "indexPatternFieldEditor": "src/plugins/index_pattern_field_editor", "indexPatternManagement": "src/plugins/index_pattern_management", "advancedSettings": "src/plugins/advanced_settings", "kibana_legacy": "src/plugins/kibana_legacy", diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 5564b4cdcf79da..38b053ea127521 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -93,6 +93,10 @@ for use in their own application. |Moves the legacy ui/registry/feature_catalogue module for registering "features" that should be shown in the home page's feature catalogue to a service within a "home" plugin. The feature catalogue refered to here should not be confused with the "feature" plugin for registering features used to derive UI capabilities for feature controls. +|{kib-repo}blob/{branch}/src/plugins/index_pattern_field_editor/README.md[indexPatternFieldEditor] +|The reusable field editor across Kibana! + + |{kib-repo}blob/{branch}/src/plugins/index_pattern_management[indexPatternManagement] |WARNING: Missing README. diff --git a/packages/kbn-monaco/src/painless/diagnostics_adapter.ts b/packages/kbn-monaco/src/painless/diagnostics_adapter.ts index fd08cc9c6b57a2..dc5f1ed95205cf 100644 --- a/packages/kbn-monaco/src/painless/diagnostics_adapter.ts +++ b/packages/kbn-monaco/src/painless/diagnostics_adapter.ts @@ -18,7 +18,12 @@ const toDiagnostics = (error: PainlessError): monaco.editor.IMarkerData => { }; }; +export interface SyntaxErrors { + [modelId: string]: PainlessError[]; +} export class DiagnosticsAdapter { + private errors: SyntaxErrors = {}; + constructor(private worker: WorkerAccessor) { const onModelAdd = (model: monaco.editor.IModel): void => { let handle: any; @@ -55,8 +60,16 @@ export class DiagnosticsAdapter { if (errorMarkers) { const model = monaco.editor.getModel(resource); + this.errors = { + ...this.errors, + [model!.id]: errorMarkers, + }; // Set the error markers and underline them with "Error" severity monaco.editor.setModelMarkers(model!, ID, errorMarkers.map(toDiagnostics)); } } + + public getSyntaxErrors() { + return this.errors; + } } diff --git a/packages/kbn-monaco/src/painless/index.ts b/packages/kbn-monaco/src/painless/index.ts index 5845186776b486..68582097564308 100644 --- a/packages/kbn-monaco/src/painless/index.ts +++ b/packages/kbn-monaco/src/painless/index.ts @@ -8,8 +8,14 @@ import { ID } from './constants'; import { lexerRules, languageConfiguration } from './lexer_rules'; -import { getSuggestionProvider } from './language'; +import { getSuggestionProvider, getSyntaxErrors } from './language'; -export const PainlessLang = { ID, getSuggestionProvider, lexerRules, languageConfiguration }; +export const PainlessLang = { + ID, + getSuggestionProvider, + lexerRules, + languageConfiguration, + getSyntaxErrors, +}; export { PainlessContext, PainlessAutocompleteField } from './types'; diff --git a/packages/kbn-monaco/src/painless/language.ts b/packages/kbn-monaco/src/painless/language.ts index 74199561bc3948..3cb26d970fc7d0 100644 --- a/packages/kbn-monaco/src/painless/language.ts +++ b/packages/kbn-monaco/src/painless/language.ts @@ -13,7 +13,7 @@ import { ID } from './constants'; import { PainlessContext, PainlessAutocompleteField } from './types'; import { PainlessWorker } from './worker'; import { PainlessCompletionAdapter } from './completion_adapter'; -import { DiagnosticsAdapter } from './diagnostics_adapter'; +import { DiagnosticsAdapter, SyntaxErrors } from './diagnostics_adapter'; const workerProxyService = new WorkerProxyService(); const editorStateService = new EditorStateService(); @@ -33,8 +33,15 @@ export const getSuggestionProvider = ( return new PainlessCompletionAdapter(worker, editorStateService); }; +let diagnosticsAdapter: DiagnosticsAdapter; + +// Returns syntax errors for all models by model id +export const getSyntaxErrors = (): SyntaxErrors => { + return diagnosticsAdapter.getSyntaxErrors(); +}; + monaco.languages.onLanguage(ID, async () => { workerProxyService.setup(); - new DiagnosticsAdapter(worker); + diagnosticsAdapter = new DiagnosticsAdapter(worker); }); diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 6d81b39df71135..1a157624d7a8ab 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -33,7 +33,7 @@ pageLoadAssetSize: home: 41661 indexLifecycleManagement: 107090 indexManagement: 140608 - indexPatternManagement: 154222 + indexPatternManagement: 28222 infra: 204800 fleet: 415829 ingestPipelines: 58003 @@ -103,6 +103,7 @@ pageLoadAssetSize: stackAlerts: 29684 presentationUtil: 28545 spacesOss: 18817 + indexPatternFieldEditor: 90489 osquery: 107090 fileUpload: 25664 banners: 17946 diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 937a89e12b7553..77792286d6839f 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -121,6 +121,7 @@ export class DocLinksService { indexPatterns: { loadingData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/tutorial-load-dataset.html`, introduction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-patterns.html`, + fieldFormattersString: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/field-formatters-string.html`, }, addData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/connect-to-elasticsearch.html`, kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`, diff --git a/src/plugins/data/common/index_patterns/constants.ts b/src/plugins/data/common/index_patterns/constants.ts new file mode 100644 index 00000000000000..88309447a8a29c --- /dev/null +++ b/src/plugins/data/common/index_patterns/constants.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; diff --git a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap index 4ef61ec0f25571..6b1d01e5ba1429 100644 --- a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap +++ b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap @@ -14,7 +14,7 @@ Object { }, "count": 1, "esTypes": Array [ - "text", + "keyword", ], "lang": "lang", "name": "name", @@ -49,7 +49,7 @@ Object { "count": 1, "customLabel": undefined, "esTypes": Array [ - "text", + "keyword", ], "format": Object { "id": "number", diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts index 85e20c5a32662e..48342a9e02a2bd 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts @@ -26,7 +26,7 @@ describe('Field', function () { script: 'script', lang: 'lang', count: 1, - esTypes: ['text'], + esTypes: ['text'], // note, this will get replaced by the runtime field type aggregatable: true, filterable: true, searchable: true, @@ -71,7 +71,7 @@ describe('Field', function () { }); it('sets type field when _source field', () => { - const field = getField({ name: '_source' }); + const field = getField({ name: '_source', runtimeField: undefined }); expect(field.type).toEqual('_source'); }); diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index 4a6ee1149d4c6d..e5f4945c9ad6d4 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -7,7 +7,7 @@ */ import type { RuntimeField } from '../types'; -import { KbnFieldType, getKbnFieldType } from '../../kbn_field_types'; +import { KbnFieldType, getKbnFieldType, castEsToKbnFieldTypeName } from '../../kbn_field_types'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import type { IFieldType } from './types'; import { FieldSpec, IndexPattern } from '../..'; @@ -99,11 +99,13 @@ export class IndexPatternField implements IFieldType { } public get type() { - return this.spec.type; + return this.runtimeField?.type + ? castEsToKbnFieldTypeName(this.runtimeField?.type) + : this.spec.type; } public get esTypes() { - return this.spec.esTypes; + return this.runtimeField?.type ? [this.runtimeField?.type] : this.spec.esTypes; } public get scripted() { diff --git a/src/plugins/data/common/index_patterns/index.ts b/src/plugins/data/common/index_patterns/index.ts index 1cea49bcbecd30..7f6249caceb52e 100644 --- a/src/plugins/data/common/index_patterns/index.ts +++ b/src/plugins/data/common/index_patterns/index.ts @@ -12,3 +12,4 @@ export { IndexPatternsService, IndexPatternsContract } from './index_patterns'; export type { IndexPattern } from './index_patterns'; export * from './errors'; export * from './expressions'; +export * from './constants'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap index 4aadddfad3b970..7757e2fdd4584d 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap @@ -565,7 +565,9 @@ Object { "conflictDescriptions": undefined, "count": 0, "customLabel": undefined, - "esTypes": undefined, + "esTypes": Array [ + "keyword", + ], "format": Object { "id": "number", "params": Object { @@ -587,7 +589,7 @@ Object { "searchable": false, "shortDotsEnable": false, "subType": undefined, - "type": undefined, + "type": "string", }, "script date": Object { "aggregatable": true, 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 ca4fee0416ac41..41ce7ba4bab4a1 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 @@ -389,6 +389,8 @@ export class IndexPattern implements IIndexPattern { existingField.runtimeField = undefined; } else { // runtimeField only + this.setFieldCustomLabel(name, null); + this.deleteFieldFormat(name); this.fields.remove(existingField); } } @@ -423,7 +425,6 @@ export class IndexPattern implements IIndexPattern { if (fieldObject) { fieldObject.customLabel = newCustomLabel; - return; } this.setFieldAttrs(fieldName, 'customLabel', newCustomLabel); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 57a452dcb12046..27794094236046 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -415,11 +415,10 @@ export class IndexPatternsService { }, spec.fieldAttrs ); - // APPLY RUNTIME FIELDS + // CREATE RUNTIME FIELDS for (const [key, value] of Object.entries(runtimeFieldMap || {})) { - if (spec.fields[key]) { - spec.fields[key].runtimeField = value; - } else { + // do not create runtime field if mapped field exists + if (!spec.fields[key]) { spec.fields[key] = { name: key, type: castEsToKbnFieldTypeName(value.type), diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 6d7327e7fb38d7..c906b809b08c4c 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -10,15 +10,16 @@ import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notificatio // eslint-disable-next-line import type { SavedObject } from 'src/core/server'; import { IFieldType } from './fields'; +import { RUNTIME_FIELD_TYPES } from './constants'; import { SerializedFieldFormat } from '../../../expressions/common'; import { KBN_FIELD_TYPES, IndexPatternField, FieldFormat } from '..'; export type FieldFormatMap = Record; -const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; -type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; + +export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; export interface RuntimeField { type: RuntimeType; - script: { + script?: { source: string; }; } diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index 40f44f3671f312..181bd9959c1bbd 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -211,11 +211,12 @@ export function useForm( // ---------------------------------- const addField: FormHook['__addField'] = useCallback( (field) => { + const fieldExists = fieldsRefs.current[field.path] !== undefined; fieldsRefs.current[field.path] = field; updateFormDataAt(field.path, field.value); - if (!field.isValidated) { + if (!fieldExists && !field.isValidated) { setIsValid(undefined); // When we submit the form (and set "isSubmitted" to "true"), we validate **all fields**. diff --git a/src/plugins/index_pattern_field_editor/README.md b/src/plugins/index_pattern_field_editor/README.md new file mode 100644 index 00000000000000..10949954cef38f --- /dev/null +++ b/src/plugins/index_pattern_field_editor/README.md @@ -0,0 +1,69 @@ +# Index pattern field editor + +The reusable field editor across Kibana! + +This editor can be used to + +* create or edit a runtime field inside an index pattern. +* edit concrete (mapped) fields. In this case certain functionalities will be disabled like the possibility to change the field _type_ or to set the field _value_. + +## How to use + +You first need to add in your kibana.json the "`indexPatternFieldEditor`" plugin as a required dependency of your plugin. + +You will then receive in the start contract of the indexPatternFieldEditor plugin the following API: + +### `openEditor(options: OpenFieldEditorOptions): CloseEditor` + +Use this method to open the index pattern field editor to either create (runtime) or edit (concrete | runtime) a field. + +#### `options` + +`ctx: FieldEditorContext` (**required**) + +This is the only required option. You need to provide the context in which the editor is being consumed. This object has the following properties: + +- `indexPattern: IndexPattern`: the index pattern you want to create/edit the field into. + +`onSave(field: IndexPatternField): void` (optional) + +You can provide an optional `onSave` handler to be notified when the field has being created/updated. This handler is called after the field has been persisted to the saved object. + +`fieldName: string` (optional) + +You can optionally pass the name of a field to edit. Leave empty to create a new runtime field based field. + +### `userPermissions.editIndexPattern(): boolean` + +Convenience method that uses the `core.application.capabilities` api to determine whether the user can edit the index pattern. + +### `` + +This children func React component provides a handler to delete one or multiple runtime fields. + +#### Props + +* `indexPattern: IndexPattern`: the current index pattern. (**required**) + +```js + +const { DeleteRuntimeFieldProvider } = indexPatternFieldEditor; + +// Single field + + {(deleteField) => ( + deleteField('myField')}> + Delete + + )} + + +// Multiple fields + + {(deleteFields) => ( + deleteFields(['field1', 'field2', 'field3'])}> + Delete + + )} + +``` diff --git a/src/plugins/index_pattern_field_editor/jest.config.js b/src/plugins/index_pattern_field_editor/jest.config.js new file mode 100644 index 00000000000000..fc358c37116c98 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/jest.config.js @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/index_pattern_field_editor'], +}; diff --git a/src/plugins/index_pattern_field_editor/kibana.json b/src/plugins/index_pattern_field_editor/kibana.json new file mode 100644 index 00000000000000..1e44b43ab36390 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "indexPatternFieldEditor", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["data"], + "optionalPlugins": ["usageCollection"], + "requiredBundles": ["kibanaReact", "esUiShared", "usageCollection"] +} diff --git a/src/plugins/index_pattern_field_editor/public/assets/icons/LICENSE.txt b/src/plugins/index_pattern_field_editor/public/assets/icons/LICENSE.txt new file mode 100644 index 00000000000000..1a86627c4a6b8c --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/assets/icons/LICENSE.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Steven Skelton + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/plugins/index_pattern_field_editor/public/assets/icons/cv.png b/src/plugins/index_pattern_field_editor/public/assets/icons/cv.png new file mode 100644 index 0000000000000000000000000000000000000000..8f2ff8432e6bd2a6fb6e52b5d11f47b679e69d1f GIT binary patch literal 802 zcmV+-1Ks?IP)fIA97IawaZjI6Pk1u^EYXI}IFh z1Qc-v6>axykuMMPB-A8P{?aRL)^0~2u;A!$)fPd7SS1Qc?Ahqru$wS0xOe1x=s zf|-GWlY4`-d4slkgSLBwwSI%4f`N>DgtGtt|Ns5&|K(@@;#mLu<^TTo|LTJO-bMfU z)%xS%`r_dF;oJJoc>2t4`ry|3(v14aVfx<6`r_f@Q9|KMJmE_{;Y&N=N;%<6I^j$_ z;!Qc>OFrUHIOJ6_`rz04(~bMhZTj8F`_6Ft$UXYnw*UV6|K)c7+eH8R;{WWM|KVo; z`{rnIsbg)XWod?FW`AUCpJQ#OVr-{kZKq;wrebQGV{4paY@rJqbpsS|8YO2+NKz6X zY6KK;2p4ZSJ6be2TnHC%4azP!z`T@1G_`)JcjjAP$bcfUC3m7AhzR z1)+!#Cl@>DQ;35PBe*)cm?;|Dn&Z$WIcXF70G{c)@Z+Atxfg|6TKq4BY$*zm<$t3P zfJyXat!Rz1l1Urd1VG6K@K!@?RdI>{NPRI;%+*owx_YAt^h{W(&_Uw zi|gqT@T~k0cpUjP!o*L2{lhjv*T7lM^K7Gz1DbceH9Ks!4CiC>R0&VhbP8O#e}FF%Mxo5AVP*p5HLn1PDccY zShg**6JY?rJU>NJDwfKRr%O>4QEZJN8dMq_4FHUAgGMY>A{Zz^(iLhZVYI%H04iim z!UhT-;%nGQhJt6*B0^(AlGLb_(qx42Fpy!O;{+-M6N3iThiV<&z$DD_(($|1O(KA^ zA()a$_`Rr9ej><5wFpQdQUj$BIT#F~5y=#42rVQ4gmIe$g_2+>5Tem3L39WPUp@pp zn^q>L3pi0PbKw?~kbz+uI*F8(l|{@7BBI)K5}8J$ksz1^!-052pl+KQ6B`26I`26K z4x*E46&g%|szEEGSc2*?CIPSX_ZC!|SF~!~%Q4{tBN@aR5}62DTbc{x^Zzu z!vx45^}d!^m$XfTkOYVh)oZ2r;^f}eR2n*4i-<8)n}nht&P_2f1I18X2C4zs?AfV- zetfZ1p|(2L&Z78ydW>3!iPcghhQlP_JVb>;MyEvv(;`C2FqgxPB$K(pp=@qMB+TZ( z)F5^cCyF|U<)Bi%3Q=QoSlJ)gpf_Tzf>3Gj%p63k*ow%aw5ST4ZJe%nO)ViX6=GAP zqTa~&5-WR6Ewk7V2zn!ygcn1yuJ=#to^#KqD%Qto=G zXH$U3z24s5+E%ctwsva#=~DwLd{B4Y?JY&s!j|Hm^I>_xM=2-82L%q7>PNoraX)bW z>L=%aaKAZN#_Y%(>hJF_HNs{8%hBs6SKG3>GK)i;8cc4&9>{n8@;*+H+R+60ZrG*T z=qGdDZ|~##?ShM8ZxfE$Ev;mA>|=)bxKS_Pu=O8R7^Z)u$vqe!OedO9Abkg?a*E+n z6eSh7?7FRN|A>4f)@xwvdTrVVQ`tkds}xVf;jE*gl?4v*RFxRId*T5Gk(WyRNS|HE zy|WxLJ&R#~c`w$d!lym#ORsNlLgf3BK_M8@9)#Mu9dl(lhd!s+_Ni+Zer4X`sd)dW z@`6a1xyNJ8aeZK^>pEz(Cg6gJaQZI}j{Vdz% ziIVS*0wsrKYvgMm!wde?;Jn}bc`1vv_kp(ffTQPDn^dO#CZy<4Ndr)Ry@yinLQ%6aGK4#vhE9<9jPRc+M7(}_0!mP3bvua*x5Z5o%{ zd;guJla`OAOkULzPf@_#ywAEX=6kev#y`?`bw7PE?)^nB%Ff!^A9k}bja4pmLSoKHMd2DPGjFhSLw|IvZ~)^;^zBs2khc3GZu?wnf(u+ zG$74i6(=m&2_L_lRJ3zZs1b%|#*Qs?ukjH6_|cBx6W1IgK0jFfcVCCc|78@MZ+x=K zG%a?$m*uZ{w8o(6Z5_{O9lX;myK62fOdXy&ckbNyd(^|0WO@nrt(2r2yIr%-mjC0F zhtLl>42uiCUz~b^_EUV+q63cbM&66BZO_7uH8nLo*~*lZlo2NRsn=q@cSMJ*#E-(Rj)4$`PUUzjN?E1RB}n25WvWpdf%=w=%`K)T3&UVGpT Qv-K|-!%g5+MvC(O52$Psod5s; literal 0 HcmV?d00001 diff --git a/src/plugins/index_pattern_field_editor/public/assets/icons/ne.png b/src/plugins/index_pattern_field_editor/public/assets/icons/ne.png new file mode 100644 index 0000000000000000000000000000000000000000..d331209e179988c1a885b3048c7cbd39d3a297ca GIT binary patch literal 336 zcmV-W0k8gvP)-Ns z|K-yEyN3U>cmLC}|N8gm^x)_905+ij0001-o+w)Y004MNL_t(2 z&+X5-5`sVwMbSGV%HU4BZ@4Sq^8bHlh6Ynz9b2CG2JgY2GoWEWRBOhqc9KK^_*&ZO z_WA=K@D&Y5naFseM$?%9^MxA4O9!x0qxB{Ox6SoUyS>QanCg)~oiEp0@LlmyR%LZB i0(cnkG~i|M+tC{bDNJMSx|^i{0000A7{?nJZW+VqtF_Z)C?=@51lbD=CT;_Y1C-RZwA+MG+F^!)O(?{w=)^Iu zck9p==C(%O=$5UNIh`_rj!_t_D{LT)vNBre4~||-+q+)xN};_w?@#bazR&lQCwX3c zlP@T!_%LJlzTF%SC!_Rev5@sQSiQ0r`+`&?cGS@R)FwmT~x@3L#hs!C~Mw@gG&t2&|-My&L;c{I}p~b)GneVu2X#CMytBr~PLIjXP zfD{AxF|}W+_KN`ah)V%~4fyN?xZc$DSM$Yw2@sG&bH@RXu#*sWM(Zw2D?qCZcq|2w z8qiS#J{Na}>p+XL%UA>2gq?V!c1Cq+w9Po6x_SR|_Z_8sfMFOaxruLBWB4kI!TIxl zw-?^+C`9c&3`3LH5{W*e>13RM!>}oth!N=1?=S1c9k+mAdgD=NVhcytM-p2Yi7khH z6Eu}zsOWFufxQ82zh^yj1?C0N+t?68Z$|tUGU6oB1q>OdY1)fhi~J)80{3$K69;_e ztk6_Ra5_Nz8woCuxH}OKZ^e8}!ryz_Aka6RHDrEc$Za!Zf7ka`l^!zuV`L}{3p|OF z2u*DzVk-=jTv{BGw9b|^O_wPr%a!Kx=KJNXBW2AGs=t2Zc8)PjVw+fwlfG@z;dTxV zjn52^&kc{wnZ};LE>kkTwoQ5%nic^OnR>Rh>-w3C16As!|LlUb9vZ1t&q&n)F@Q>dkQj)t70O9BK4$Ih7GZ^mjrl(Z?D?aG{enx2 zkBHx#RV>Ie_i%Tfcsc9A*%XL7=E~O>i1|T{0HgLoujlMhX1da$^q=gfH6|{vn)l_o z%5RHwu9AF1s)^L*GONh(uY1hI#Q?sG-Mb} zwL7xB2kUS4ZmK4Ds7!Qi>XAy6+K2pFknR1d0n%`1_2weirT4r?8lYFXpO`=Bf6>>H zYJE4C23~mB-0z;qknBidu5x*+lx3d>*9aN(`lu5rBrR2uzIk&?EMAXBon!Q mgP{)J@ExT0SMl1b_?$!c@-pl{qpNISPAR{l`1eBD5B~v;Q#?8V literal 0 HcmV?d00001 diff --git a/src/plugins/index_pattern_field_editor/public/assets/icons/stop.png b/src/plugins/index_pattern_field_editor/public/assets/icons/stop.png new file mode 100644 index 0000000000000000000000000000000000000000..4bf65fc96f59fd0ff9ac094c903510dae4c30ae9 GIT binary patch literal 1912 zcmaJ?X;2eq7!GJT6qUmRu$DE52LrpCaD)V+h9qc|K#URbKn%%3A|xBK8;PiBK|#d> z5fBkP0To5drPZLwKpl~KD=JVy5C^J8KphYTxWBLj({k1Ivs+k5S8jmc(`I4l(<;ys>B?J88`?g zQOi}h9904aMsXOb!I@-2>DLq#s_(Q)>|2?Lf1P(Ote9TKtcpVHEIdbxNt{9C>4XPM#MO(7NKa=a1{kI6h|=`ssh;Tp{fD1`C^G& zX*irSgyQoVJSB#Ul@f%&1Lyg*&M2;8{3WJ z=Q)h!pc0J&QR2f`=?^SxM66*T6e=Pz2T{w{BT_##ssM%(XUIp^LK~6q8&*297OxSp z5FrLKwD(`_9zG&w$gurByoB+6{1GKF^J-$a$M;CK5_jemkHZpa>&urdj-0V<;y{7& z>J?o<(?y%xDTkz)wziJs>hu+VY}Yq-ERr($9-<|Vb@=x}$& zGnp}%dhFHo#~pf{d7-JWz~499B?8>ES$Bt-{XPfN)0ZC*`WEKoeQ(lH=SH~a^#nP<&nh?SI?Z31o!g)yjgWLgxnKU;jC`do1 z3ceN^60&)X8^1B?VrM~XbHDqIzkW}OIbb`jp)v!us*c{jdR$v^LgFmAei22(tn4kD z$e&=ljS(-H3nn1XI_;g(>gP+SzL<03XR>RqeY|o_U*`HiLh%lFenovl)07K3Fkd(K zx&C9P*s>+hEOX&j+DY3>Wwt$qLcjc1beYb5QKLQYeu42MQyJzUU-)y6#v(CwhKu$I zp4(>Bc$nh+#K~gHZ?Cs{FEW|4)8VBwvFiA#m?U;|^1+B3Jzc)3Ycote$aCgzE@ulJ z%>7z|+qFhfZk5 zzr(7wTc=>Bf8NoXE~w_P=0n+Ea_`t2&Z8W+KiwcvBo&P6PR_kw)Ewu3`Q6w@IkDk0 zgt2jPw25|n?^#b9n6-ONPKIT5RC(1ke4OdmFZtr~{!MvMqn$>)1<)^{Pvdxbbv3VK zTJwOL#Wh)7<0H0B+SxRa+d7+thO#xeCR^H14#xblhZ|DahdDR5Td88xY|egaY;N@9}v;NtAmV88RhB_$=g=7w(8$@^YM&t<2j z3wc+z&8$vuUGY#OUy+DMB|KbKp)OvRwrb|7vZ|VO_Ja>!;|w*1y)Sul1B9S0XGY)c zD?F5$DQNBYR$JCTd#x)fqEwWYe(0*Jt9uJu+&=ZGFQc|i8y+6MF)nUkRqCgeI|psn YlFp|X8_`>;bcTN+j~l=_@4GhsA7XF;BLDyZ literal 0 HcmV?d00001 diff --git a/src/plugins/index_pattern_field_editor/public/assets/icons/us.png b/src/plugins/index_pattern_field_editor/public/assets/icons/us.png new file mode 100644 index 0000000000000000000000000000000000000000..f30f21f85d06a0f4fee61bad2f6ad4bdd26ffb4c GIT binary patch literal 1074 zcmV-21kL-2P)Fc7vXIhM|a)tN;K1JWh2Y*VMGGoxgeO4kpUSn z8iHkCO28F9lE+l!29^8F2d3VNFn;64P$Z+oC3K%(7;L~T^?MA;jEB@E=Av6?%y`}i zWZz*v1CUXd9{Dm(LN`gt|GL)EUrvncodiH?R)sTe^JeToQ>1OR-;P@l?4G|Yj4!@O z@Sd=2KsTvSobkRA<3#D-zrJNLPV!US%Wb*OxrzbJItH~@@nD1QY%XGC{IejnslEW+ zq&bWW)kRbvC^EKa2|K))T{p*C3p1Pw5LY}1PtHx&QyGU zej-Tsi>WQ2d&=is^YvmxbCa^I%C0I##-%a6;ZOd&+dhM_+vxKW43p-h%|33$m{IuM z5ai8unirxl-6X>JpMmNBe+D4M$N-_37#V+|D%#FK7%&PDqi8uXig<}p#6^rEPGS_X s5u=EO7)6Z4D3T&Zku))i42Vz!08vwAal)*8c>n+a07*qoM6N<$f>$NdB>(^b literal 0 HcmV?d00001 diff --git a/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/delete_field_provider.tsx b/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/delete_field_provider.tsx new file mode 100644 index 00000000000000..a42e1c18c1a614 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/delete_field_provider.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; + +type DeleteFieldFunc = (fieldName: string | string[]) => void; + +export interface Props { + children: (deleteFieldHandler: DeleteFieldFunc) => React.ReactNode; + onConfirmDelete: (fieldsToDelete: string[]) => Promise; +} + +interface State { + isModalOpen: boolean; + fieldsToDelete: string[]; +} + +const geti18nTexts = (fieldsToDelete?: string[]) => { + let modalTitle = ''; + if (fieldsToDelete) { + const isSingle = fieldsToDelete.length === 1; + + modalTitle = isSingle + ? i18n.translate( + 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.deleteSingleTitle', + { + defaultMessage: `Remove field '{name}'?`, + values: { name: fieldsToDelete[0] }, + } + ) + : i18n.translate( + 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.deleteMultipleTitle', + { + defaultMessage: `Remove {count} fields?`, + values: { count: fieldsToDelete.length }, + } + ); + } + + return { + modalTitle, + confirmButtonText: i18n.translate( + 'indexPatternFieldEditor.deleteRuntimeField.confirmationModal.removeButtonLabel', + { + defaultMessage: 'Remove', + } + ), + cancelButtonText: i18n.translate( + 'indexPatternFieldEditor.deleteRuntimeField.confirmationModal.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } + ), + warningMultipleFields: i18n.translate( + 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.multipleDeletionDescription', + { + defaultMessage: 'You are about to remove these runtime fields:', + } + ), + }; +}; + +export const DeleteRuntimeFieldProvider = ({ children, onConfirmDelete }: Props) => { + const [state, setState] = useState({ isModalOpen: false, fieldsToDelete: [] }); + + const { isModalOpen, fieldsToDelete } = state; + const i18nTexts = geti18nTexts(fieldsToDelete); + const { modalTitle, confirmButtonText, cancelButtonText, warningMultipleFields } = i18nTexts; + const isMultiple = Boolean(fieldsToDelete.length > 1); + + const deleteField: DeleteFieldFunc = useCallback((fieldNames) => { + setState({ + isModalOpen: true, + fieldsToDelete: Array.isArray(fieldNames) ? fieldNames : [fieldNames], + }); + }, []); + + const closeModal = useCallback(() => { + setState({ isModalOpen: false, fieldsToDelete: [] }); + }, []); + + const confirmDelete = useCallback(async () => { + try { + await onConfirmDelete(fieldsToDelete); + closeModal(); + } catch (e) { + // silently fail as "onConfirmDelete" is responsible + // to show a toast message if there is an error + } + }, [closeModal, onConfirmDelete, fieldsToDelete]); + + return ( + <> + {children(deleteField)} + + {isModalOpen && ( + + + {isMultiple && ( + <> +

{warningMultipleFields}

+
    + {fieldsToDelete.map((fieldName) => ( +
  • {fieldName}
  • + ))} +
+ + )} +
+
+ )} + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/get_delete_provider.tsx b/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/get_delete_provider.tsx new file mode 100644 index 00000000000000..c8f1ad90357617 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/get_delete_provider.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { NotificationsStart } from 'src/core/public'; +import { IndexPattern, UsageCollectionStart } from '../../shared_imports'; +import { pluginName } from '../../constants'; +import { DeleteRuntimeFieldProvider, Props as DeleteProviderProps } from './delete_field_provider'; +import { DataPublicPluginStart } from '../../../../data/public'; + +export interface Props extends Omit { + indexPattern: IndexPattern; + onDelete?: (fieldNames: string[]) => void; +} + +export const getDeleteProvider = ( + indexPatternService: DataPublicPluginStart['indexPatterns'], + usageCollection: UsageCollectionStart, + notifications: NotificationsStart +): React.FunctionComponent => { + return React.memo(({ indexPattern, children, onDelete }: Props) => { + const deleteFields = useCallback( + async (fieldNames: string[]) => { + fieldNames.forEach((fieldName) => { + indexPattern.removeRuntimeField(fieldName); + }); + + try { + usageCollection.reportUiCounter( + pluginName, + usageCollection.METRIC_TYPE.COUNT, + 'delete_runtime' + ); + // eslint-disable-next-line no-empty + } catch {} + + try { + await indexPatternService.updateSavedObject(indexPattern); + } catch (e) { + const title = i18n.translate('indexPatternFieldEditor.save.deleteErrorTitle', { + defaultMessage: 'Failed to save field removal', + }); + notifications.toasts.addError(e, { title }); + } + + if (onDelete) { + onDelete(fieldNames); + } + }, + [onDelete, indexPattern] + ); + + return ; + }); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/index.ts b/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/index.ts new file mode 100644 index 00000000000000..b93b7b92560ecf --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { getDeleteProvider, Props as DeleteProviderProps } from './get_delete_provider'; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/advanced_parameters_section.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/advanced_parameters_section.tsx new file mode 100644 index 00000000000000..26504eee28ddbd --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/advanced_parameters_section.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; + +interface Props { + children: React.ReactNode; +} + +export const AdvancedParametersSection = ({ children }: Props) => { + const [isVisible, setIsVisible] = useState(false); + + const toggleIsVisible = () => { + setIsVisible(!isVisible); + }; + + return ( + <> + + {isVisible + ? i18n.translate('indexPatternFieldEditor.editor.form.advancedSettings.hideButtonLabel', { + defaultMessage: 'Hide advanced settings', + }) + : i18n.translate('indexPatternFieldEditor.editor.form.advancedSettings.showButtonLabel', { + defaultMessage: 'Show advanced settings', + })} + + +
+ + {/* We ned to wrap the children inside a "div" to have our css :first-child rule */} +
{children}
+
+ + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/constants.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/constants.ts new file mode 100644 index 00000000000000..82711f707fa199 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/constants.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { EuiComboBoxOptionOption } from '@elastic/eui'; +import { RuntimeType } from '../../shared_imports'; + +export const RUNTIME_FIELD_OPTIONS: Array> = [ + { + label: 'Keyword', + value: 'keyword', + }, + { + label: 'Long', + value: 'long', + }, + { + label: 'Double', + value: 'double', + }, + { + label: 'Date', + value: 'date', + }, + { + label: 'IP', + value: 'ip', + }, + { + label: 'Boolean', + value: 'boolean', + }, +]; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx new file mode 100644 index 00000000000000..562f15301590bb --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { act } from 'react-dom/test-utils'; + +import '../../test_utils/setup_environment'; +import { registerTestBed, TestBed, getCommonActions } from '../../test_utils'; +import { Field } from '../../types'; +import { FieldEditor, Props, FieldEditorFormState } from './field_editor'; + +const defaultProps: Props = { + onChange: jest.fn(), + links: { + runtimePainless: 'https://elastic.co', + }, + ctx: { + existingConcreteFields: [], + namesNotAllowed: [], + fieldTypeToProcess: 'runtime', + }, + indexPattern: { fields: [] } as any, + fieldFormatEditors: { + getAll: () => [], + getById: () => undefined, + }, + fieldFormats: {} as any, + uiSettings: {} as any, + syntaxError: { + error: null, + clear: () => {}, + }, +}; + +const setup = (props?: Partial) => { + const testBed = registerTestBed(FieldEditor, { + memoryRouter: { + wrapComponent: false, + }, + })({ ...defaultProps, ...props }) as TestBed; + + const actions = { + ...getCommonActions(testBed), + }; + + return { + ...testBed, + actions, + }; +}; + +describe('', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + let testBed: TestBed & { actions: ReturnType }; + let onChange: jest.Mock = jest.fn(); + + const lastOnChangeCall = (): FieldEditorFormState[] => + onChange.mock.calls[onChange.mock.calls.length - 1]; + + const getLastStateUpdate = () => lastOnChangeCall()[0]; + + const submitFormAndGetData = async (state: FieldEditorFormState) => { + let formState: + | { + data: Field; + isValid: boolean; + } + | undefined; + + let promise: ReturnType; + + await act(async () => { + // We can't await for the promise here as the validation for the + // "script" field has a setTimeout which is mocked by jest. If we await + // we don't have the chance to call jest.advanceTimersByTime and thus the + // test times out. + promise = state.submit(); + }); + + await act(async () => { + // The painless syntax validation has a timeout set to 600ms + // we give it a bit more time just to be on the safe side + jest.advanceTimersByTime(1000); + }); + + await act(async () => { + promise.then((response) => { + formState = response; + }); + }); + + return formState!; + }; + + beforeEach(() => { + onChange = jest.fn(); + }); + + test('initial state should have "set custom label", "set value" and "set format" turned off', () => { + testBed = setup(); + + ['customLabel', 'value', 'format'].forEach((row) => { + const testSubj = `${row}Row.toggle`; + const toggle = testBed.find(testSubj); + const isOn = toggle.props()['aria-checked']; + + try { + expect(isOn).toBe(false); + } catch (e) { + e.message = `"${row}" row toggle expected to be 'off' but was 'on'. \n${e.message}`; + throw e; + } + }); + }); + + test('should accept a defaultValue and onChange prop to forward the form state', async () => { + const field = { + name: 'foo', + type: 'date', + script: { source: 'emit("hello")' }, + }; + + testBed = setup({ onChange, field }); + + expect(onChange).toHaveBeenCalled(); + + let lastState = getLastStateUpdate(); + expect(lastState.isValid).toBe(undefined); + expect(lastState.isSubmitted).toBe(false); + expect(lastState.submit).toBeDefined(); + + const { data: formData } = await submitFormAndGetData(lastState); + expect(formData).toEqual(field); + + // Make sure that both isValid and isSubmitted state are now "true" + lastState = getLastStateUpdate(); + expect(lastState.isValid).toBe(true); + expect(lastState.isSubmitted).toBe(true); + }); + + describe('validation', () => { + test('should accept an optional list of existing fields and prevent creating duplicates', async () => { + const existingFields = ['myRuntimeField']; + testBed = setup({ + onChange, + ctx: { + namesNotAllowed: existingFields, + existingConcreteFields: [], + fieldTypeToProcess: 'runtime', + }, + }); + + const { form, component, actions } = testBed; + + await act(async () => { + actions.toggleFormRow('value'); + }); + + await act(async () => { + form.setInputValue('nameField.input', existingFields[0]); + form.setInputValue('scriptField', 'echo("hello")'); + }); + + await act(async () => { + jest.advanceTimersByTime(1000); // Make sure our debounced error message is in the DOM + }); + + const lastState = getLastStateUpdate(); + await submitFormAndGetData(lastState); + component.update(); + expect(getLastStateUpdate().isValid).toBe(false); + expect(form.getErrorsMessages()).toEqual(['A field with this name already exists.']); + }); + + test('should not count the default value as a duplicate', async () => { + const existingRuntimeFieldNames = ['myRuntimeField']; + const field: Field = { + name: 'myRuntimeField', + type: 'boolean', + script: { source: 'emit("hello"' }, + }; + + testBed = setup({ + field, + onChange, + ctx: { + namesNotAllowed: existingRuntimeFieldNames, + existingConcreteFields: [], + fieldTypeToProcess: 'runtime', + }, + }); + + const { form, component } = testBed; + const lastState = getLastStateUpdate(); + await submitFormAndGetData(lastState); + component.update(); + expect(getLastStateUpdate().isValid).toBe(true); + expect(form.getErrorsMessages()).toEqual([]); + }); + }); +}); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx new file mode 100644 index 00000000000000..afb87bd1e73344 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx @@ -0,0 +1,286 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiComboBoxOptionOption, + EuiCode, +} from '@elastic/eui'; +import type { CoreStart } from 'src/core/public'; + +import { + Form, + useForm, + FormHook, + UseField, + TextField, + RuntimeType, + IndexPattern, + DataPublicPluginStart, +} from '../../shared_imports'; +import { Field, InternalFieldType, PluginStart } from '../../types'; + +import { RUNTIME_FIELD_OPTIONS } from './constants'; +import { schema } from './form_schema'; +import { getNameFieldConfig } from './lib'; +import { + TypeField, + CustomLabelField, + ScriptField, + FormatField, + PopularityField, + ScriptSyntaxError, +} from './form_fields'; +import { FormRow } from './form_row'; +import { AdvancedParametersSection } from './advanced_parameters_section'; + +export interface FieldEditorFormState { + isValid: boolean | undefined; + isSubmitted: boolean; + submit: FormHook['submit']; +} + +export interface FieldFormInternal extends Omit { + type: Array>; + __meta__: { + isCustomLabelVisible: boolean; + isValueVisible: boolean; + isFormatVisible: boolean; + isPopularityVisible: boolean; + }; +} + +export interface Props { + /** Link URLs to our doc site */ + links: { + runtimePainless: string; + }; + /** Optional field to edit */ + field?: Field; + /** Handler to receive state changes updates */ + onChange?: (state: FieldEditorFormState) => void; + indexPattern: IndexPattern; + fieldFormatEditors: PluginStart['fieldFormatEditors']; + fieldFormats: DataPublicPluginStart['fieldFormats']; + uiSettings: CoreStart['uiSettings']; + /** Context object */ + ctx: { + /** The internal field type we are dealing with (concrete|runtime)*/ + fieldTypeToProcess: InternalFieldType; + /** + * An array of field names not allowed. + * e.g we probably don't want a user to give a name of an existing + * runtime field (for that the user should edit the existing runtime field). + */ + namesNotAllowed: string[]; + /** + * An array of existing concrete fields. If the user gives a name to the runtime + * field that matches one of the concrete fields, a callout will be displayed + * to indicate that this runtime field will shadow the concrete field. + * It is also used to provide the list of field autocomplete suggestions to the code editor. + */ + existingConcreteFields: Array<{ name: string; type: string }>; + }; + syntaxError: ScriptSyntaxError; +} + +const geti18nTexts = (): { + [key: string]: { title: string; description: JSX.Element | string }; +} => ({ + customLabel: { + title: i18n.translate('indexPatternFieldEditor.editor.form.customLabelTitle', { + defaultMessage: 'Set custom label', + }), + description: i18n.translate('indexPatternFieldEditor.editor.form.customLabelDescription', { + defaultMessage: `Create a label to display in place of the field name in Discover, Maps, and Visualize. Useful for shortening a long field name. Queries and filters use the original field name.`, + }), + }, + value: { + title: i18n.translate('indexPatternFieldEditor.editor.form.valueTitle', { + defaultMessage: 'Set value', + }), + description: ( + {'_source'}, + }} + /> + ), + }, + format: { + title: i18n.translate('indexPatternFieldEditor.editor.form.formatTitle', { + defaultMessage: 'Set format', + }), + description: i18n.translate('indexPatternFieldEditor.editor.form.formatDescription', { + defaultMessage: `Set your preferred format for displaying the value. Changing the format can affect the value and prevent highlighting in Discover.`, + }), + }, + popularity: { + title: i18n.translate('indexPatternFieldEditor.editor.form.popularityTitle', { + defaultMessage: 'Set popularity', + }), + description: i18n.translate('indexPatternFieldEditor.editor.form.popularityDescription', { + defaultMessage: `Adjust the popularity to make the field appear higher or lower in the fields list. By default, Discover orders fields from most selected to least selected.`, + }), + }, +}); + +const formDeserializer = (field: Field): FieldFormInternal => { + let fieldType: Array>; + if (!field.type) { + fieldType = [RUNTIME_FIELD_OPTIONS[0]]; + } else { + const label = RUNTIME_FIELD_OPTIONS.find(({ value }) => value === field.type)?.label; + fieldType = [{ label: label ?? field.type, value: field.type as RuntimeType }]; + } + + return { + ...field, + type: fieldType, + __meta__: { + isCustomLabelVisible: field.customLabel !== undefined, + isValueVisible: field.script !== undefined, + isFormatVisible: field.format !== undefined, + isPopularityVisible: field.popularity !== undefined, + }, + }; +}; + +const formSerializer = (field: FieldFormInternal): Field => { + const { __meta__, type, ...rest } = field; + return { + type: type[0].value!, + ...rest, + }; +}; + +const FieldEditorComponent = ({ + field, + onChange, + links, + indexPattern, + fieldFormatEditors, + fieldFormats, + uiSettings, + syntaxError, + ctx: { fieldTypeToProcess, namesNotAllowed, existingConcreteFields }, +}: Props) => { + const { form } = useForm({ + defaultValue: field, + schema, + deserializer: formDeserializer, + serializer: formSerializer, + }); + const { submit, isValid: isFormValid, isSubmitted } = form; + + const nameFieldConfig = getNameFieldConfig(namesNotAllowed, field); + const i18nTexts = geti18nTexts(); + + useEffect(() => { + if (onChange) { + onChange({ isValid: isFormValid, isSubmitted, submit }); + } + }, [onChange, isFormValid, isSubmitted, submit]); + + return ( +
+ + {/* Name */} + + + path="name" + config={nameFieldConfig} + component={TextField} + data-test-subj="nameField" + componentProps={{ + euiFieldProps: { + disabled: fieldTypeToProcess === 'concrete', + 'aria-label': i18n.translate('indexPatternFieldEditor.editor.form.nameAriaLabel', { + defaultMessage: 'Name field', + }), + }, + }} + /> + + + {/* Type */} + + + + + + + + {/* Set custom label */} + + + + + {/* Set value */} + {fieldTypeToProcess === 'runtime' && ( + + + + )} + + {/* Set custom format */} + + + + + {/* Advanced settings */} + + + + + + + ); +}; + +export const FieldEditor = React.memo(FieldEditorComponent); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/custom_label_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/custom_label_field.tsx new file mode 100644 index 00000000000000..313137de463032 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/custom_label_field.tsx @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { UseField, TextField } from '../../../shared_imports'; + +export const CustomLabelField = () => { + return ; +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx new file mode 100644 index 00000000000000..db98e4a1591625 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useState, useEffect, useRef } from 'react'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; + +import { UseField, useFormData, ES_FIELD_TYPES, useFormContext } from '../../../shared_imports'; +import { FormatSelectEditor, FormatSelectEditorProps } from '../../field_format_editor'; +import { FieldFormInternal } from '../field_editor'; +import { FieldFormatConfig } from '../../../types'; + +export const FormatField = ({ + indexPattern, + fieldFormatEditors, + fieldFormats, + uiSettings, +}: Omit) => { + const isMounted = useRef(false); + const [{ type }] = useFormData({ watch: ['name', 'type'] }); + const { getFields, isSubmitted } = useFormContext(); + const [formatError, setFormatError] = useState(); + // convert from combobox type to values + const typeValue = type.reduce((collector, item) => { + if (item.value !== undefined) { + collector.push(item.value as ES_FIELD_TYPES); + } + return collector; + }, [] as ES_FIELD_TYPES[]); + + useEffect(() => { + if (formatError === undefined) { + getFields().format.setErrors([]); + } else { + getFields().format.setErrors([{ message: formatError }]); + } + }, [formatError, getFields]); + + useEffect(() => { + if (isMounted.current) { + getFields().format.reset(); + } + isMounted.current = true; + }, [type, getFields]); + + return ( + path="format"> + {({ setValue, errors, value }) => { + return ( + <> + {isSubmitted && errors.length > 0 && ( + <> + err.message)} + color="danger" + iconType="cross" + data-test-subj="formFormatError" + /> + + + )} + + + + ); + }} + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.ts new file mode 100644 index 00000000000000..e958e1362bb054 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { TypeField } from './type_field'; + +export { CustomLabelField } from './custom_label_field'; + +export { PopularityField } from './popularity_field'; + +export { ScriptField, ScriptSyntaxError } from './script_field'; + +export { FormatField } from './format_field'; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/popularity_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/popularity_field.tsx new file mode 100644 index 00000000000000..44f83138fe1d31 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/popularity_field.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { UseField, NumericField } from '../../../shared_imports'; + +export const PopularityField = () => { + return ( + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx new file mode 100644 index 00000000000000..d15445f3e10ae6 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx @@ -0,0 +1,227 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useEffect, useMemo, useRef } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFormRow, EuiLink, EuiCode, EuiCodeBlock, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { PainlessLang, PainlessContext } from '@kbn/monaco'; + +import { + UseField, + useFormData, + RuntimeType, + FieldConfig, + CodeEditor, +} from '../../../shared_imports'; +import { RuntimeFieldPainlessError } from '../../../lib'; +import { schema } from '../form_schema'; +import type { FieldFormInternal } from '../field_editor'; + +interface Props { + links: { runtimePainless: string }; + existingConcreteFields?: Array<{ name: string; type: string }>; + syntaxError: ScriptSyntaxError; +} + +export interface ScriptSyntaxError { + error: RuntimeFieldPainlessError | null; + clear: () => void; +} + +const mapReturnTypeToPainlessContext = (runtimeType: RuntimeType): PainlessContext => { + switch (runtimeType) { + case 'keyword': + return 'string_script_field_script_field'; + case 'long': + return 'long_script_field_script_field'; + case 'double': + return 'double_script_field_script_field'; + case 'date': + return 'date_script_field'; + case 'ip': + return 'ip_script_field_script_field'; + case 'boolean': + return 'boolean_script_field_script_field'; + default: + return 'string_script_field_script_field'; + } +}; + +export const ScriptField = React.memo(({ existingConcreteFields, links, syntaxError }: Props) => { + const editorValidationTimeout = useRef>(); + + const [painlessContext, setPainlessContext] = useState( + mapReturnTypeToPainlessContext(schema.type.defaultValue[0].value!) + ); + + const [editorId, setEditorId] = useState(); + + const suggestionProvider = PainlessLang.getSuggestionProvider( + painlessContext, + existingConcreteFields + ); + + const [{ type, script: { source } = { source: '' } }] = useFormData({ + watch: ['type', 'script.source'], + }); + + const { clear: clearSyntaxError } = syntaxError; + + const sourceFieldConfig: FieldConfig = useMemo(() => { + return { + ...schema.script.source, + validations: [ + ...schema.script.source.validations, + { + validator: () => { + if (editorValidationTimeout.current) { + clearTimeout(editorValidationTimeout.current); + } + + return new Promise((resolve) => { + // monaco waits 500ms before validating, so we also add a delay + // before checking if there are any syntax errors + editorValidationTimeout.current = setTimeout(() => { + const painlessSyntaxErrors = PainlessLang.getSyntaxErrors(); + // It is possible for there to be more than one editor in a view, + // so we need to get the syntax errors based on the editor (aka model) ID + const editorHasSyntaxErrors = editorId && painlessSyntaxErrors[editorId].length > 0; + + if (editorHasSyntaxErrors) { + return resolve({ + message: i18n.translate( + 'indexPatternFieldEditor.editor.form.scriptEditorValidationMessage', + { + defaultMessage: 'Invalid Painless syntax.', + } + ), + }); + } + + resolve(undefined); + }, 600); + }); + }, + }, + ], + }; + }, [editorId]); + + useEffect(() => { + setPainlessContext(mapReturnTypeToPainlessContext(type[0]!.value!)); + }, [type]); + + useEffect(() => { + // Whenever the source changes we clear potential syntax errors + clearSyntaxError(); + }, [source, clearSyntaxError]); + + return ( + path="script.source" config={sourceFieldConfig}> + {({ value, setValue, label, isValid, getErrorsMessages }) => { + let errorMessage: string | null = ''; + if (syntaxError.error !== null) { + errorMessage = syntaxError.error.reason ?? syntaxError.error.message; + } else { + errorMessage = getErrorsMessages(); + } + + return ( + <> + + {i18n.translate( + 'indexPatternFieldEditor.editor.form.script.learnMoreLinkText', + { + defaultMessage: 'Learn about script syntax.', + } + )} + + ), + source: {'_source'}, + }} + /> + } + fullWidth + > + setEditorId(editor.getModel()?.id)} + options={{ + fontSize: 12, + minimap: { + enabled: false, + }, + scrollBeyondLastLine: false, + wordWrap: 'on', + wrappingIndent: 'indent', + automaticLayout: true, + suggest: { + snippetsPreventQuickSuggestions: false, + }, + }} + data-test-subj="scriptField" + aria-label={i18n.translate( + 'indexPatternFieldEditor.editor.form.scriptEditorAriaLabel', + { + defaultMessage: 'Script editor', + } + )} + /> + + + {/* Help the user debug the error by showing where it failed in the script */} + {syntaxError.error !== null && ( + <> + + +

+ {i18n.translate( + 'indexPatternFieldEditor.editor.form.scriptEditor.debugErrorMessage', + { + defaultMessage: 'Syntax error detail', + } + )} +

+
+ + + {syntaxError.error.scriptStack.join('\n')} + + + )} + + ); + }} +
+ ); +}); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/type_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/type_field.tsx new file mode 100644 index 00000000000000..36428579a30e86 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/type_field.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +import { UseField, RuntimeType } from '../../../shared_imports'; +import { RUNTIME_FIELD_OPTIONS } from '../constants'; + +interface Props { + isDisabled?: boolean; +} + +export const TypeField = ({ isDisabled = false }: Props) => { + return ( + >> path="type"> + {({ label, value, setValue }) => { + if (value === undefined) { + return null; + } + return ( + <> + + { + if (newValue.length === 0) { + // Don't allow clearing the type. One must always be selected + return; + } + setValue(newValue); + }} + isClearable={false} + isDisabled={isDisabled} + data-test-subj="typeField" + aria-label={i18n.translate( + 'indexPatternFieldEditor.editor.form.typeSelectAriaLabel', + { + defaultMessage: 'Type select', + } + )} + fullWidth + /> + + + ); + }} + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_row.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_row.tsx new file mode 100644 index 00000000000000..66f5af09c8b2f4 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_row.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { get } from 'lodash'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiText, + EuiHorizontalRule, + EuiSpacer, +} from '@elastic/eui'; + +import { UseField, ToggleField, useFormData } from '../../shared_imports'; + +interface Props { + title: string; + formFieldPath: string; + children: React.ReactNode; + description?: string | JSX.Element; + withDividerRule?: boolean; + 'data-test-subj'?: string; +} + +export const FormRow = ({ + title, + description, + children, + formFieldPath, + withDividerRule = false, + 'data-test-subj': dataTestSubj, +}: Props) => { + const [formData] = useFormData({ watch: formFieldPath }); + const isContentVisible = Boolean(get(formData, formFieldPath)); + + return ( + <> + + + + + + +
+ {/* Title */} + +

{title}

+
+ + + {/* Description */} + + {description} + + + {/* Content */} + {isContentVisible && ( + <> + + {children} + + )} +
+
+
+ + {withDividerRule && } + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.ts new file mode 100644 index 00000000000000..a722f277b8e237 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { fieldValidators } from '../../shared_imports'; + +import { RUNTIME_FIELD_OPTIONS } from './constants'; + +const { emptyField, numberGreaterThanField } = fieldValidators; + +export const schema = { + name: { + label: i18n.translate('indexPatternFieldEditor.editor.form.nameLabel', { + defaultMessage: 'Name', + }), + validations: [ + { + validator: emptyField( + i18n.translate( + 'indexPatternFieldEditor.editor.form.validations.nameIsRequiredErrorMessage', + { + defaultMessage: 'A name is required.', + } + ) + ), + }, + ], + }, + type: { + label: i18n.translate('indexPatternFieldEditor.editor.form.runtimeTypeLabel', { + defaultMessage: 'Type', + }), + defaultValue: [RUNTIME_FIELD_OPTIONS[0]], + }, + script: { + source: { + label: i18n.translate('indexPatternFieldEditor.editor.form.defineFieldLabel', { + defaultMessage: 'Define script', + }), + validations: [ + { + validator: emptyField( + i18n.translate( + 'indexPatternFieldEditor.editor.form.validations.scriptIsRequiredErrorMessage', + { + defaultMessage: 'A script is required to set the field value.', + } + ) + ), + }, + ], + }, + }, + customLabel: { + label: i18n.translate('indexPatternFieldEditor.editor.form.customLabelLabel', { + defaultMessage: 'Custom label', + }), + validations: [ + { + validator: emptyField( + i18n.translate( + 'indexPatternFieldEditor.editor.form.validations.customLabelIsRequiredErrorMessage', + { + defaultMessage: 'Give a label to the field.', + } + ) + ), + }, + ], + }, + popularity: { + label: i18n.translate('indexPatternFieldEditor.editor.form.popularityLabel', { + defaultMessage: 'Popularity', + }), + validations: [ + { + validator: emptyField( + i18n.translate( + 'indexPatternFieldEditor.editor.form.validations.popularityIsRequiredErrorMessage', + { + defaultMessage: 'Give a popularity to the field.', + } + ) + ), + }, + { + validator: numberGreaterThanField({ + than: 0, + allowEquality: true, + message: i18n.translate( + 'indexPatternFieldEditor.editor.form.validations.popularityGreaterThan0ErrorMessage', + { + defaultMessage: 'The popularity must be zero or greater.', + } + ), + }), + }, + ], + }, + __meta__: { + isCustomLabelVisible: { + defaultValue: false, + }, + isValueVisible: { + defaultValue: false, + }, + isFormatVisible: { + defaultValue: false, + }, + isPopularityVisible: { + defaultValue: false, + }, + }, +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/index.ts new file mode 100644 index 00000000000000..db7c05fa7ff7a4 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { FieldEditor } from './field_editor'; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts new file mode 100644 index 00000000000000..2d324804c9e43d --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +import { ValidationFunc, FieldConfig } from '../../shared_imports'; +import { Field } from '../../types'; +import { schema } from './form_schema'; +import { Props } from './field_editor'; + +const createNameNotAllowedValidator = ( + namesNotAllowed: string[] +): ValidationFunc<{}, string, string> => ({ value }) => { + if (namesNotAllowed.includes(value)) { + return { + message: i18n.translate( + 'indexPatternFieldEditor.editor.runtimeFieldsEditor.existRuntimeFieldNamesValidationErrorMessage', + { + defaultMessage: 'A field with this name already exists.', + } + ), + }; + } +}; + +/** + * Dynamically retrieve the config for the "name" field, adding + * a validator to avoid duplicated runtime fields to be created. + * + * @param namesNotAllowed Array of names not allowed for the field "name" + * @param field Initial value of the form + */ +export const getNameFieldConfig = ( + namesNotAllowed?: string[], + field?: Props['field'] +): FieldConfig => { + const nameFieldConfig = schema.name as FieldConfig; + + if (!namesNotAllowed) { + return nameFieldConfig; + } + + // Add validation to not allow duplicates + return { + ...nameFieldConfig!, + validations: [ + ...(nameFieldConfig.validations ?? []), + { + validator: createNameNotAllowedValidator( + namesNotAllowed.filter((name) => name !== field?.name) + ), + }, + ], + }; +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/shadowing_field_warning.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/shadowing_field_warning.tsx new file mode 100644 index 00000000000000..4343b13db9a5af --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/shadowing_field_warning.tsx @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut } from '@elastic/eui'; + +export const ShadowingFieldWarning = () => { + return ( + +
+ {i18n.translate('indexPatternFieldEditor.editor.form.fieldShadowingCalloutDescription', { + defaultMessage: + 'This field shares the name of a mapped field. Values for this field will be returned in search results.', + })} +
+
+ ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts new file mode 100644 index 00000000000000..e943dbdda998df --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { act } from 'react-dom/test-utils'; + +import '../test_utils/setup_environment'; +import { registerTestBed, TestBed, noop, docLinks, getCommonActions } from '../test_utils'; + +import { FieldEditor } from './field_editor'; +import { FieldEditorFlyoutContent, Props } from './field_editor_flyout_content'; + +const defaultProps: Props = { + onSave: noop, + onCancel: noop, + docLinks, + FieldEditor, + indexPattern: { fields: [] } as any, + uiSettings: {} as any, + fieldFormats: {} as any, + fieldFormatEditors: {} as any, + fieldTypeToProcess: 'runtime', + runtimeFieldValidator: () => Promise.resolve(null), + isSavingField: false, +}; + +const setup = (props: Props = defaultProps) => { + const testBed = registerTestBed(FieldEditorFlyoutContent, { + memoryRouter: { wrapComponent: false }, + })(props) as TestBed; + + const actions = { + ...getCommonActions(testBed), + }; + + return { + ...testBed, + actions, + }; +}; + +describe('', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + test('should have the correct title', () => { + const { exists, find } = setup(); + expect(exists('flyoutTitle')).toBe(true); + expect(find('flyoutTitle').text()).toBe('Create field'); + }); + + test('should allow a field to be provided', () => { + const field = { + name: 'foo', + type: 'ip', + script: { + source: 'emit("hello world")', + }, + }; + + const { find } = setup({ ...defaultProps, field }); + + expect(find('flyoutTitle').text()).toBe(`Edit ${field.name} field`); + expect(find('nameField.input').props().value).toBe(field.name); + expect(find('typeField').props().value).toBe(field.type); + expect(find('scriptField').props().value).toBe(field.script.source); + }); + + test('should accept an "onSave" prop', async () => { + const field = { + name: 'foo', + type: 'date', + script: { source: 'test=123' }, + }; + const onSave: jest.Mock = jest.fn(); + + const { find } = setup({ ...defaultProps, onSave, field }); + + await act(async () => { + find('fieldSaveButton').simulate('click'); + }); + + await act(async () => { + // The painless syntax validation has a timeout set to 600ms + // we give it a bit more time just to be on the safe side + jest.advanceTimersByTime(1000); + }); + + expect(onSave).toHaveBeenCalled(); + const fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0]; + expect(fieldReturned).toEqual(field); + }); + + test('should accept an onCancel prop', () => { + const onCancel = jest.fn(); + const { find } = setup({ ...defaultProps, onCancel }); + + find('closeFlyoutButton').simulate('click'); + + expect(onCancel).toHaveBeenCalled(); + }); + + describe('validation', () => { + test('should validate the fields and prevent saving invalid form', async () => { + const onSave: jest.Mock = jest.fn(); + + const { find, exists, form, component } = setup({ ...defaultProps, onSave }); + + expect(find('fieldSaveButton').props().disabled).toBe(false); + + await act(async () => { + find('fieldSaveButton').simulate('click'); + }); + + await act(async () => { + jest.advanceTimersByTime(1000); + }); + + component.update(); + + expect(onSave).toHaveBeenCalledTimes(0); + expect(find('fieldSaveButton').props().disabled).toBe(true); + expect(form.getErrorsMessages()).toEqual(['A name is required.']); + expect(exists('formError')).toBe(true); + expect(find('formError').text()).toBe('Fix errors in form before continuing.'); + }); + + test('should forward values from the form', async () => { + const onSave: jest.Mock = jest.fn(); + + const { + find, + component, + form, + actions: { toggleFormRow }, + } = setup({ ...defaultProps, onSave }); + + act(() => { + form.setInputValue('nameField.input', 'someName'); + toggleFormRow('value'); + }); + component.update(); + + await act(async () => { + form.setInputValue('scriptField', 'echo("hello")'); + }); + + await act(async () => { + // Let's make sure that validation has finished running + jest.advanceTimersByTime(1000); + }); + + await act(async () => { + find('fieldSaveButton').simulate('click'); + }); + + expect(onSave).toHaveBeenCalled(); + + let fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0]; + + expect(fieldReturned).toEqual({ + name: 'someName', + type: 'keyword', // default to keyword + script: { source: 'echo("hello")' }, + }); + + // Change the type and make sure it is forwarded + act(() => { + find('typeField').simulate('change', [ + { + label: 'Other type', + value: 'other_type', + }, + ]); + }); + + await act(async () => { + find('fieldSaveButton').simulate('click'); + }); + + fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0]; + + expect(fieldReturned).toEqual({ + name: 'someName', + type: 'other_type', + script: { source: 'echo("hello")' }, + }); + }); + }); +}); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx new file mode 100644 index 00000000000000..1511836da85e73 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx @@ -0,0 +1,253 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; + +import { DocLinksStart, CoreStart } from 'src/core/public'; + +import { Field, InternalFieldType, PluginStart, EsRuntimeField } from '../types'; +import { getLinks, RuntimeFieldPainlessError } from '../lib'; +import type { IndexPattern, DataPublicPluginStart } from '../shared_imports'; +import type { Props as FieldEditorProps, FieldEditorFormState } from './field_editor/field_editor'; + +const geti18nTexts = (field?: Field) => { + return { + flyoutTitle: field + ? i18n.translate('indexPatternFieldEditor.editor.flyoutEditFieldTitle', { + defaultMessage: 'Edit {fieldName} field', + values: { + fieldName: field.name, + }, + }) + : i18n.translate('indexPatternFieldEditor.editor.flyoutDefaultTitle', { + defaultMessage: 'Create field', + }), + closeButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutCloseButtonLabel', { + defaultMessage: 'Close', + }), + saveButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutSaveButtonLabel', { + defaultMessage: 'Save', + }), + formErrorsCalloutTitle: i18n.translate('indexPatternFieldEditor.editor.validationErrorTitle', { + defaultMessage: 'Fix errors in form before continuing.', + }), + }; +}; + +export interface Props { + /** + * Handler for the "save" footer button + */ + onSave: (field: Field) => void; + /** + * Handler for the "cancel" footer button + */ + onCancel: () => void; + /** + * The docLinks start service from core + */ + docLinks: DocLinksStart; + /** + * The Field editor component that contains the form to create or edit a field + */ + FieldEditor: React.ComponentType | null; + /** The internal field type we are dealing with (concrete|runtime)*/ + fieldTypeToProcess: InternalFieldType; + /** Handler to validate the script */ + runtimeFieldValidator: (field: EsRuntimeField) => Promise; + /** Optional field to process */ + field?: Field; + + indexPattern: IndexPattern; + fieldFormatEditors: PluginStart['fieldFormatEditors']; + fieldFormats: DataPublicPluginStart['fieldFormats']; + uiSettings: CoreStart['uiSettings']; + isSavingField: boolean; +} + +const FieldEditorFlyoutContentComponent = ({ + field, + onSave, + onCancel, + FieldEditor, + docLinks, + indexPattern, + fieldFormatEditors, + fieldFormats, + uiSettings, + fieldTypeToProcess, + runtimeFieldValidator, + isSavingField, +}: Props) => { + const i18nTexts = geti18nTexts(field); + + const [formState, setFormState] = useState({ + isSubmitted: false, + isValid: field ? true : undefined, + submit: field + ? async () => ({ isValid: true, data: field }) + : async () => ({ isValid: false, data: {} as Field }), + }); + + const [painlessSyntaxError, setPainlessSyntaxError] = useState( + null + ); + + const [isValidating, setIsValidating] = useState(false); + + const { submit, isValid: isFormValid, isSubmitted } = formState; + const { fields } = indexPattern; + const isSaveButtonDisabled = isFormValid === false || painlessSyntaxError !== null; + + const clearSyntaxError = useCallback(() => setPainlessSyntaxError(null), []); + + const syntaxError = useMemo( + () => ({ + error: painlessSyntaxError, + clear: clearSyntaxError, + }), + [painlessSyntaxError, clearSyntaxError] + ); + + const onClickSave = useCallback(async () => { + const { isValid, data } = await submit(); + + if (isValid) { + if (data.script) { + setIsValidating(true); + + const error = await runtimeFieldValidator({ + type: data.type, + script: data.script, + }); + + setIsValidating(false); + setPainlessSyntaxError(error); + + if (error) { + return; + } + } + + onSave(data); + } + }, [onSave, submit, runtimeFieldValidator]); + + const namesNotAllowed = useMemo(() => fields.map((fld) => fld.name), [fields]); + + const existingConcreteFields = useMemo(() => { + const existing: Array<{ name: string; type: string }> = []; + + fields + .filter((fld) => { + const isFieldBeingEdited = field?.name === fld.name; + return !isFieldBeingEdited && fld.isMapped; + }) + .forEach((fld) => { + existing.push({ + name: fld.name, + type: (fld.esTypes && fld.esTypes[0]) || '', + }); + }); + + return existing; + }, [fields, field]); + + const ctx = useMemo( + () => ({ + fieldTypeToProcess, + namesNotAllowed, + existingConcreteFields, + }), + [fieldTypeToProcess, namesNotAllowed, existingConcreteFields] + ); + + return ( + <> + + +

{i18nTexts.flyoutTitle}

+
+
+ + + {FieldEditor && ( + + )} + + + + {FieldEditor && ( + <> + {isSubmitted && isSaveButtonDisabled && ( + <> + + + + )} + + + + {i18nTexts.closeButtonLabel} + + + + + + {i18nTexts.saveButtonLabel} + + + + + )} + + + ); +}; + +export const FieldEditorFlyoutContent = React.memo(FieldEditorFlyoutContentComponent); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx new file mode 100644 index 00000000000000..ade25424c22509 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useEffect, useState, useMemo } from 'react'; +import { DocLinksStart, NotificationsStart, CoreStart } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; + +import { + IndexPatternField, + IndexPattern, + DataPublicPluginStart, + RuntimeType, + UsageCollectionStart, +} from '../shared_imports'; +import { Field, PluginStart, InternalFieldType } from '../types'; +import { pluginName } from '../constants'; +import { deserializeField, getRuntimeFieldValidator } from '../lib'; +import { Props as FieldEditorProps } from './field_editor/field_editor'; +import { FieldEditorFlyoutContent } from './field_editor_flyout_content'; + +export interface FieldEditorContext { + indexPattern: IndexPattern; + /** + * The Kibana field type of the field to create or edit + * Default: "runtime" + */ + fieldTypeToProcess: InternalFieldType; + /** The search service from the data plugin */ + search: DataPublicPluginStart['search']; +} + +export interface Props { + /** + * Handler for the "save" footer button + */ + onSave: (field: IndexPatternField) => void; + /** + * Handler for the "cancel" footer button + */ + onCancel: () => void; + /** + * The docLinks start service from core + */ + docLinks: DocLinksStart; + /** + * The context object specific to where the editor is currently being consumed + */ + ctx: FieldEditorContext; + /** + * Optional field to edit + */ + field?: IndexPatternField; + /** + * Services + */ + indexPatternService: DataPublicPluginStart['indexPatterns']; + notifications: NotificationsStart; + fieldFormatEditors: PluginStart['fieldFormatEditors']; + fieldFormats: DataPublicPluginStart['fieldFormats']; + uiSettings: CoreStart['uiSettings']; + usageCollection: UsageCollectionStart; +} + +/** + * The container component will be in charge of the communication with the index pattern service + * to retrieve/save the field in the saved object. + * The component is the presentational component that won't know + * anything about where a field comes from and where it should be persisted. + */ + +export const FieldEditorFlyoutContentContainer = ({ + field, + onSave, + onCancel, + docLinks, + indexPatternService, + ctx: { indexPattern, fieldTypeToProcess, search }, + notifications, + fieldFormatEditors, + fieldFormats, + uiSettings, + usageCollection, +}: Props) => { + const fieldToEdit = deserializeField(indexPattern, field); + const [Editor, setEditor] = useState | null>(null); + const [isSaving, setIsSaving] = useState(false); + + const saveField = useCallback( + async (updatedField: Field) => { + setIsSaving(true); + + const { script } = updatedField; + + if (fieldTypeToProcess === 'runtime') { + try { + usageCollection.reportUiCounter( + pluginName, + usageCollection.METRIC_TYPE.COUNT, + 'save_runtime' + ); + // eslint-disable-next-line no-empty + } catch {} + // rename an existing runtime field + if (field?.name && field.name !== updatedField.name) { + indexPattern.removeRuntimeField(field.name); + } + + indexPattern.addRuntimeField(updatedField.name, { + type: updatedField.type as RuntimeType, + script, + }); + } else { + try { + usageCollection.reportUiCounter( + pluginName, + usageCollection.METRIC_TYPE.COUNT, + 'save_concrete' + ); + // eslint-disable-next-line no-empty + } catch {} + } + + const editedField = indexPattern.getFieldByName(updatedField.name); + + try { + if (!editedField) { + throw new Error( + `Unable to find field named '${updatedField.name}' on index pattern '${indexPattern.title}'` + ); + } + + indexPattern.setFieldCustomLabel(updatedField.name, updatedField.customLabel); + editedField.count = updatedField.popularity || 0; + if (updatedField.format) { + indexPattern.setFieldFormat(updatedField.name, updatedField.format); + } else { + indexPattern.deleteFieldFormat(updatedField.name); + } + + await indexPatternService.updateSavedObject(indexPattern).then(() => { + const message = i18n.translate('indexPatternFieldEditor.deleteField.savedHeader', { + defaultMessage: "Saved '{fieldName}'", + values: { fieldName: updatedField.name }, + }); + notifications.toasts.addSuccess(message); + setIsSaving(false); + onSave(editedField); + }); + } catch (e) { + const title = i18n.translate('indexPatternFieldEditor.save.errorTitle', { + defaultMessage: 'Failed to save field changes', + }); + notifications.toasts.addError(e, { title }); + setIsSaving(false); + } + }, + [ + onSave, + indexPattern, + indexPatternService, + notifications, + fieldTypeToProcess, + field?.name, + usageCollection, + ] + ); + + const validateRuntimeField = useMemo(() => getRuntimeFieldValidator(indexPattern.title, search), [ + search, + indexPattern, + ]); + + const loadEditor = useCallback(async () => { + const { FieldEditor } = await import('./field_editor'); + + setEditor(() => FieldEditor); + }, []); + + useEffect(() => { + // On mount: load the editor asynchronously + loadEditor(); + }, [loadEditor]); + + return ( + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/__snapshots__/format_editor.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/__snapshots__/format_editor.test.tsx.snap new file mode 100644 index 00000000000000..82d21eb5d30ada --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/__snapshots__/format_editor.test.tsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FieldFormatEditor should render normally 1`] = ` + + + +`; + +exports[`FieldFormatEditor should render nothing if there is no editor for the format 1`] = ` + + + +`; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap similarity index 92% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap index 69ea6c481d49b1..0f35267e1fb387 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap @@ -16,7 +16,7 @@ exports[`BytesFormatEditor should render normally 1`] = ` >   @@ -30,7 +30,7 @@ exports[`BytesFormatEditor should render normally 1`] = ` label={ diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/bytes.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/bytes.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/bytes.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/bytes.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/bytes.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/bytes.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/bytes.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/bytes.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap similarity index 87% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap index c66e7789aa511e..c33bb57bfeac89 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap @@ -9,7 +9,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = ` "field": "regex", "name": , "render": [Function], @@ -18,7 +18,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = ` "field": "text", "name": , "render": [Function], @@ -27,7 +27,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = ` "field": "background", "name": , "render": [Function], @@ -35,7 +35,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = ` Object { "name": , "render": [Function], @@ -89,7 +89,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = ` > @@ -108,7 +108,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] = "field": "range", "name": , "render": [Function], @@ -117,7 +117,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] = "field": "text", "name": , "render": [Function], @@ -126,7 +126,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] = "field": "background", "name": , "render": [Function], @@ -134,7 +134,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] = Object { "name": , "render": [Function], @@ -181,7 +181,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] = > @@ -200,7 +200,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`] "field": "regex", "name": , "render": [Function], @@ -209,7 +209,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`] "field": "text", "name": , "render": [Function], @@ -218,7 +218,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`] "field": "background", "name": , "render": [Function], @@ -226,7 +226,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`] Object { "name": , "render": [Function], @@ -273,7 +273,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`] > diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/color.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/color.test.tsx similarity index 96% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/color.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/color.test.tsx index f0e7d4aea42c84..1026012f3b8878 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/color.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/color.test.tsx @@ -11,7 +11,7 @@ import { shallowWithI18nProvider } from '@kbn/test/jest'; import { FieldFormat } from 'src/plugins/data/public'; import { ColorFormatEditor } from './color'; -import { fieldFormats } from '../../../../../../../../data/public'; +import { fieldFormats } from '../../../../../../data/public'; const fieldType = 'string'; const format = { diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/color.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/color.tsx similarity index 89% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/color.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/color.tsx index b169624fce9080..1e899a7179554f 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/color.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/color.tsx @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { DefaultFormatEditor, FormatEditorProps } from '../default'; -import { fieldFormats } from '../../../../../../../../../plugins/data/public'; +import { fieldFormats } from '../../../../../../data/public'; interface Color { range?: string; @@ -86,7 +86,7 @@ export class ColorFormatEditor extends DefaultFormatEditor ), @@ -110,7 +110,7 @@ export class ColorFormatEditor extends DefaultFormatEditor ), @@ -134,7 +134,7 @@ export class ColorFormatEditor extends DefaultFormatEditor ), @@ -158,7 +158,7 @@ export class ColorFormatEditor extends DefaultFormatEditor ), @@ -181,7 +181,7 @@ export class ColorFormatEditor extends DefaultFormatEditor ), @@ -200,15 +200,15 @@ export class ColorFormatEditor extends DefaultFormatEditor { @@ -229,7 +229,7 @@ export class ColorFormatEditor extends DefaultFormatEditor diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap similarity index 93% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap index 48a7c3e013b1af..4560904c9b4c41 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap @@ -16,7 +16,7 @@ exports[`DateFormatEditor should render normally 1`] = ` >   @@ -30,7 +30,7 @@ exports[`DateFormatEditor should render normally 1`] = ` label={ diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/date.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/date.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/date.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/date.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/date.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/date.tsx similarity index 94% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/date.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/date.tsx index ae29c7a1236f57..62fb08855ce93e 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/date.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/date.tsx @@ -41,7 +41,7 @@ export class DateFormatEditor extends DefaultFormatEditor{defaultPattern}, @@ -54,7 +54,7 @@ export class DateFormatEditor extends DefaultFormatEditor   diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap similarity index 93% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap index 540c8ece9e35b9..0d0962a281950c 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap @@ -16,7 +16,7 @@ exports[`DateFormatEditor should render normally 1`] = ` >   @@ -30,7 +30,7 @@ exports[`DateFormatEditor should render normally 1`] = ` label={ diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx similarity index 95% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx index 3f66dc59ab5697..4e8d56f91c6eb1 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { FieldFormat } from '../../../../../../../../data/public'; +import type { FieldFormat } from 'src/plugins/data/public'; import { DateNanosFormatEditor } from './date_nanos'; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.tsx similarity index 94% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.tsx index fab96322f0a16d..d9ee099aaef36a 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.tsx @@ -40,7 +40,7 @@ export class DateNanosFormatEditor extends DefaultFormatEditor{defaultPattern}, @@ -53,7 +53,7 @@ export class DateNanosFormatEditor extends DefaultFormatEditor   diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/__snapshots__/default.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/__snapshots__/default.test.tsx.snap similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/__snapshots__/default.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/__snapshots__/default.test.tsx.snap diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/default.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/default.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/default.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.tsx similarity index 92% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/default.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.tsx index 1bfc2c2fa340e1..06f3b318b6e935 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/default.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.tsx @@ -10,8 +10,8 @@ import React, { PureComponent, ReactText } from 'react'; import { i18n } from '@kbn/i18n'; import { FieldFormat, FieldFormatsContentType } from 'src/plugins/data/public'; -import { Sample } from '../../../../types'; -import { FieldFormatEditorProps } from '../../field_format_editor'; +import { Sample } from '../../types'; +import { FormatSelectEditorProps } from '../../field_format_editor'; export type ConverterParams = string | number | Array; @@ -30,7 +30,7 @@ export const convertSampleInput = ( }; }); } catch (e) { - error = i18n.translate('indexPatternManagement.defaultErrorMessage', { + error = i18n.translate('indexPatternFieldEditor.defaultErrorMessage', { defaultMessage: 'An error occurred while trying to use this format configuration: {message}', values: { message: e.message }, }); @@ -51,7 +51,7 @@ export interface FormatEditorProps

{ format: FieldFormat; formatParams: { type?: string } & P; onChange: (newParams: Record) => void; - onError: FieldFormatEditorProps['onError']; + onError: FormatSelectEditorProps['onError']; } export interface FormatEditorState { diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap similarity index 93% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap index c617c3b43039bf..cb7949deda64f6 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap @@ -12,7 +12,7 @@ exports[`DurationFormatEditor should render human readable output normally 1`] = label={ } @@ -42,7 +42,7 @@ exports[`DurationFormatEditor should render human readable output normally 1`] = label={ } @@ -124,7 +124,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1 label={ } @@ -154,7 +154,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1 label={ } @@ -189,7 +189,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1 label={ } @@ -216,7 +216,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1 label={ } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/duration.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/duration.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/duration.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.tsx similarity index 93% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/duration.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.tsx index 4842c7066a2ef8..de413d02c5011c 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/duration.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.tsx @@ -65,7 +65,7 @@ export class DurationFormatEditor extends DefaultFormatEditor< !(nextProps.format as DurationFormat).isHuman() && nextProps.formatParams.outputPrecision > 20 ) { - error = i18n.translate('indexPatternManagement.durationErrorMessage', { + error = i18n.translate('indexPatternFieldEditor.durationErrorMessage', { defaultMessage: 'Decimal places must be between 0 and 20', }); nextProps.onError(error); @@ -91,7 +91,7 @@ export class DurationFormatEditor extends DefaultFormatEditor< } @@ -115,7 +115,7 @@ export class DurationFormatEditor extends DefaultFormatEditor< } @@ -140,7 +140,7 @@ export class DurationFormatEditor extends DefaultFormatEditor< } @@ -163,7 +163,7 @@ export class DurationFormatEditor extends DefaultFormatEditor< } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/index.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/index.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/index.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/index.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap similarity index 92% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap index c73b5e7186547f..3cac3850548351 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap @@ -16,7 +16,7 @@ exports[`NumberFormatEditor should render normally 1`] = ` >   @@ -30,7 +30,7 @@ exports[`NumberFormatEditor should render normally 1`] = ` label={ diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.tsx similarity index 94% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.tsx index 250bbe570a9c4f..2aeb90373bfaba 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.tsx @@ -36,7 +36,7 @@ export class NumberFormatEditor extends DefaultFormatEditor{defaultPattern} }} /> @@ -45,7 +45,7 @@ export class NumberFormatEditor extends DefaultFormatEditor   diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap similarity index 92% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap index 16ce8ca9643ef2..f6af1f0dff7fe3 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap @@ -16,7 +16,7 @@ exports[`PercentFormatEditor should render normally 1`] = ` >   @@ -30,7 +30,7 @@ exports[`PercentFormatEditor should render normally 1`] = ` label={ diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/percent.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/percent.test.tsx similarity index 95% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/percent.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/percent.test.tsx index 6eff2fd279cbb5..072dc0caeb3c8b 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/percent.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/percent.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { FieldFormat } from '../../../../../../../../data/public'; +import { FieldFormat } from 'src/plugins/data/public'; import { PercentFormatEditor } from './percent'; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/percent.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/percent.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/percent.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/percent.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap similarity index 88% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap index 46267b0c3c0e98..c5697cb699eb7a 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap @@ -9,7 +9,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn "field": "key", "name": , "render": [Function], @@ -18,7 +18,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn "field": "value", "name": , "render": [Function], @@ -73,7 +73,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn > @@ -89,7 +89,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn label={ } @@ -116,7 +116,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = ` "field": "key", "name": , "render": [Function], @@ -125,7 +125,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = ` "field": "value", "name": , "render": [Function], @@ -174,7 +174,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = ` > @@ -190,7 +190,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = ` label={ } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx similarity index 96% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx index e24b656267d1aa..8d9cb17b33a403 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { shallowWithI18nProvider } from '@kbn/test/jest'; import { StaticLookupFormatEditorFormatParams } from './static_lookup'; -import { FieldFormat } from '../../../../../../../../data/public'; +import { FieldFormat } from 'src/plugins/data/public'; import { StaticLookupFormatEditor } from './static_lookup'; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/static_lookup.tsx similarity index 88% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/static_lookup.tsx index 8c49615c99f6c2..8ac03bb23bd25a 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/static_lookup.tsx @@ -72,7 +72,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor ), @@ -96,7 +96,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor ), @@ -118,15 +118,15 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor { @@ -148,7 +148,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor @@ -156,7 +156,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor } @@ -164,7 +164,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/string.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/string.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/string.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/string.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/string.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/string.tsx similarity index 96% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/string.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/string.tsx index 6f9e0e10e188a4..e86a62775cebcb 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/string.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/string.tsx @@ -47,7 +47,7 @@ export class StringFormatEditor extends DefaultFormatEditor } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap similarity index 96% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap index 2d1ee496d2786b..f982632bba5235 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap @@ -12,7 +12,7 @@ exports[`TruncateFormatEditor should render normally 1`] = ` label={ } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/sample.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/sample.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/sample.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/sample.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/truncate.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/truncate.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/truncate.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/truncate.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/truncate.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/truncate.tsx similarity index 96% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/truncate.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/truncate.tsx index 4b24d33e58f3ce..03b7d6e0573cc5 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/truncate.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/truncate.tsx @@ -37,7 +37,7 @@ export class TruncateFormatEditor extends DefaultFormatEditor } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap similarity index 92% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap index b627dbe0576ee2..bc5efb8f5eda42 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap @@ -166,14 +166,26 @@ exports[`UrlFormatEditor should render normally 1`] = ` class="euiFormHelpText euiFormRow__text" id="generated-id-help" > - + + + (opens in a new tab or window) + +

@@ -217,14 +229,26 @@ exports[`UrlFormatEditor should render normally 1`] = ` class="euiFormHelpText euiFormRow__text" id="generated-id-help" > - + + + (opens in a new tab or window) + + diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/url.test.tsx similarity index 75% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/url.test.tsx index 5c86abc3b4a9c1..9f299a433aab1a 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/url.test.tsx @@ -10,8 +10,8 @@ import React from 'react'; import { FieldFormat } from 'src/plugins/data/public'; import { IntlProvider } from 'react-intl'; import { UrlFormatEditor } from './url'; -import { coreMock } from '../../../../../../../../../core/public/mocks'; -import { createKibanaReactContext } from '../../../../../../../../kibana_react/public'; +import { coreMock } from 'src/core/public/mocks'; +import { createKibanaReactContext } from '../../../../../../kibana_react/public'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -76,38 +76,6 @@ describe('UrlFormatEditor', () => { expect(container).toMatchSnapshot(); }); - it('should render url template help', async () => { - const { getByText, getByTestId } = renderWithContext( - - ); - - getByText('URL template help'); - userEvent.click(getByText('URL template help')); - expect(getByTestId('urlTemplateFlyoutTestSubj')).toBeVisible(); - }); - - it('should render label template help', async () => { - const { getByText, getByTestId } = renderWithContext( - - ); - - getByText('Label template help'); - userEvent.click(getByText('Label template help')); - expect(getByTestId('labelTemplateFlyoutTestSubj')).toBeVisible(); - }); - it('should render width and height fields if image', async () => { const { getByLabelText } = renderWithContext( { static contextType = contextType; static formatId = 'url'; - // TODO: @kbn/optimizer can't compile this - // declare context: IndexPatternManagmentContextValue; - context: IndexPatternManagmentContextValue | undefined; private get sampleIconPath() { const sampleIconPath = `/plugins/indexPatternManagement/assets/icons/{{value}}.png`; return this.context?.services.http @@ -110,32 +103,6 @@ export class UrlFormatEditor extends DefaultFormatEditor< this.onChange(params); }; - showUrlTemplateHelp = () => { - this.setState({ - showLabelTemplateHelp: false, - showUrlTemplateHelp: true, - }); - }; - - hideUrlTemplateHelp = () => { - this.setState({ - showUrlTemplateHelp: false, - }); - }; - - showLabelTemplateHelp = () => { - this.setState({ - showLabelTemplateHelp: true, - showUrlTemplateHelp: false, - }); - }; - - hideLabelTemplateHelp = () => { - this.setState({ - showLabelTemplateHelp: false, - }); - }; - renderWidthHeightParameters = () => { const width = this.sanitizeNumericValue(this.props.formatParams.width); const height = this.sanitizeNumericValue(this.props.formatParams.height); @@ -143,7 +110,7 @@ export class UrlFormatEditor extends DefaultFormatEditor< + } > + } > - - + } > } @@ -217,9 +179,12 @@ export class UrlFormatEditor extends DefaultFormatEditor< + ) : ( - + ) } checked={!formatParams.openLinkInCurrentTab} @@ -233,14 +198,17 @@ export class UrlFormatEditor extends DefaultFormatEditor< } helpText={ - + @@ -260,14 +228,17 @@ export class UrlFormatEditor extends DefaultFormatEditor< } helpText={ - + diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx new file mode 100644 index 00000000000000..1f3e87e69fd4c2 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx @@ -0,0 +1,186 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { PureComponent } from 'react'; +import { EuiCode, EuiFormRow, EuiSelect } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + FieldFormatInstanceType, + IndexPattern, + KBN_FIELD_TYPES, + ES_FIELD_TYPES, + DataPublicPluginStart, + FieldFormat, +} from 'src/plugins/data/public'; +import { CoreStart } from 'src/core/public'; +import { castEsToKbnFieldTypeName } from '../../../../data/public'; +import { FormatEditor } from './format_editor'; +import { FormatEditorServiceStart } from '../../service'; +import { FieldFormatConfig } from '../../types'; + +export interface FormatSelectEditorProps { + esTypes: ES_FIELD_TYPES[]; + indexPattern: IndexPattern; + fieldFormatEditors: FormatEditorServiceStart['fieldFormatEditors']; + fieldFormats: DataPublicPluginStart['fieldFormats']; + uiSettings: CoreStart['uiSettings']; + onChange: (change?: FieldFormatConfig) => void; + onError: (error?: string) => void; + value?: FieldFormatConfig; +} + +interface FieldTypeFormat { + id: string; + title: string; +} + +export interface FormatSelectEditorState { + fieldTypeFormats: FieldTypeFormat[]; + fieldFormatId?: string; + fieldFormatParams?: { [key: string]: unknown }; + format: FieldFormat; + kbnType: KBN_FIELD_TYPES; +} + +interface InitialFieldTypeFormat extends FieldTypeFormat { + defaultFieldFormat: FieldFormatInstanceType; +} + +const getFieldTypeFormatsList = ( + fieldType: KBN_FIELD_TYPES, + defaultFieldFormat: FieldFormatInstanceType, + fieldFormats: DataPublicPluginStart['fieldFormats'] +) => { + const formatsByType = fieldFormats.getByFieldType(fieldType).map(({ id, title }) => ({ + id, + title, + })); + + return [ + { + id: '', + defaultFieldFormat, + title: i18n.translate('indexPatternFieldEditor.defaultFormatDropDown', { + defaultMessage: '- Default -', + }), + }, + ...formatsByType, + ]; +}; + +export class FormatSelectEditor extends PureComponent< + FormatSelectEditorProps, + FormatSelectEditorState +> { + constructor(props: FormatSelectEditorProps) { + super(props); + const { fieldFormats, esTypes, value } = props; + const kbnType = castEsToKbnFieldTypeName(esTypes[0] || 'keyword'); + + // get current formatter for field, provides default if none exists + const format = value?.id + ? fieldFormats.getInstance(value?.id, value?.params) + : fieldFormats.getDefaultInstance(kbnType, esTypes); + + this.state = { + fieldTypeFormats: getFieldTypeFormatsList( + kbnType, + fieldFormats.getDefaultType(kbnType, esTypes) as FieldFormatInstanceType, + fieldFormats + ), + format, + kbnType, + }; + } + onFormatChange = (formatId: string, params?: any) => { + const { fieldTypeFormats } = this.state; + const { fieldFormats, uiSettings } = this.props; + + const FieldFormatClass = fieldFormats.getType( + formatId || (fieldTypeFormats[0] as InitialFieldTypeFormat).defaultFieldFormat.id + ) as FieldFormatInstanceType; + + const newFormat = new FieldFormatClass(params, (key: string) => uiSettings.get(key)); + + this.setState( + { + fieldFormatId: formatId, + fieldFormatParams: params, + format: newFormat, + }, + () => { + this.props.onChange( + formatId + ? { + id: formatId, + params: params || {}, + } + : undefined + ); + } + ); + }; + onFormatParamsChange = (newParams: { fieldType: string; [key: string]: any }) => { + const { fieldFormatId } = this.state; + this.onFormatChange(fieldFormatId as string, newParams); + }; + + render() { + const { fieldFormatEditors, onError, value } = this.props; + const fieldFormatId = value?.id; + const fieldFormatParams = value?.params; + const { kbnType } = this.state; + + const { fieldTypeFormats, format } = this.state; + + const defaultFormat = (fieldTypeFormats[0] as InitialFieldTypeFormat).defaultFieldFormat.title; + + const label = defaultFormat ? ( + {defaultFormat}, + }} + /> + ) : ( + + ); + return ( + <> + + { + return { value: fmt.id || '', text: fmt.title }; + })} + data-test-subj="editorSelectedFormatId" + onChange={(e) => { + this.onFormatChange(e.target.value); + }} + /> + + {fieldFormatId ? ( + { + this.onFormatChange(fieldFormatId, params); + }} + onError={onError} + /> + ) : null} + + ); + } +} diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.test.tsx new file mode 100644 index 00000000000000..5514baf43ecfc0 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { PureComponent } from 'react'; +import { shallow } from 'enzyme'; +import { FormatEditor } from './format_editor'; + +class TestEditor extends PureComponent { + render() { + if (this.props) { + return null; + } + return
Test editor
; + } +} + +const formatEditors = { + byFormatId: { + ip: TestEditor, + number: TestEditor, + }, + getById: jest.fn(() => TestEditor as any), + getAll: jest.fn(), +}; + +describe('FieldFormatEditor', () => { + it('should render normally', async () => { + const component = shallow( + {}} + onError={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render nothing if there is no editor for the format', async () => { + const component = shallow( + {}} + onError={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.tsx new file mode 100644 index 00000000000000..043a911e69812a --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { PureComponent, Fragment } from 'react'; +import { FieldFormat } from 'src/plugins/data/public'; + +export interface FormatEditorProps { + fieldType: string; + fieldFormat: FieldFormat; + fieldFormatId: string; + fieldFormatParams: { [key: string]: unknown }; + fieldFormatEditors: any; + onChange: (change: { fieldType: string; [key: string]: any }) => void; + onError: (error?: string) => void; +} + +interface EditorComponentProps { + fieldType: FormatEditorProps['fieldType']; + format: FormatEditorProps['fieldFormat']; + formatParams: FormatEditorProps['fieldFormatParams']; + onChange: FormatEditorProps['onChange']; + onError: FormatEditorProps['onError']; +} + +interface FormatEditorState { + EditorComponent: React.FC; + fieldFormatId?: string; +} + +export class FormatEditor extends PureComponent { + constructor(props: FormatEditorProps) { + super(props); + this.state = { + EditorComponent: props.fieldFormatEditors.getById(props.fieldFormatId), + }; + } + + static getDerivedStateFromProps(nextProps: FormatEditorProps) { + return { + EditorComponent: nextProps.fieldFormatEditors.getById(nextProps.fieldFormatId) || null, + }; + } + + render() { + const { EditorComponent } = this.state; + const { fieldType, fieldFormat, fieldFormatParams, onChange, onError } = this.props; + + return ( + + {EditorComponent ? ( + + ) : null} + + ); + } +} diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/index.ts new file mode 100644 index 00000000000000..34619f53e9eed6 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { FormatSelectEditor, FormatSelectEditorProps } from './field_format_editor'; +export * from './editors'; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap similarity index 96% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap index ce8c9e70433c84..1a0b96c14fe359 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap @@ -10,7 +10,7 @@ exports[`FormatEditorSamples should render normally 1`] = ` label={ } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.scss b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.scss similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.scss rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.scss diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.tsx similarity index 87% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.tsx index 536cb3567017a9..73727119f10777 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.tsx @@ -14,7 +14,7 @@ import { EuiBasicTable, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Sample } from '../../../types'; +import { Sample } from '../types'; interface FormatEditorSamplesProps { samples: Sample[]; @@ -32,7 +32,7 @@ export class FormatEditorSamples extends PureComponent const columns = [ { field: 'input', - name: i18n.translate('indexPatternManagement.samples.inputHeader', { + name: i18n.translate('indexPatternFieldEditor.samples.inputHeader', { defaultMessage: 'Input', }), render: (input: {} | string) => { @@ -41,7 +41,7 @@ export class FormatEditorSamples extends PureComponent }, { field: 'output', - name: i18n.translate('indexPatternManagement.samples.outputHeader', { + name: i18n.translate('indexPatternFieldEditor.samples.outputHeader', { defaultMessage: 'Output', }), render: (output: string) => { @@ -63,7 +63,7 @@ export class FormatEditorSamples extends PureComponent return samples.length ? ( + } > diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/types.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/types.ts new file mode 100644 index 00000000000000..11c0b8a6259070 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/types.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ReactText } from 'react'; + +export interface Sample { + input: ReactText | ReactText[]; + output: string; +} diff --git a/src/plugins/index_pattern_field_editor/public/components/index.ts b/src/plugins/index_pattern_field_editor/public/components/index.ts new file mode 100644 index 00000000000000..0fbb574a6f0a41 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { + FieldEditorFlyoutContent, + Props as FieldEditorFlyoutContentProps, +} from './field_editor_flyout_content'; + +export { + FieldEditorFlyoutContentContainer, + Props as FieldEditorFlyoutContentContainerProps, + FieldEditorContext, +} from './field_editor_flyout_content_container'; + +export * from './field_format_editor'; diff --git a/src/plugins/index_pattern_field_editor/public/constants.ts b/src/plugins/index_pattern_field_editor/public/constants.ts new file mode 100644 index 00000000000000..69d231f3758486 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/constants.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const pluginName = 'index_pattern_field_editor'; diff --git a/src/plugins/index_pattern_field_editor/public/index.ts b/src/plugins/index_pattern_field_editor/public/index.ts new file mode 100644 index 00000000000000..38735013d576d5 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * Management Plugin - public + * + * This is the entry point for the entire client-side public contract of the plugin. + * If something is not explicitly exported here, you can safely assume it is private + * to the plugin and not considered stable. + * + * All stateful contracts will be injected by the platform at runtime, and are defined + * in the setup/start interfaces in `plugin.ts`. The remaining items exported here are + * either types, or static code. + */ + +import { IndexPatternFieldEditorPlugin } from './plugin'; + +export { PluginStart as IndexPatternFieldEditorStart } from './types'; +export { DefaultFormatEditor } from './components'; + +export function plugin() { + return new IndexPatternFieldEditorPlugin(); +} + +// Expose types +export type { OpenFieldEditorOptions } from './open_editor'; +export type { FieldEditorContext } from './components/field_editor_flyout_content_container'; diff --git a/src/plugins/index_pattern_field_editor/public/lib/documentation.ts b/src/plugins/index_pattern_field_editor/public/lib/documentation.ts new file mode 100644 index 00000000000000..9577f25184ba0a --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/lib/documentation.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DocLinksStart } from 'src/core/public'; + +export const getLinks = (docLinks: DocLinksStart) => { + const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; + const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; + const esDocsBase = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`; + const painlessDocsBase = `${docsBase}/elasticsearch/painless/${DOC_LINK_VERSION}`; + + return { + runtimePainless: `${esDocsBase}/runtime.html#runtime-mapping-fields`, + painlessSyntax: `${painlessDocsBase}/painless-lang-spec.html`, + }; +}; diff --git a/src/plugins/index_pattern_field_editor/public/lib/index.ts b/src/plugins/index_pattern_field_editor/public/lib/index.ts new file mode 100644 index 00000000000000..5d5b3d881e9769 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/lib/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { deserializeField } from './serialization'; + +export { getLinks } from './documentation'; + +export { getRuntimeFieldValidator, RuntimeFieldPainlessError } from './runtime_field_validation'; diff --git a/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts new file mode 100644 index 00000000000000..b25d47b3d0d151 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { dataPluginMock } from '../../../data/public/mocks'; +import { getRuntimeFieldValidator } from './runtime_field_validation'; + +const dataStart = dataPluginMock.createStartContract(); +const { search } = dataStart; + +const runtimeField = { + type: 'keyword', + script: { + source: 'emit("hello")', + }, +}; + +const spy = jest.fn(); + +search.search = () => + ({ + toPromise: spy, + } as any); + +const validator = getRuntimeFieldValidator('myIndex', search); + +describe('Runtime field validation', () => { + const expectedError = { + message: 'Error compiling the painless script', + position: { offset: 4, start: 0, end: 18 }, + reason: 'cannot resolve symbol [emit]', + scriptStack: ["emit.some('value')", ' ^---- HERE'], + }; + + [ + { + title: 'should return null when there are no errors', + response: {}, + status: 200, + expected: null, + }, + { + title: 'should return the error in the first failed shard', + response: { + attributes: { + type: 'status_exception', + reason: 'error while executing search', + caused_by: { + failed_shards: [ + { + shard: 0, + index: 'kibana_sample_data_logs', + node: 'gVwk20UWSdO6VyuNOc_6UA', + reason: { + type: 'script_exception', + script_stack: ["emit.some('value')", ' ^---- HERE'], + position: { offset: 4, start: 0, end: 18 }, + caused_by: { + type: 'illegal_argument_exception', + reason: 'cannot resolve symbol [emit]', + }, + }, + }, + ], + }, + }, + }, + status: 400, + expected: expectedError, + }, + { + title: 'should return the error in the third failed shard', + response: { + attributes: { + type: 'status_exception', + reason: 'error while executing search', + caused_by: { + failed_shards: [ + { + shard: 0, + index: 'kibana_sample_data_logs', + node: 'gVwk20UWSdO6VyuNOc_6UA', + reason: { + type: 'foo', + }, + }, + { + shard: 1, + index: 'kibana_sample_data_logs', + node: 'gVwk20UWSdO6VyuNOc_6UA', + reason: { + type: 'bar', + }, + }, + { + shard: 2, + index: 'kibana_sample_data_logs', + node: 'gVwk20UWSdO6VyuNOc_6UA', + reason: { + type: 'script_exception', + script_stack: ["emit.some('value')", ' ^---- HERE'], + position: { offset: 4, start: 0, end: 18 }, + caused_by: { + type: 'illegal_argument_exception', + reason: 'cannot resolve symbol [emit]', + }, + }, + }, + ], + }, + }, + }, + status: 400, + expected: expectedError, + }, + { + title: 'should have default values if an error prop is not found', + response: { + attributes: { + type: 'status_exception', + reason: 'error while executing search', + caused_by: { + failed_shards: [ + { + shard: 0, + index: 'kibana_sample_data_logs', + node: 'gVwk20UWSdO6VyuNOc_6UA', + reason: { + // script_stack, position and caused_by are missing + type: 'script_exception', + caused_by: { + type: 'illegal_argument_exception', + }, + }, + }, + ], + }, + }, + }, + status: 400, + expected: { + message: 'Error compiling the painless script', + position: null, + reason: null, + scriptStack: [], + }, + }, + ].map(({ title, response, status, expected }) => { + test(title, async () => { + if (status !== 200) { + spy.mockRejectedValueOnce(response); + } else { + spy.mockResolvedValueOnce(response); + } + + const result = await validator(runtimeField); + + expect(result).toEqual(expected); + }); + }); +}); diff --git a/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts new file mode 100644 index 00000000000000..f1a6fd7f9e8aa8 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { i18n } from '@kbn/i18n'; + +import { DataPublicPluginStart } from '../shared_imports'; +import { EsRuntimeField } from '../types'; + +export interface RuntimeFieldPainlessError { + message: string; + reason: string; + position: { + offset: number; + start: number; + end: number; + } | null; + scriptStack: string[]; +} + +type Error = Record; + +/** + * We are only interested in "script_exception" error type + */ +const getScriptExceptionErrorOnShard = (error: Error): Error | null => { + if (error.type === 'script_exception') { + return error; + } + + if (!error.caused_by) { + return null; + } + + // Recursively try to get a script exception error + return getScriptExceptionErrorOnShard(error.caused_by); +}; + +/** + * We get the first script exception error on any failing shard. + * The UI can only display one error at the time so there is no need + * to look any further. + */ +const getScriptExceptionError = (error: Error): Error | null => { + if (error === undefined || !Array.isArray(error.failed_shards)) { + return null; + } + + let scriptExceptionError = null; + for (const err of error.failed_shards) { + scriptExceptionError = getScriptExceptionErrorOnShard(err.reason); + + if (scriptExceptionError !== null) { + break; + } + } + return scriptExceptionError; +}; + +const parseEsError = (error?: Error): RuntimeFieldPainlessError | null => { + if (error === undefined) { + return null; + } + + const scriptError = getScriptExceptionError(error.caused_by); + + if (scriptError === null) { + return null; + } + + return { + message: i18n.translate( + 'indexPatternFieldEditor.editor.form.scriptEditor.compileErrorMessage', + { + defaultMessage: 'Error compiling the painless script', + } + ), + position: scriptError.position ?? null, + scriptStack: scriptError.script_stack ?? [], + reason: scriptError.caused_by?.reason ?? null, + }; +}; + +/** + * Handler to validate the painless script for syntax and semantic errors. + * This is a temporary solution. In a future work we will have a dedicate + * ES API to debug the script. + */ +export const getRuntimeFieldValidator = ( + index: string, + searchService: DataPublicPluginStart['search'] +) => async (runtimeField: EsRuntimeField) => { + return await searchService + .search({ + params: { + index, + body: { + runtime_mappings: { + temp: runtimeField, + }, + size: 0, + query: { + match_none: {}, + }, + }, + }, + }) + .toPromise() + .then(() => null) + .catch((e) => { + return parseEsError(e.attributes); + }); +}; diff --git a/src/plugins/index_pattern_field_editor/public/lib/serialization.ts b/src/plugins/index_pattern_field_editor/public/lib/serialization.ts new file mode 100644 index 00000000000000..9000a34b23cbea --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/lib/serialization.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IndexPatternField, IndexPattern } from '../shared_imports'; +import { Field } from '../types'; + +export const deserializeField = ( + indexPattern: IndexPattern, + field?: IndexPatternField +): Field | undefined => { + if (field === undefined) { + return undefined; + } + + return { + name: field.name, + type: field?.esTypes ? field.esTypes[0] : 'keyword', + script: field.runtimeField ? field.runtimeField.script : undefined, + customLabel: field.customLabel, + popularity: field.count, + format: indexPattern.getFormatterForFieldNoDefault(field.name)?.toJSON(), + }; +}; diff --git a/src/plugins/index_pattern_field_editor/public/mocks.ts b/src/plugins/index_pattern_field_editor/public/mocks.ts new file mode 100644 index 00000000000000..23bd4c385ca3b2 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/mocks.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IndexPatternFieldEditorPlugin } from './plugin'; + +export type Start = jest.Mocked< + Omit, 'DeleteRuntimeFieldProvider'> +> & { + DeleteRuntimeFieldProvider: ReturnType< + IndexPatternFieldEditorPlugin['start'] + >['DeleteRuntimeFieldProvider']; +}; + +export type Setup = jest.Mocked>; + +const createSetupContract = (): Setup => { + return { + fieldFormatEditors: { + register: jest.fn(), + } as any, + }; +}; + +const createStartContract = (): Start => { + return { + openEditor: jest.fn(), + fieldFormatEditors: { + getAll: jest.fn(), + getById: jest.fn(), + } as any, + userPermissions: { + editIndexPattern: jest.fn(), + }, + DeleteRuntimeFieldProvider: ({ children }) => children(jest.fn()) as JSX.Element, + }; +}; + +export const indexPatternFieldEditorPluginMock = { + createSetupContract, + createStartContract, +}; diff --git a/src/plugins/index_pattern_field_editor/public/open_editor.tsx b/src/plugins/index_pattern_field_editor/public/open_editor.tsx new file mode 100644 index 00000000000000..d4d1f71433ea74 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/open_editor.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { CoreStart, OverlayRef } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; + +import { + createKibanaReactContext, + toMountPoint, + IndexPatternField, + DataPublicPluginStart, + IndexPattern, + UsageCollectionStart, +} from './shared_imports'; + +import { InternalFieldType } from './types'; +import { FieldEditorFlyoutContentContainer } from './components/field_editor_flyout_content_container'; + +import { PluginStart } from './types'; + +export interface OpenFieldEditorOptions { + ctx: { + indexPattern: IndexPattern; + }; + onSave?: (field: IndexPatternField) => void; + fieldName?: string; +} + +type CloseEditor = () => void; +interface Dependencies { + core: CoreStart; + /** The search service from the data plugin */ + search: DataPublicPluginStart['search']; + indexPatternService: DataPublicPluginStart['indexPatterns']; + fieldFormats: DataPublicPluginStart['fieldFormats']; + fieldFormatEditors: PluginStart['fieldFormatEditors']; + usageCollection: UsageCollectionStart; +} + +export const getFieldEditorOpener = ({ + core, + indexPatternService, + fieldFormats, + fieldFormatEditors, + search, + usageCollection, +}: Dependencies) => (options: OpenFieldEditorOptions): CloseEditor => { + const { uiSettings, overlays, docLinks, notifications } = core; + const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ + uiSettings, + docLinks, + http: core.http, + }); + + let overlayRef: OverlayRef | null = null; + + const openEditor = ({ onSave, fieldName, ctx }: OpenFieldEditorOptions): CloseEditor => { + const closeEditor = () => { + if (overlayRef) { + overlayRef.close(); + overlayRef = null; + } + }; + + const onSaveField = (updatedField: IndexPatternField) => { + closeEditor(); + + if (onSave) { + onSave(updatedField); + } + }; + + const field = fieldName ? ctx.indexPattern.getFieldByName(fieldName) : undefined; + + if (fieldName && !field) { + const err = i18n.translate('indexPatternFieldEditor.noSuchFieldName', { + defaultMessage: "Field named '{fieldName}' not found on index pattern", + values: { fieldName }, + }); + notifications.toasts.addDanger(err); + return closeEditor; + } + + const isNewRuntimeField = !fieldName; + const isExistingRuntimeField = field && field.runtimeField && !field.isMapped; + const fieldTypeToProcess: InternalFieldType = + isNewRuntimeField || isExistingRuntimeField ? 'runtime' : 'concrete'; + + overlayRef = overlays.openFlyout( + toMountPoint( + + + + ) + ); + + return closeEditor; + }; + + return openEditor(options); +}; diff --git a/src/plugins/index_pattern_field_editor/public/plugin.test.tsx b/src/plugins/index_pattern_field_editor/public/plugin.test.tsx new file mode 100644 index 00000000000000..4870ecc827ab09 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/plugin.test.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; + +jest.mock('../../kibana_react/public', () => { + const original = jest.requireActual('../../kibana_react/public'); + + return { + ...original, + toMountPoint: (node: React.ReactNode) => node, + }; +}); + +import { CoreStart } from 'src/core/public'; +import { coreMock } from 'src/core/public/mocks'; +import { dataPluginMock } from '../../data/public/mocks'; +import { usageCollectionPluginMock } from '../../usage_collection/public/mocks'; + +import { registerTestBed } from './test_utils'; + +import { FieldEditorFlyoutContentContainer } from './components/field_editor_flyout_content_container'; +import { IndexPatternFieldEditorPlugin } from './plugin'; + +const noop = () => {}; + +describe('IndexPatternFieldEditorPlugin', () => { + const coreStart: CoreStart = coreMock.createStart(); + const pluginStart = { + data: dataPluginMock.createStartContract(), + usageCollection: usageCollectionPluginMock.createSetupContract(), + }; + + let plugin: IndexPatternFieldEditorPlugin; + + beforeEach(() => { + plugin = new IndexPatternFieldEditorPlugin(); + }); + + test('should expose a handler to open the indexpattern field editor', async () => { + const startApi = await plugin.start(coreStart, pluginStart); + expect(startApi.openEditor).toBeDefined(); + }); + + test('should call core.overlays.openFlyout when opening the editor', async () => { + const openFlyout = jest.fn(); + const onSaveSpy = jest.fn(); + + const coreStartMocked = { + ...coreStart, + overlays: { + ...coreStart.overlays, + openFlyout, + }, + }; + const { openEditor } = await plugin.start(coreStartMocked, pluginStart); + + openEditor({ onSave: onSaveSpy, ctx: { indexPattern: {} as any } }); + + expect(openFlyout).toHaveBeenCalled(); + + const [[arg]] = openFlyout.mock.calls; + expect(arg.props.children.type).toBe(FieldEditorFlyoutContentContainer); + + // We force call the "onSave" prop from the component + // and make sure that the the spy is being called. + // Note: we are testing implementation details, if we change or rename the "onSave" prop on + // the component, we will need to update this test accordingly. + expect(arg.props.children.props.onSave).toBeDefined(); + arg.props.children.props.onSave(); + expect(onSaveSpy).toHaveBeenCalled(); + }); + + test('should return a handler to close the flyout', async () => { + const { openEditor } = await plugin.start(coreStart, pluginStart); + + const closeEditorHandler = openEditor({ onSave: noop, ctx: { indexPattern: {} as any } }); + expect(typeof closeEditorHandler).toBe('function'); + }); + + test('should expose a render props component to delete runtime fields', async () => { + const { DeleteRuntimeFieldProvider } = await plugin.start(coreStart, pluginStart); + + const TestComponent = ({ callback }: { callback: (...args: any[]) => void }) => { + return ( + + {(...args) => { + // Forward arguments passed down to children to our spy callback + callback(args); + return null; + }} + + ); + }; + + const setup = registerTestBed(TestComponent, { + memoryRouter: { wrapComponent: false }, + }); + + const spy = jest.fn(); + // Mount our dummy component and pass it the spy + setup({ callback: spy }); + + expect(spy).toHaveBeenCalled(); + const argumentsFromRenderProps = spy.mock.calls[0][0]; + + expect(argumentsFromRenderProps.length).toBe(1); + expect(typeof argumentsFromRenderProps[0]).toBe('function'); + }); +}); diff --git a/src/plugins/index_pattern_field_editor/public/plugin.ts b/src/plugins/index_pattern_field_editor/public/plugin.ts new file mode 100644 index 00000000000000..c3736b50c344cb --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/plugin.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Plugin, CoreSetup, CoreStart } from 'src/core/public'; + +import { PluginSetup, PluginStart, SetupPlugins, StartPlugins } from './types'; +import { getFieldEditorOpener } from './open_editor'; +import { FormatEditorService } from './service'; +import { getDeleteProvider } from './components/delete_field_provider'; + +export class IndexPatternFieldEditorPlugin + implements Plugin { + private readonly formatEditorService = new FormatEditorService(); + + public setup(core: CoreSetup, plugins: SetupPlugins): PluginSetup { + const { fieldFormatEditors } = this.formatEditorService.setup(); + + return { + fieldFormatEditors, + }; + } + + public start(core: CoreStart, plugins: StartPlugins) { + const { fieldFormatEditors } = this.formatEditorService.start(); + const { + application: { capabilities }, + } = core; + const { data, usageCollection } = plugins; + return { + fieldFormatEditors, + openEditor: getFieldEditorOpener({ + core, + indexPatternService: data.indexPatterns, + fieldFormats: data.fieldFormats, + fieldFormatEditors, + search: data.search, + usageCollection, + }), + userPermissions: { + editIndexPattern: () => { + return capabilities.management.kibana.indexPatterns; + }, + }, + DeleteRuntimeFieldProvider: getDeleteProvider( + data.indexPatterns, + usageCollection, + core.notifications + ), + }; + } + + public stop() { + return {}; + } +} diff --git a/src/plugins/index_pattern_management/public/service/field_format_editors/field_format_editors.ts b/src/plugins/index_pattern_field_editor/public/service/field_format_editors/field_format_editors.ts similarity index 89% rename from src/plugins/index_pattern_management/public/service/field_format_editors/field_format_editors.ts rename to src/plugins/index_pattern_field_editor/public/service/field_format_editors/field_format_editors.ts index d5335cdf0f06e7..fdc54a39c8c2a6 100644 --- a/src/plugins/index_pattern_management/public/service/field_format_editors/field_format_editors.ts +++ b/src/plugins/index_pattern_field_editor/public/service/field_format_editors/field_format_editors.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { DefaultFormatEditor } from '../../components/field_editor/components/field_format_editor'; +import { DefaultFormatEditor } from '../../components/field_format_editor'; export class FieldFormatEditors { private editors: Array = []; diff --git a/src/plugins/index_pattern_management/public/service/field_format_editors/index.ts b/src/plugins/index_pattern_field_editor/public/service/field_format_editors/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/service/field_format_editors/index.ts rename to src/plugins/index_pattern_field_editor/public/service/field_format_editors/index.ts diff --git a/src/plugins/index_pattern_field_editor/public/service/format_editor_service.ts b/src/plugins/index_pattern_field_editor/public/service/format_editor_service.ts new file mode 100644 index 00000000000000..67064e8a9cdbf9 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/service/format_editor_service.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FieldFormatEditors } from './field_format_editors'; + +import { + BytesFormatEditor, + ColorFormatEditor, + DateFormatEditor, + DateNanosFormatEditor, + DurationFormatEditor, + NumberFormatEditor, + PercentFormatEditor, + StaticLookupFormatEditor, + StringFormatEditor, + TruncateFormatEditor, + UrlFormatEditor, +} from '../components'; + +/** + * Index patterns management service + * + * @internal + */ +export class FormatEditorService { + fieldFormatEditors: FieldFormatEditors; + + constructor() { + this.fieldFormatEditors = new FieldFormatEditors(); + } + + public setup() { + const defaultFieldFormatEditors = [ + BytesFormatEditor, + ColorFormatEditor, + DateFormatEditor, + DateNanosFormatEditor, + DurationFormatEditor, + NumberFormatEditor, + PercentFormatEditor, + StaticLookupFormatEditor, + StringFormatEditor, + TruncateFormatEditor, + UrlFormatEditor, + ]; + + const fieldFormatEditorsSetup = this.fieldFormatEditors.setup(defaultFieldFormatEditors); + + return { + fieldFormatEditors: fieldFormatEditorsSetup, + }; + } + + public start() { + return { + fieldFormatEditors: this.fieldFormatEditors.start(), + }; + } + + public stop() { + // nothing to do here yet. + } +} + +/** @internal */ +export type FormatEditorServiceSetup = ReturnType; +export type FormatEditorServiceStart = ReturnType; diff --git a/src/plugins/index_pattern_field_editor/public/service/index.ts b/src/plugins/index_pattern_field_editor/public/service/index.ts new file mode 100644 index 00000000000000..700d79459327c1 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/service/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './format_editor_service'; diff --git a/src/plugins/index_pattern_field_editor/public/shared_imports.ts b/src/plugins/index_pattern_field_editor/public/shared_imports.ts new file mode 100644 index 00000000000000..9caa5e093a96f1 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/shared_imports.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { + IndexPattern, + IndexPatternField, + DataPublicPluginStart, + FieldFormat, +} from '../../data/public'; + +export { UsageCollectionStart } from '../../usage_collection/public'; + +export { RuntimeType, RuntimeField, KBN_FIELD_TYPES, ES_FIELD_TYPES } from '../../data/common'; + +export { createKibanaReactContext, toMountPoint, CodeEditor } from '../../kibana_react/public'; + +export { + useForm, + useFormData, + useFormContext, + Form, + FormSchema, + UseField, + FormHook, + ValidationFunc, + FieldConfig, +} from '../../es_ui_shared/static/forms/hook_form_lib'; + +export { fieldValidators } from '../../es_ui_shared/static/forms/helpers'; + +export { TextField, ToggleField, NumericField } from '../../es_ui_shared/static/forms/components'; diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts b/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts new file mode 100644 index 00000000000000..295c32cf28e78d --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { TestBed } from './test_utils'; + +export const getCommonActions = (testBed: TestBed) => { + const toggleFormRow = (row: 'customLabel' | 'value' | 'format', value: 'on' | 'off' = 'on') => { + const testSubj = `${row}Row.toggle`; + const toggle = testBed.find(testSubj); + const isOn = toggle.props()['aria-checked']; + + if ((value === 'on' && isOn) || (value === 'off' && isOn === false)) { + return; + } + + testBed.form.toggleEuiSwitch(testSubj); + }; + + return { + toggleFormRow, + }; +}; diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/index.ts b/src/plugins/index_pattern_field_editor/public/test_utils/index.ts new file mode 100644 index 00000000000000..b5d943281cd79b --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/test_utils/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './test_utils'; + +export * from './mocks'; + +export * from './helpers'; diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/mocks.ts b/src/plugins/index_pattern_field_editor/public/test_utils/mocks.ts new file mode 100644 index 00000000000000..c6bc24f1768588 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/test_utils/mocks.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DocLinksStart } from 'src/core/public'; + +export const noop = () => {}; + +export const docLinks: DocLinksStart = { + ELASTIC_WEBSITE_URL: 'htts://jestTest.elastic.co', + DOC_LINK_VERSION: 'jest', + links: {} as any, +}; + +// TODO check how we can better stub an index pattern format +export const fieldFormats = { + getDefaultInstance: () => ({ + convert: (val: any) => val, + }), +} as any; diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/setup_environment.tsx b/src/plugins/index_pattern_field_editor/public/test_utils/setup_environment.tsx new file mode 100644 index 00000000000000..885bcc87f89df9 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/test_utils/setup_environment.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +const EDITOR_ID = 'testEditor'; + +jest.mock('../../../kibana_react/public', () => { + const original = jest.requireActual('../../../kibana_react/public'); + + /** + * We mock the CodeEditor because it requires the + * with the uiSettings passed down. Let's use a simple in our tests. + */ + const CodeEditorMock = (props: any) => { + // Forward our deterministic ID to the consumer + // We need below for the PainlessLang.getSyntaxErrors mock + props.editorDidMount({ + getModel() { + return { + id: EDITOR_ID, + }; + }, + }); + + return ( + ) => { + props.onChange(e.target.value); + }} + /> + ); + }; + + return { + ...original, + CodeEditor: CodeEditorMock, + }; +}); + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + EuiComboBox: (props: any) => ( + { + props.onChange([syntheticEvent['0']]); + }} + /> + ), + }; +}); + +jest.mock('@kbn/monaco', () => { + const original = jest.requireActual('@kbn/monaco'); + + return { + ...original, + PainlessLang: { + ID: 'painless', + getSuggestionProvider: () => undefined, + getSyntaxErrors: () => ({ + [EDITOR_ID]: [], + }), + }, + }; +}); diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/test_utils.ts b/src/plugins/index_pattern_field_editor/public/test_utils/test_utils.ts new file mode 100644 index 00000000000000..c8e4aedc264716 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/test_utils/test_utils.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { getRandomString } from '@kbn/test/jest'; + +export { registerTestBed, TestBed } from '@kbn/test/jest'; diff --git a/src/plugins/index_pattern_field_editor/public/types.ts b/src/plugins/index_pattern_field_editor/public/types.ts new file mode 100644 index 00000000000000..363af9ceb20fbb --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/types.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FunctionComponent } from 'react'; + +import { + DataPublicPluginStart, + RuntimeField, + RuntimeType, + UsageCollectionStart, +} from './shared_imports'; +import { OpenFieldEditorOptions } from './open_editor'; +import { FormatEditorServiceSetup, FormatEditorServiceStart } from './service'; +import { DeleteProviderProps } from './components/delete_field_provider'; + +export interface PluginSetup { + fieldFormatEditors: FormatEditorServiceSetup['fieldFormatEditors']; +} + +export interface PluginStart { + openEditor(options: OpenFieldEditorOptions): () => void; + fieldFormatEditors: FormatEditorServiceStart['fieldFormatEditors']; + userPermissions: { + editIndexPattern: () => boolean; + }; + DeleteRuntimeFieldProvider: FunctionComponent; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SetupPlugins {} + +export interface StartPlugins { + data: DataPublicPluginStart; + usageCollection: UsageCollectionStart; +} + +export type InternalFieldType = 'concrete' | 'runtime'; + +export interface Field { + name: string; + type: RuntimeField['type'] | string; + script?: RuntimeField['script']; + customLabel?: string; + popularity?: number; + format?: FieldFormatConfig; +} + +export interface FieldFormatConfig { + id: string; + params?: { [key: string]: any }; +} + +export interface EsRuntimeField { + type: RuntimeType | string; + script?: { + source: string; + }; +} diff --git a/src/plugins/index_pattern_field_editor/tsconfig.json b/src/plugins/index_pattern_field_editor/tsconfig.json new file mode 100644 index 00000000000000..559b1aaf0fc26c --- /dev/null +++ b/src/plugins/index_pattern_field_editor/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../es_ui_shared/tsconfig.json" }, + ] +} diff --git a/src/plugins/index_pattern_management/kibana.json b/src/plugins/index_pattern_management/kibana.json index 6c3025485bbd7c..60e382fb395f77 100644 --- a/src/plugins/index_pattern_management/kibana.json +++ b/src/plugins/index_pattern_management/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["management", "data", "urlForwarding"], + "requiredPlugins": ["management", "data", "urlForwarding", "indexPatternFieldEditor"], "requiredBundles": ["kibanaReact", "kibanaUtils"] } diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap index 0e5fc0582f72c5..70b638d5d0b8d4 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap @@ -18,6 +18,8 @@ exports[`CreateIndexPatternWizard renders index pattern step when there are indi
{indexPattern.title} }} + defaultMessage="View and edit fields in {indexPatternTitle}. Field attributes, such as type and searchability, are based on {mappingAPILink} in Elasticsearch." + values={{ + indexPatternTitle: {indexPattern.title}, + mappingAPILink: ( + + {mappingAPILink} + + ), + }} />{' '} - - {mappingAPILink} -

{conflictedFields.length > 0 && ( @@ -203,6 +207,9 @@ export const EditIndexPattern = withRouter( fields={fields} history={history} location={location} + refreshFields={() => { + setFields(indexPattern.getNonScriptedFields()); + }} /> diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap index 8e7fac9c6c1483..6e5e652b8d0eb3 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap @@ -3,6 +3,7 @@ exports[`IndexedFieldsTable should filter based on the query bar 1`] = `
`; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx index c5ef40be9c0657..9c154ce1b0e7ba 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx @@ -23,99 +23,84 @@ const items: IndexedFieldItem[] = [ searchable: true, info: [], type: 'name', + kbnType: 'string', excluded: false, format: '', + isMapped: true, }, { name: 'timestamp', displayName: 'timestamp', type: 'date', + kbnType: 'date', info: [], excluded: false, format: 'YYYY-MM-DD', + isMapped: true, }, { name: 'conflictingField', displayName: 'conflictingField', - type: 'conflict', + type: 'text, long', + kbnType: 'conflict', info: [], excluded: false, format: '', + isMapped: true, }, ]; +const renderTable = ( + { editField } = { + editField: () => {}, + } +) => + shallow( +
{}} /> + ); + describe('Table', () => { test('should render normally', () => { - const component = shallow( -
{}} /> - ); - - expect(component).toMatchSnapshot(); + expect(renderTable()).toMatchSnapshot(); }); test('should render normal field name', () => { - const component = shallow( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[0].render('Elastic', items[0])); + const tableCell = shallow(renderTable().prop('columns')[0].render('Elastic', items[0])); expect(tableCell).toMatchSnapshot(); }); test('should render timestamp field name', () => { - const component = shallow( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[0].render('timestamp', items[1])); + const tableCell = shallow(renderTable().prop('columns')[0].render('timestamp', items[1])); expect(tableCell).toMatchSnapshot(); }); test('should render the boolean template (true)', () => { - const component = shallow( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[3].render(true)); + const tableCell = shallow(renderTable().prop('columns')[3].render(true)); expect(tableCell).toMatchSnapshot(); }); test('should render the boolean template (false)', () => { - const component = shallow( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[3].render(false, items[2])); + const tableCell = shallow(renderTable().prop('columns')[3].render(false, items[2])); expect(tableCell).toMatchSnapshot(); }); test('should render normal type', () => { - const component = shallow( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[1].render('string')); + const tableCell = shallow(renderTable().prop('columns')[1].render('string', {})); expect(tableCell).toMatchSnapshot(); }); test('should render conflicting type', () => { - const component = shallow( -
{}} /> + const tableCell = shallow( + renderTable().prop('columns')[1].render('conflict', { kbnType: 'conflict' }) ); - - const tableCell = shallow(component.prop('columns')[1].render('conflict', true)); expect(tableCell).toMatchSnapshot(); }); test('should allow edits', () => { const editField = jest.fn(); - const component = shallow( -
- ); - // Click the edit button - component.prop('columns')[6].actions[0].onClick(); + renderTable({ editField }).prop('columns')[6].actions[0].onClick(); expect(editField).toBeCalled(); }); }); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx index 58080722d1bced..4e9a2bb6451125 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx @@ -140,6 +140,18 @@ const editDescription = i18n.translate( { defaultMessage: 'Edit' } ); +const deleteLabel = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.table.deleteLabel', + { + defaultMessage: 'Delete', + } +); + +const deleteDescription = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.table.deleteDescription', + { defaultMessage: 'Delete' } +); + const labelDescription = i18n.translate( 'indexPatternManagement.editIndexPattern.fields.table.customLabelTooltip', { defaultMessage: 'A custom label for the field.' } @@ -149,6 +161,7 @@ interface IndexedFieldProps { indexPattern: IIndexPattern; items: IndexedFieldItem[]; editField: (field: IndexedFieldItem) => void; + deleteField: (fieldName: string) => void; } export class Table extends PureComponent { @@ -221,7 +234,7 @@ export class Table extends PureComponent { } render() { - const { items, editField } = this.props; + const { items, editField, deleteField } = this.props; const pagination = { initialPageSize: 10, @@ -245,8 +258,8 @@ export class Table extends PureComponent { name: typeHeader, dataType: 'string', sortable: true, - render: (value: string) => { - return this.renderFieldType(value, value === 'conflict'); + render: (value: string, field: IndexedFieldItem) => { + return this.renderFieldType(value, field.kbnType === 'conflict'); }, 'data-test-subj': 'indexedFieldType', }, @@ -294,10 +307,30 @@ export class Table extends PureComponent { ], width: '40px', }, + { + name: '', + actions: [ + { + name: deleteLabel, + description: deleteDescription, + icon: 'trash', + onClick: (field) => deleteField(field.name), + type: 'icon', + 'data-test-subj': 'deleteField', + available: (field) => !field.isMapped, + }, + ], + width: '40px', + }, ]; return ( - + ); } } diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx index 8786f2f4e8deca..e587ada6695cb4 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx @@ -26,7 +26,8 @@ jest.mock('./components/table', () => ({ })); const helpers = { - redirectToRoute: (obj: any) => {}, + editField: (fieldName: string) => {}, + deleteField: (fieldName: string) => {}, getFieldInfo: () => [], }; @@ -35,7 +36,9 @@ const indexPattern = ({ getFormatterForFieldNoDefault: () => ({ params: () => ({}) }), } as unknown) as IndexPattern; -const mockFieldToIndexPatternField = (spec: Record) => { +const mockFieldToIndexPatternField = ( + spec: Record +) => { return new IndexPatternField((spec as unknown) as IndexPatternField['spec']); }; @@ -44,10 +47,10 @@ const fields = [ name: 'Elastic', displayName: 'Elastic', searchable: true, - type: 'string', + esTypes: ['keyword'], }, - { name: 'timestamp', displayName: 'timestamp', type: 'date' }, - { name: 'conflictingField', displayName: 'conflictingField', type: 'conflict' }, + { name: 'timestamp', displayName: 'timestamp', esTypes: ['date'] }, + { name: 'conflictingField', displayName: 'conflictingField', esTypes: ['keyword', 'long'] }, ].map(mockFieldToIndexPatternField); describe('IndexedFieldsTable', () => { diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 95458b55dbf2a7..c703a882d38d6a 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -18,7 +18,8 @@ interface IndexedFieldsTableProps { fieldFilter?: string; indexedFieldTypeFilter?: string; helpers: { - redirectToRoute: (obj: any) => void; + editField: (fieldName: string) => void; + deleteField: (fieldName: string) => void; getFieldInfo: (indexPattern: IndexPattern, field: IFieldType) => string[]; }; fieldWildcardMatcher: (filters: any[]) => (val: any) => boolean; @@ -60,10 +61,13 @@ export class IndexedFieldsTable extends Component< fields.map((field) => { return { ...field.spec, + type: field.esTypes?.join(', ') || '', + kbnType: field.type, displayName: field.displayName, format: indexPattern.getFormatterForFieldNoDefault(field.name)?.type?.title || '', excluded: fieldWildcardMatch ? fieldWildcardMatch(field.name) : false, info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field), + isMapped: !!field.isMapped, }; })) || [] @@ -102,7 +106,8 @@ export class IndexedFieldsTable extends Component<
this.props.helpers.redirectToRoute(field)} + editField={(field) => this.props.helpers.editField(field.name)} + deleteField={(fieldName) => this.props.helpers.deleteField(fieldName)} /> ); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts index dee8f5b0d775f9..47ae84b4d2fd8f 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts @@ -11,4 +11,6 @@ import { IFieldType } from '../../../../../../plugins/data/public'; export interface IndexedFieldItem extends IFieldType { info: string[]; excluded: boolean; + kbnType: string; + isMapped: boolean; } diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx index ac57c6ffd78ed9..7771c5d54f4157 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState, useCallback, useEffect, Fragment, useMemo } from 'react'; +import React, { useState, useCallback, useEffect, Fragment, useMemo, useRef } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { EuiFlexGroup, @@ -17,6 +17,7 @@ import { EuiFieldSearch, EuiSelect, EuiSelectOption, + EuiButton, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { fieldWildcardMatcher } from '../../../../../kibana_utils/public'; @@ -39,6 +40,7 @@ interface TabsProps extends Pick { indexPattern: IndexPattern; fields: IndexPatternField[]; saveIndexPattern: DataPublicPluginStart['indexPatterns']['updateSavedObject']; + refreshFields: () => void; } const searchAriaLabel = i18n.translate( @@ -62,11 +64,26 @@ const filterPlaceholder = i18n.translate( } ); -export function Tabs({ indexPattern, saveIndexPattern, fields, history, location }: TabsProps) { +const addFieldButtonLabel = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.addFieldButtonLabel', + { + defaultMessage: 'Add field', + } +); + +export function Tabs({ + indexPattern, + saveIndexPattern, + fields, + history, + location, + refreshFields, +}: TabsProps) { const { uiSettings, indexPatternManagementStart, docLinks, + indexPatternFieldEditor, } = useKibana().services; const [fieldFilter, setFieldFilter] = useState(''); const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState(''); @@ -76,6 +93,8 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location const [syncingStateFunc, setSyncingStateFunc] = useState({ getCurrentTab: () => TAB_INDEXED_FIELDS, }); + const closeEditorHandler = useRef<() => void | undefined>(); + const { DeleteRuntimeFieldProvider } = indexPatternFieldEditor; const refreshFilters = useCallback(() => { const tempIndexedFieldTypes: string[] = []; @@ -86,7 +105,9 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location tempScriptedFieldLanguages.push(field.lang); } } else { - tempIndexedFieldTypes.push(field.type); + if (field.esTypes) { + tempIndexedFieldTypes.push(field.esTypes?.join(', ')); + } } }); @@ -96,10 +117,36 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location ); }, [indexPattern]); + const closeFieldEditor = useCallback(() => { + if (closeEditorHandler.current) { + closeEditorHandler.current(); + } + }, []); + + const openFieldEditor = useCallback( + (fieldName?: string) => { + closeEditorHandler.current = indexPatternFieldEditor.openEditor({ + ctx: { + indexPattern, + }, + onSave: refreshFields, + fieldName, + }); + }, + [indexPatternFieldEditor, indexPattern, refreshFields] + ); + useEffect(() => { refreshFilters(); }, [indexPattern, indexPattern.fields, refreshFilters]); + useEffect(() => { + return () => { + // When the component unmounts, make sure to close the field editor + closeFieldEditor(); + }; + }, [closeFieldEditor]); + const fieldWildcardMatcherDecorated = useCallback( (filters: string[]) => fieldWildcardMatcher(filters, uiSettings.get(UI_SETTINGS.META_FIELDS)), [uiSettings] @@ -120,15 +167,22 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location /> {type === TAB_INDEXED_FIELDS && indexedFieldTypes.length > 0 && ( - - setIndexedFieldTypeFilter(e.target.value)} - data-test-subj="indexedFieldTypeFilterDropdown" - aria-label={filterAriaLabel} - /> - + <> + + setIndexedFieldTypeFilter(e.target.value)} + data-test-subj="indexedFieldTypeFilterDropdown" + aria-label={filterAriaLabel} + /> + + + openFieldEditor()}> + {addFieldButtonLabel} + + + )} {type === TAB_SCRIPTED_FIELDS && scriptedFieldLanguages.length > 0 && ( @@ -149,6 +203,7 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location indexedFieldTypes, scriptedFieldLanguageFilter, scriptedFieldLanguages, + openFieldEditor, ] ); @@ -161,19 +216,22 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location {getFilterSection(type)} - { - history.push(getPath(field, indexPattern)); - }, - getFieldInfo: indexPatternManagementStart.list.getFieldInfo, - }} - /> + + {(deleteField) => ( + + )} + ); case TAB_SCRIPTED_FIELDS: @@ -227,6 +285,9 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location refreshFilters, scriptedFieldLanguageFilter, saveIndexPattern, + openFieldEditor, + DeleteRuntimeFieldProvider, + refreshFields, ] ); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap deleted file mode 100644 index 38f630358d064c..00000000000000 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap +++ /dev/null @@ -1,109 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LabelTemplateFlyout should not render if not visible 1`] = `""`; - -exports[`LabelTemplateFlyout should render normally 1`] = ` - - - -

- -

-

- - {{ }} - , - } - } - /> -

-
    -
  • - - value - - —  - -
  • -
  • - - url - - —  - -
  • -
-

- -

- User #1234", - "urlTemplate": "http://company.net/profiles?user_id={{value}}", - }, - Object { - "input": "/assets/main.css", - "labelTemplate": "View Asset", - "output": "View Asset", - "urlTemplate": "http://site.com{{rawValue}}", - }, - ] - } - noItemsMessage="No items found" - responsive={true} - tableLayout="fixed" - /> -
-
-
-`; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap deleted file mode 100644 index 83e815dd72661c..00000000000000 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap +++ /dev/null @@ -1,114 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UrlTemplateFlyout should not render if not visible 1`] = `""`; - -exports[`UrlTemplateFlyout should render normally 1`] = ` - - - -

- -

-

- - {{ }} - , - "strongUrlTemplate": - - , - } - } - /> -

-
    -
  • - - value - - —  - -
  • -
  • - - rawValue - - —  - -
  • -
-

- -

- -
-
-
-`; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.tsx deleted file mode 100644 index 0af6ba062e86b8..00000000000000 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { shallowWithI18nProvider } from '@kbn/test/jest'; - -import { LabelTemplateFlyout } from './label_template_flyout'; - -describe('LabelTemplateFlyout', () => { - it('should render normally', async () => { - const component = shallowWithI18nProvider(); - expect(component).toMatchSnapshot(); - }); - - it('should not render if not visible', async () => { - const component = shallowWithI18nProvider(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx deleted file mode 100644 index 0ce1858a5cc44c..00000000000000 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; - -import { EuiBasicTable, EuiCode, EuiFlyout, EuiFlyoutBody, EuiText } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -interface LabelTemplateExampleItem { - input: string | number; - urlTemplate: string; - labelTemplate: string; - output: string; -} - -const items: LabelTemplateExampleItem[] = [ - { - input: 1234, - urlTemplate: 'http://company.net/profiles?user_id={{value}}', - labelTemplate: i18n.translate('indexPatternManagement.labelTemplate.example.idLabel', { - defaultMessage: 'User #{value}', - values: { value: '{{value}}' }, - }), - output: - '' + - i18n.translate('indexPatternManagement.labelTemplate.example.output.idLabel', { - defaultMessage: 'User', - }) + - ' #1234', - }, - { - input: '/assets/main.css', - urlTemplate: 'http://site.com{{rawValue}}', - labelTemplate: i18n.translate('indexPatternManagement.labelTemplate.example.pathLabel', { - defaultMessage: 'View Asset', - }), - output: - '' + - i18n.translate('indexPatternManagement.labelTemplate.example.output.pathLabel', { - defaultMessage: 'View Asset', - }) + - '', - }, -]; - -export const LabelTemplateFlyout = ({ isVisible = false, onClose = () => {} }) => { - return isVisible ? ( - - - -

- -

-

- {'{{ }}'} }} - /> -

-
    -
  • - value —  - -
  • -
  • - url —  - -
  • -
-

- -

- - items={items} - columns={[ - { - field: 'input', - name: i18n.translate('indexPatternManagement.labelTemplate.inputHeader', { - defaultMessage: 'Input', - }), - width: '160px', - }, - { - field: 'urlTemplate', - name: i18n.translate('indexPatternManagement.labelTemplate.urlHeader', { - defaultMessage: 'URL Template', - }), - }, - { - field: 'labelTemplate', - name: i18n.translate('indexPatternManagement.labelTemplate.labelHeader', { - defaultMessage: 'Label Template', - }), - }, - { - field: 'output', - name: i18n.translate('indexPatternManagement.labelTemplate.outputHeader', { - defaultMessage: 'Output', - }), - render: (value: LabelTemplateExampleItem['output']) => { - return ( - - ); - }, - }, - ]} - /> -
-
-
- ) : null; -}; - -LabelTemplateFlyout.displayName = 'LabelTemplateFlyout'; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.test.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.test.tsx deleted file mode 100644 index bbdb18da901d1f..00000000000000 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { shallowWithI18nProvider } from '@kbn/test/jest'; - -import { UrlTemplateFlyout } from './url_template_flyout'; - -describe('UrlTemplateFlyout', () => { - it('should render normally', async () => { - const component = shallowWithI18nProvider(); - expect(component).toMatchSnapshot(); - }); - - it('should not render if not visible', async () => { - const component = shallowWithI18nProvider(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx deleted file mode 100644 index fc2b8d72536ec6..00000000000000 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; - -import { EuiBasicTable, EuiCode, EuiFlyout, EuiFlyoutBody, EuiText } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export const UrlTemplateFlyout = ({ isVisible = false, onClose = () => {} }) => { - return isVisible ? ( - - - -

- -

-

- {'{{ }}'}, - strongUrlTemplate: ( - - - - ), - }} - /> -

-
    -
  • - value —  - -
  • -
  • - rawValue —  - -
  • -
-

- -

- -
-
-
- ) : null; -}; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.test.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.test.tsx index d9352f18e96731..78dc87f7a8027a 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.test.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.test.tsx @@ -10,7 +10,7 @@ import React, { PureComponent } from 'react'; import { shallow } from 'enzyme'; import { FieldFormatEditor } from './field_format_editor'; -import { DefaultFormatEditor } from './editors/default'; +import type { DefaultFormatEditor } from 'src/plugins/index_pattern_field_editor/public'; class TestEditor extends PureComponent { render() { diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.tsx index 81abf2b5b1d203..60107e19170c77 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.tsx @@ -7,7 +7,7 @@ */ import React, { PureComponent, Fragment } from 'react'; -import { DefaultFormatEditor } from '../../components/field_format_editor/editors/default'; +import type { DefaultFormatEditor } from 'src/plugins/index_pattern_field_editor/public'; export interface FieldFormatEditorProps { fieldType: string; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/index.ts b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/index.ts index 7eea994a3e2d2f..66d9760b24c657 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/index.ts +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/index.ts @@ -7,4 +7,3 @@ */ export { FieldFormatEditor } from './field_format_editor'; -export * from './editors'; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx index 829536063a26c9..f0da57a5f9b6f3 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx @@ -487,7 +487,7 @@ export class FieldEditor extends PureComponent diff --git a/src/plugins/index_pattern_management/public/index.ts b/src/plugins/index_pattern_management/public/index.ts index 27e405a4113de9..94611705a93908 100644 --- a/src/plugins/index_pattern_management/public/index.ts +++ b/src/plugins/index_pattern_management/public/index.ts @@ -31,6 +31,4 @@ export { IndexPatternListConfig, } from './service'; -export { DefaultFormatEditor } from './components/field_editor/components/field_format_editor'; - export { MlCardState } from './types'; diff --git a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx index 45941969dbed12..e47f60ad6fcdd6 100644 --- a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx @@ -42,7 +42,7 @@ export async function mountManagementSection( ) { const [ { chrome, application, savedObjects, uiSettings, notifications, overlays, http, docLinks }, - { data }, + { data, indexPatternFieldEditor }, indexPatternManagementStart, ] = await getStartServices(); const canSave = Boolean(application.capabilities.indexPatterns.save); @@ -61,9 +61,11 @@ export async function mountManagementSection( http, docLinks, data, + indexPatternFieldEditor, indexPatternManagementStart: indexPatternManagementStart as IndexPatternManagementStart, setBreadcrumbs: params.setBreadcrumbs, getMlCardState, + fieldFormatEditors: indexPatternFieldEditor.fieldFormatEditors, }; ReactDOM.render( diff --git a/src/plugins/index_pattern_management/public/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts index 974b1ae1bd8630..309d5a5611cd62 100644 --- a/src/plugins/index_pattern_management/public/mocks.ts +++ b/src/plugins/index_pattern_management/public/mocks.ts @@ -11,11 +11,13 @@ import { coreMock } from '../../../core/public/mocks'; import { managementPluginMock } from '../../management/public/mocks'; import { urlForwardingPluginMock } from '../../url_forwarding/public/mocks'; import { dataPluginMock } from '../../data/public/mocks'; +import { indexPatternFieldEditorPluginMock } from '../../index_pattern_field_editor/public/mocks'; import { IndexPatternManagementSetup, IndexPatternManagementStart, IndexPatternManagementPlugin, } from './plugin'; +import { IndexPatternManagmentContext } from './types'; const createSetupContract = (): IndexPatternManagementSetup => ({ creation: { @@ -24,10 +26,6 @@ const createSetupContract = (): IndexPatternManagementSetup => ({ list: { addListConfig: jest.fn(), } as any, - fieldFormatEditors: { - getAll: jest.fn(), - getById: jest.fn(), - } as any, environment: { update: jest.fn(), }, @@ -43,10 +41,6 @@ const createStartContract = (): IndexPatternManagementStart => ({ getFieldInfo: jest.fn(), areScriptedFieldsEnabled: jest.fn(), } as any, - fieldFormatEditors: { - getAll: jest.fn(), - getById: jest.fn(), - } as any, }); const createInstance = async () => { @@ -59,6 +53,7 @@ const createInstance = async () => { const doStart = () => plugin.start(coreMock.createStart(), { data: dataPluginMock.createStartContract(), + indexPatternFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(), }); return { @@ -69,13 +64,17 @@ const createInstance = async () => { }; const docLinks = { + ELASTIC_WEBSITE_URL: 'htts://jestTest.elastic.co', + DOC_LINK_VERSION: 'jest', links: { indexPatterns: {}, scriptedFields: {}, - }, + } as any, }; -const createIndexPatternManagmentContext = () => { +const createIndexPatternManagmentContext = (): { + [key in keyof IndexPatternManagmentContext]: any; +} => { const { chrome, application, @@ -86,6 +85,7 @@ const createIndexPatternManagmentContext = () => { } = coreMock.createStart(); const { http } = coreMock.createSetup(); const data = dataPluginMock.createStartContract(); + const indexPatternFieldEditor = indexPatternFieldEditorPluginMock.createStartContract(); return { chrome, @@ -97,8 +97,11 @@ const createIndexPatternManagmentContext = () => { http, docLinks, data, + indexPatternFieldEditor, indexPatternManagementStart: createStartContract(), setBreadcrumbs: () => {}, + getMlCardState: () => 2, + fieldFormatEditors: indexPatternFieldEditor.fieldFormatEditors, }; }; diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts index bc4ef83de5012f..ed92172c8b91ca 100644 --- a/src/plugins/index_pattern_management/public/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -17,6 +17,7 @@ import { } from './service'; import { ManagementSetup } from '../../management/public'; +import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/public'; export interface IndexPatternManagementSetupDependencies { management: ManagementSetup; @@ -25,6 +26,7 @@ export interface IndexPatternManagementSetupDependencies { export interface IndexPatternManagementStartDependencies { data: DataPublicPluginStart; + indexPatternFieldEditor: IndexPatternFieldEditorStart; } export type IndexPatternManagementSetup = IndexPatternManagementServiceSetup; diff --git a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts index a686891c980149..15be7f11892e49 100644 --- a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts +++ b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts @@ -9,23 +9,7 @@ import { HttpSetup } from '../../../../core/public'; import { IndexPatternCreationManager, IndexPatternCreationConfig } from './creation'; import { IndexPatternListManager, IndexPatternListConfig } from './list'; -import { FieldFormatEditors } from './field_format_editors'; import { EnvironmentService } from './environment'; - -import { - BytesFormatEditor, - ColorFormatEditor, - DateFormatEditor, - DateNanosFormatEditor, - DurationFormatEditor, - NumberFormatEditor, - PercentFormatEditor, - StaticLookupFormatEditor, - StringFormatEditor, - TruncateFormatEditor, - UrlFormatEditor, -} from '../components/field_editor/components/field_format_editor'; - interface SetupDependencies { httpClient: HttpSetup; } @@ -38,13 +22,11 @@ interface SetupDependencies { export class IndexPatternManagementService { indexPatternCreationManager: IndexPatternCreationManager; indexPatternListConfig: IndexPatternListManager; - fieldFormatEditors: FieldFormatEditors; environmentService: EnvironmentService; constructor() { this.indexPatternCreationManager = new IndexPatternCreationManager(); this.indexPatternListConfig = new IndexPatternListManager(); - this.fieldFormatEditors = new FieldFormatEditors(); this.environmentService = new EnvironmentService(); } @@ -55,26 +37,9 @@ export class IndexPatternManagementService { const indexPatternListConfigSetup = this.indexPatternListConfig.setup(); indexPatternListConfigSetup.addListConfig(IndexPatternListConfig); - const defaultFieldFormatEditors = [ - BytesFormatEditor, - ColorFormatEditor, - DateFormatEditor, - DateNanosFormatEditor, - DurationFormatEditor, - NumberFormatEditor, - PercentFormatEditor, - StaticLookupFormatEditor, - StringFormatEditor, - TruncateFormatEditor, - UrlFormatEditor, - ]; - - const fieldFormatEditorsSetup = this.fieldFormatEditors.setup(defaultFieldFormatEditors); - return { creation: creationManagerSetup, list: indexPatternListConfigSetup, - fieldFormatEditors: fieldFormatEditorsSetup, environment: this.environmentService.setup(), }; } @@ -83,7 +48,6 @@ export class IndexPatternManagementService { return { creation: this.indexPatternCreationManager.start(), list: this.indexPatternListConfig.start(), - fieldFormatEditors: this.fieldFormatEditors.start(), }; } diff --git a/src/plugins/index_pattern_management/public/types.ts b/src/plugins/index_pattern_management/public/types.ts index 84e8ae007b99c8..62ee18ababc0bd 100644 --- a/src/plugins/index_pattern_management/public/types.ts +++ b/src/plugins/index_pattern_management/public/types.ts @@ -20,6 +20,7 @@ import { DataPublicPluginStart } from 'src/plugins/data/public'; import { ManagementAppMountParams } from '../../management/public'; import { IndexPatternManagementStart } from './index'; import { KibanaReactContextValue } from '../../kibana_react/public'; +import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/public'; export interface IndexPatternManagmentContext { chrome: ChromeStart; @@ -31,9 +32,11 @@ export interface IndexPatternManagmentContext { http: HttpSetup; docLinks: DocLinksStart; data: DataPublicPluginStart; + indexPatternFieldEditor: IndexPatternFieldEditorStart; indexPatternManagementStart: IndexPatternManagementStart; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; getMlCardState: () => MlCardState; + fieldFormatEditors: IndexPatternFieldEditorStart['fieldFormatEditors']; } export type IndexPatternManagmentContextValue = KibanaReactContextValue; diff --git a/src/plugins/index_pattern_management/tsconfig.json b/src/plugins/index_pattern_management/tsconfig.json index 4dca1634fddb69..37bd3e4aa5bbb9 100644 --- a/src/plugins/index_pattern_management/tsconfig.json +++ b/src/plugins/index_pattern_management/tsconfig.json @@ -18,5 +18,7 @@ { "path": "../url_forwarding/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../es_ui_shared/tsconfig.json" }, + { "path": "../index_pattern_field_editor/tsconfig.json" }, ] } diff --git a/test/functional/apps/management/_handle_version_conflict.js b/test/functional/apps/management/_handle_version_conflict.js index 61b143e9874714..16c427e9bbe20b 100644 --- a/test/functional/apps/management/_handle_version_conflict.js +++ b/test/functional/apps/management/_handle_version_conflict.js @@ -18,6 +18,7 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { + const testSubjects = getService('testSubjects'); const esArchiver = getService('esArchiver'); const browser = getService('browser'); const es = getService('legacyEs'); @@ -65,6 +66,11 @@ export default function ({ getService, getPageObjects }) { log.debug('Starting openControlsByName (' + fieldName + ')'); await PageObjects.settings.openControlsByName(fieldName); log.debug('controls are open'); + await ( + await (await testSubjects.find('formatRow')).findAllByCssSelector( + '[data-test-subj="toggle"]' + ) + )[0].click(); await PageObjects.settings.setFieldFormat('url'); const response = await es.update({ index: '.kibana', diff --git a/test/functional/apps/management/_index_pattern_filter.js b/test/functional/apps/management/_index_pattern_filter.js index eeb0b224d5f0ca..261ba29410a09d 100644 --- a/test/functional/apps/management/_index_pattern_filter.js +++ b/test/functional/apps/management/_index_pattern_filter.js @@ -35,23 +35,23 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); await PageObjects.settings.getFieldTypes(); - await PageObjects.settings.setFieldTypeFilter('string'); + await PageObjects.settings.setFieldTypeFilter('keyword'); await retry.try(async function () { const fieldTypes = await PageObjects.settings.getFieldTypes(); expect(fieldTypes.length).to.be.above(0); for (const fieldType of fieldTypes) { - expect(fieldType).to.be('string'); + expect(fieldType).to.be('keyword'); } }); - await PageObjects.settings.setFieldTypeFilter('number'); + await PageObjects.settings.setFieldTypeFilter('long'); await retry.try(async function () { const fieldTypes = await PageObjects.settings.getFieldTypes(); expect(fieldTypes.length).to.be.above(0); for (const fieldType of fieldTypes) { - expect(fieldType).to.be('number'); + expect(fieldType).to.be('long'); } }); }); diff --git a/test/functional/apps/management/_index_pattern_popularity.js b/test/functional/apps/management/_index_pattern_popularity.js index 231617d7084e96..0618dd79e272ec 100644 --- a/test/functional/apps/management/_index_pattern_popularity.js +++ b/test/functional/apps/management/_index_pattern_popularity.js @@ -10,6 +10,7 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); const log = getService('log'); const PageObjects = getPageObjects(['settings', 'common']); @@ -27,11 +28,12 @@ export default function ({ getService, getPageObjects }) { log.debug('Starting openControlsByName (' + fieldName + ')'); await PageObjects.settings.openControlsByName(fieldName); log.debug('increasePopularity'); + await testSubjects.click('toggleAdvancedSetting'); await PageObjects.settings.increasePopularity(); }); afterEach(async () => { - await PageObjects.settings.controlChangeCancel(); + await testSubjects.click('closeFlyoutButton'); await PageObjects.settings.removeIndexPattern(); // Cancel saving the popularity change (we didn't make a change in this case, just checking the value) }); @@ -44,12 +46,12 @@ export default function ({ getService, getPageObjects }) { it('should be reset on cancel', async function () { // Cancel saving the popularity change - await PageObjects.settings.controlChangeCancel(); + await testSubjects.click('closeFlyoutButton'); await PageObjects.settings.openControlsByName(fieldName); // check that it is 0 (previous increase was cancelled const popularity = await PageObjects.settings.getPopularity(); log.debug('popularity = ' + popularity); - expect(popularity).to.be(''); + expect(popularity).to.be('0'); }); it('can be saved', async function () { diff --git a/test/functional/apps/management/_index_pattern_results_sort.js b/test/functional/apps/management/_index_pattern_results_sort.js index 90af0636bcd486..cedf5ee355b36a 100644 --- a/test/functional/apps/management/_index_pattern_results_sort.js +++ b/test/functional/apps/management/_index_pattern_results_sort.js @@ -17,6 +17,12 @@ export default function ({ getService, getPageObjects }) { before(async function () { // delete .kibana index and then wait for Kibana to re-create it await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.createIndexPattern(); + }); + + after(async function () { + return await PageObjects.settings.removeIndexPattern(); }); const columns = [ @@ -31,8 +37,8 @@ export default function ({ getService, getPageObjects }) { }, { heading: 'Type', - first: '_source', - last: 'string', + first: '', + last: 'text', selector: async function () { const tableRow = await PageObjects.settings.getTableRow(0, 1); return await tableRow.getVisibleText(); @@ -42,16 +48,11 @@ export default function ({ getService, getPageObjects }) { columns.forEach(function (col) { describe('sort by heading - ' + col.heading, function indexPatternCreation() { - before(async function () { - await PageObjects.settings.createIndexPattern(); - }); - - after(async function () { - return await PageObjects.settings.removeIndexPattern(); - }); - it('should sort ascending', async function () { - await PageObjects.settings.sortBy(col.heading); + console.log('col.heading', col.heading); + if (col.heading !== 'Name') { + await PageObjects.settings.sortBy(col.heading); + } const rowText = await col.selector(); expect(rowText).to.be(col.first); }); @@ -65,15 +66,6 @@ export default function ({ getService, getPageObjects }) { }); describe('field list pagination', function () { const EXPECTED_FIELD_COUNT = 86; - - before(async function () { - await PageObjects.settings.createIndexPattern(); - }); - - after(async function () { - return await PageObjects.settings.removeIndexPattern(); - }); - it('makelogs data should have expected number of fields', async function () { await retry.try(async function () { const TabCount = await PageObjects.settings.getFieldsTabCount(); diff --git a/test/functional/apps/visualize/_tag_cloud.ts b/test/functional/apps/visualize/_tag_cloud.ts index e619a35fb3d0b7..c7d864e5cfb233 100644 --- a/test/functional/apps/visualize/_tag_cloud.ts +++ b/test/functional/apps/visualize/_tag_cloud.ts @@ -11,6 +11,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); const log = getService('log'); const inspector = getService('inspector'); @@ -145,6 +146,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.settings.clickIndexPatternLogstash(); await PageObjects.settings.filterField(termsField); await PageObjects.settings.openControlsByName(termsField); + await ( + await (await testSubjects.find('formatRow')).findAllByCssSelector( + '[data-test-subj="toggle"]' + ) + )[0].click(); await PageObjects.settings.setFieldFormat('bytes'); await PageObjects.settings.controlChangeSave(); await PageObjects.common.navigateToApp('visualize'); diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index f59db345f39ff9..09a05732b791bb 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -249,7 +249,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider await find.clickByCssSelector( `table.euiTable tbody tr.euiTableRow:nth-child(${tableFields.indexOf(name) + 1}) - td:last-child button` + td:nth-last-child(2) button` ); } diff --git a/tsconfig.json b/tsconfig.json index 48feac3efe4752..f6ce6b92b7e02f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -63,5 +63,6 @@ { "path": "./src/plugins/visualizations/tsconfig.json" }, { "path": "./src/plugins/visualize/tsconfig.json" }, { "path": "./src/plugins/index_pattern_management/tsconfig.json" }, + { "path": "./src/plugins/index_pattern_field_editor/tsconfig.json" }, ] } diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx index f6815a4264a5d4..023b620522282b 100644 --- a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx @@ -8,7 +8,6 @@ import React, { useEffect, useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCode } from '@elastic/eui'; import { PainlessLang, PainlessContext } from '@kbn/monaco'; import { EuiFlexGroup, @@ -19,6 +18,7 @@ import { EuiComboBoxOptionOption, EuiLink, EuiCallOut, + EuiCode, } from '@elastic/eui'; import { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c561339d1a6670..1162a9bf00c70e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2591,15 +2591,15 @@ "indexPatternManagement.actions.deleteButton": "削除", "indexPatternManagement.actions.saveButton": "フィールドを保存", "indexPatternManagement.aliasLabel": "エイリアス", - "indexPatternManagement.color.actions": "アクション", - "indexPatternManagement.color.addColorButton": "色を追加", - "indexPatternManagement.color.backgroundLabel": "背景色", - "indexPatternManagement.color.deleteAria": "削除", - "indexPatternManagement.color.deleteTitle": "色のフォーマットを削除", - "indexPatternManagement.color.exampleLabel": "例", - "indexPatternManagement.color.patternLabel": "パターン(正規表現)", - "indexPatternManagement.color.rangeLabel": "範囲(min:max)", - "indexPatternManagement.color.textColorLabel": "文字の色", + "indexPatternFieldEditor.color.actions": "アクション", + "indexPatternFieldEditor.color.addColorButton": "色を追加", + "indexPatternFieldEditor.color.backgroundLabel": "背景色", + "indexPatternFieldEditor.color.deleteAria": "削除", + "indexPatternFieldEditor.color.deleteTitle": "色のフォーマットを削除", + "indexPatternFieldEditor.color.exampleLabel": "例", + "indexPatternFieldEditor.color.patternLabel": "パターン(正規表現)", + "indexPatternFieldEditor.color.rangeLabel": "範囲(min:max)", + "indexPatternFieldEditor.color.textColorLabel": "文字の色", "indexPatternManagement.createHeader": "スクリプトフィールドを作成", "indexPatternManagement.createIndexPattern.betaLabel": "ベータ", "indexPatternManagement.createIndexPattern.description": "インデックスパターンは、{single}または{multiple}データソース、{star}と一致します。", @@ -2663,10 +2663,10 @@ "indexPatternManagement.createIndexPatternHeader": "{indexPatternName}の作成", "indexPatternManagement.customLabel": "カスタムラベル", "indexPatternManagement.dataStreamLabel": "データストリーム", - "indexPatternManagement.date.documentationLabel": "ドキュメント", - "indexPatternManagement.date.momentLabel": "Moment.jsのフォーマットパターン(デフォルト: {defaultPattern})", - "indexPatternManagement.defaultErrorMessage": "このフォーマット構成の使用を試みた際にエラーが発生しました: {message}", - "indexPatternManagement.defaultFormatDropDown": "- デフォルト -", + "indexPatternFieldEditor.date.documentationLabel": "ドキュメント", + "indexPatternFieldEditor.date.momentLabel": "Moment.jsのフォーマットパターン(デフォルト: {defaultPattern})", + "indexPatternFieldEditor.defaultErrorMessage": "このフォーマット構成の使用を試みた際にエラーが発生しました: {message}", + "indexPatternFieldEditor.defaultFormatDropDown": "- デフォルト -", "indexPatternManagement.defaultFormatHeader": "フォーマット (デフォルト: {defaultFormat})", "indexPatternManagement.deleteField.cancelButton": "キャンセル", "indexPatternManagement.deleteField.deleteButton": "削除", @@ -2676,11 +2676,11 @@ "indexPatternManagement.deleteFieldLabel": "削除されたフィールドは復元できません。{separator}続行してよろしいですか?", "indexPatternManagement.disabledCallOutHeader": "スクリプティングが無効です", "indexPatternManagement.disabledCallOutLabel": "Elasticsearchでのすべてのインラインスクリプティングが無効になっています。Kibanaでスクリプトフィールドを使用するには、インラインスクリプティングを有効にする必要があります。", - "indexPatternManagement.duration.decimalPlacesLabel": "小数部分の桁数", - "indexPatternManagement.duration.inputFormatLabel": "インプット形式", - "indexPatternManagement.duration.outputFormatLabel": "アウトプット形式", - "indexPatternManagement.duration.showSuffixLabel": "接尾辞を表示", - "indexPatternManagement.durationErrorMessage": "小数部分の桁数は0から20までの間で指定する必要があります", + "indexPatternFieldEditor.duration.decimalPlacesLabel": "小数部分の桁数", + "indexPatternFieldEditor.duration.inputFormatLabel": "インプット形式", + "indexPatternFieldEditor.duration.outputFormatLabel": "アウトプット形式", + "indexPatternFieldEditor.duration.showSuffixLabel": "接尾辞を表示", + "indexPatternFieldEditor.durationErrorMessage": "小数部分の桁数は0から20までの間で指定する必要があります", "indexPatternManagement.editHeader": "{fieldName}を編集", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonDescription": "すべてのデータに完全アグリゲーションを実行", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonText": "標準インデックスパターン", @@ -2765,7 +2765,6 @@ "indexPatternManagement.editIndexPattern.tabs.sourceHeader": "フィールドフィルター", "indexPatternManagement.editIndexPattern.timeFilterHeader": "時刻フィールド:「{timeFieldName}」", "indexPatternManagement.editIndexPattern.timeFilterLabel.mappingAPILink": "マッピングAPI", - "indexPatternManagement.editIndexPattern.timeFilterLabel.timeFilterDetail": "このページは{indexPatternTitle}インデックス内のすべてのフィールドと、Elasticsearchに記録された各フィールドのコアタイプを一覧表示します。フィールドタイプを変更するにはElasticsearchを使用します", "indexPatternManagement.editIndexPatternLiveRegionAriaLabel": "インデックスパターン", "indexPatternManagement.emptyIndexPatternPrompt.documentation": "ドキュメンテーションを表示", "indexPatternManagement.emptyIndexPatternPrompt.indexPatternExplanation": "Kibanaでは、検索するインデックスを特定するためにインデックスパターンが必要です。インデックスパターンは、昨日のログデータなど特定のインデックス、またはログデータを含むすべてのインデックスを参照できます。", @@ -2774,7 +2773,6 @@ "indexPatternManagement.emptyIndexPatternPrompt.youHaveData": "Elasticsearchにデータがあります。", "indexPatternManagement.fieldTypeConflict": "フィールドタイプの矛盾", "indexPatternManagement.formatHeader": "フォーマット", - "indexPatternManagement.formatLabel": "フォーマットは、特定の値の表示形式を管理できます。また、値を完全に変更したり、ディスカバリでのハイライト機能を無効にしたりすることも可能です。", "indexPatternManagement.frozenLabel": "凍結", "indexPatternManagement.indexLabel": "インデックス", "indexPatternManagement.indexNameLabel": "インデックス名", @@ -2791,19 +2789,6 @@ "indexPatternManagement.indexPatternTable.indexPatternExplanation": "Elasticsearchからのデータの取得に役立つインデックスパターンを作成して管理します。", "indexPatternManagement.indexPatternTable.title": "インデックスパターン", "indexPatternManagement.labelHelpText": "このフィールドが Discover、Maps、Visualize に表示されるときに使用するカスタムラベルを設定します。現在、クエリとフィルターはカスタムラベルをサポートせず、元のフィールド名が使用されます。", - "indexPatternManagement.labelTemplate.example.idLabel": "ユーザー#{value}", - "indexPatternManagement.labelTemplate.example.output.idLabel": "ユーザー", - "indexPatternManagement.labelTemplate.example.output.pathLabel": "アセットを表示", - "indexPatternManagement.labelTemplate.example.pathLabel": "アセットを表示", - "indexPatternManagement.labelTemplate.examplesHeader": "例", - "indexPatternManagement.labelTemplate.inputHeader": "インプット", - "indexPatternManagement.labelTemplate.labelHeader": "ラベルテンプレート", - "indexPatternManagement.labelTemplate.outputHeader": "アウトプット", - "indexPatternManagement.labelTemplate.urlHeader": "URLテンプレート", - "indexPatternManagement.labelTemplate.urlLabel": "フォーマット済みURL", - "indexPatternManagement.labelTemplate.valueLabel": "フィールド値", - "indexPatternManagement.labelTemplateHeader": "ラベルテンプレート", - "indexPatternManagement.labelTemplateLabel": "このフィールドのURLが長い場合、URLのテキストバージョン用の代替テンプレートを使用すると良いかもしれません。URLの代わりに表示されますが、URLにリンクされます。このフォーマットは、値の投入に二重中括弧の表記{doubleCurlyBraces}を使用する文字列です。次の値にアクセスできます。", "indexPatternManagement.languageLabel": "言語", "indexPatternManagement.mappingConflictLabel.mappingConflictDetail": "{mappingConflict} {fieldName}というフィールドはすでに存在します。スクリプトフィールドに同じ名前を付けると、同時に両方のフィールドにクエリが実行できなくなります。", "indexPatternManagement.mappingConflictLabel.mappingConflictLabel": "マッピングの矛盾:", @@ -2811,27 +2796,27 @@ "indexPatternManagement.nameErrorMessage": "名前が必要です", "indexPatternManagement.nameLabel": "名前", "indexPatternManagement.namePlaceholder": "新規スクリプトフィールド", - "indexPatternManagement.number.documentationLabel": "ドキュメント", - "indexPatternManagement.number.numeralLabel": "Numeral.js のフォーマットパターン (デフォルト: {defaultPattern})", + "indexPatternFieldEditor.number.documentationLabel": "ドキュメント", + "indexPatternFieldEditor.number.numeralLabel": "Numeral.js のフォーマットパターン (デフォルト: {defaultPattern})", "indexPatternManagement.popularityLabel": "利用頻度", - "indexPatternManagement.samples.inputHeader": "インプット", - "indexPatternManagement.samples.outputHeader": "アウトプット", - "indexPatternManagement.samplesHeader": "サンプル", + "indexPatternFieldEditor.samples.inputHeader": "インプット", + "indexPatternFieldEditor.samples.outputHeader": "アウトプット", + "indexPatternFieldEditor.samplesHeader": "サンプル", "indexPatternManagement.script.accessWithLabel": "{code} でフィールドにアクセスします。", "indexPatternManagement.script.getHelpLabel": "構文のヒントを得たり、スクリプトの結果をプレビューしたりできます。", "indexPatternManagement.scriptingLanguages.errorFetchingToastDescription": "Elasticsearchから利用可能なスクリプト言語の取得中にエラーが発生しました", "indexPatternManagement.scriptInvalidErrorMessage": "スクリプトが無効です。詳細については、スクリプトプレビューを表示してください", "indexPatternManagement.scriptLabel": "スクリプト", "indexPatternManagement.scriptRequiredErrorMessage": "スクリプトが必要です", - "indexPatternManagement.staticLookup.actions": "アクション", - "indexPatternManagement.staticLookup.addEntryButton": "エントリーを追加", - "indexPatternManagement.staticLookup.deleteAria": "削除", - "indexPatternManagement.staticLookup.deleteTitle": "エントリーの削除", - "indexPatternManagement.staticLookup.keyLabel": "キー", - "indexPatternManagement.staticLookup.leaveBlankPlaceholder": "値をそのままにするには空欄にします", - "indexPatternManagement.staticLookup.unknownKeyLabel": "不明なキーの値", - "indexPatternManagement.staticLookup.valueLabel": "値", - "indexPatternManagement.string.transformLabel": "変換", + "indexPatternFieldEditor.staticLookup.actions": "アクション", + "indexPatternFieldEditor.staticLookup.addEntryButton": "エントリーを追加", + "indexPatternFieldEditor.staticLookup.deleteAria": "削除", + "indexPatternFieldEditor.staticLookup.deleteTitle": "エントリーの削除", + "indexPatternFieldEditor.staticLookup.keyLabel": "キー", + "indexPatternFieldEditor.staticLookup.leaveBlankPlaceholder": "値をそのままにするには空欄にします", + "indexPatternFieldEditor.staticLookup.unknownKeyLabel": "不明なキーの値", + "indexPatternFieldEditor.staticLookup.valueLabel": "値", + "indexPatternFieldEditor.string.transformLabel": "変換", "indexPatternManagement.syntax.default.formatLabel": "doc['some_field'].value", "indexPatternManagement.syntax.defaultLabel.defaultDetail": "デフォルトで、KibanaのスクリプトフィールドはElasticsearchでの使用を目的に特別に開発されたシンプルでセキュアなスクリプト言語の{painless}を使用します。ドキュメントの値にアクセスするには次のフォーマットを使用します。", "indexPatternManagement.syntax.defaultLabel.painlessLink": "Painless", @@ -2862,27 +2847,18 @@ "indexPatternManagement.testScript.resultsLabel": "最初の10件", "indexPatternManagement.testScript.resultsTitle": "結果を表示", "indexPatternManagement.testScript.submitButtonLabel": "スクリプトを実行", - "indexPatternManagement.truncate.lengthLabel": "フィールドの長さ", + "indexPatternFieldEditor.truncate.lengthLabel": "フィールドの長さ", "indexPatternManagement.typeLabel": "型", - "indexPatternManagement.url.heightLabel": "高さ", - "indexPatternManagement.url.labelTemplateHelpText": "ラベルテンプレートのヘルプ", - "indexPatternManagement.url.labelTemplateLabel": "ラベルテンプレート", - "indexPatternManagement.url.offLabel": "オフ", - "indexPatternManagement.url.onLabel": "オン", - "indexPatternManagement.url.openTabLabel": "新規タブで開く", - "indexPatternManagement.url.template.helpLinkText": "URLテンプレートのヘルプ", - "indexPatternManagement.url.typeLabel": "型", - "indexPatternManagement.url.urlTemplateLabel": "URLテンプレート", - "indexPatternManagement.url.widthLabel": "幅", - "indexPatternManagement.urlTemplate.examplesHeader": "例", - "indexPatternManagement.urlTemplate.inputHeader": "インプット", - "indexPatternManagement.urlTemplate.outputHeader": "アウトプット", - "indexPatternManagement.urlTemplate.rawValueLabel": "非エスケープ値", - "indexPatternManagement.urlTemplate.templateHeader": "テンプレート", - "indexPatternManagement.urlTemplate.valueLabel": "URLエスケープ値", - "indexPatternManagement.urlTemplateHeader": "URLテンプレート", - "indexPatternManagement.urlTemplateLabel.fieldDetail": "フィールドにURLの一部のみが含まれている場合、{strongUrlTemplate}でその値を完全なURLとしてフォーマットできます。このフォーマットは、値の投入に二重中括弧の表記{doubleCurlyBraces}を使用する文字列です。次の値にアクセスできます。", - "indexPatternManagement.urlTemplateLabel.strongUrlTemplateLabel": "URLテンプレート", + "indexPatternFieldEditor.url.heightLabel": "高さ", + "indexPatternFieldEditor.url.labelTemplateHelpText": "ラベルテンプレートのヘルプ", + "indexPatternFieldEditor.url.labelTemplateLabel": "ラベルテンプレート", + "indexPatternFieldEditor.url.offLabel": "オフ", + "indexPatternFieldEditor.url.onLabel": "オン", + "indexPatternFieldEditor.url.openTabLabel": "新規タブで開く", + "indexPatternFieldEditor.url.template.helpLinkText": "URLテンプレートのヘルプ", + "indexPatternFieldEditor.url.typeLabel": "型", + "indexPatternFieldEditor.url.urlTemplateLabel": "URLテンプレート", + "indexPatternFieldEditor.url.widthLabel": "幅", "indexPatternManagement.warningCallOut.descriptionLabel": "計算値の表示と集約にスクリプトフィールドが使用できます。そのため非常に遅い場合があり、適切に行わないとKibanaが使用できなくなる可能性もあります。この場合安全策はありません。入力ミスがあると、あちこちに予期せぬ例外が起こります!", "indexPatternManagement.warningCallOutHeader": "十分ご注意ください", "indexPatternManagement.warningCallOutLabel.callOutDetail": "スクリプトフィールドを使う前に、{scripFields}と{scriptsInAggregation}についてよく理解するようにしてください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ab09f6c1ec56e8..fc658ae8ce719e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2595,15 +2595,15 @@ "indexPatternManagement.actions.deleteButton": "删除", "indexPatternManagement.actions.saveButton": "保存字段", "indexPatternManagement.aliasLabel": "别名", - "indexPatternManagement.color.actions": "操作", - "indexPatternManagement.color.addColorButton": "添加颜色", - "indexPatternManagement.color.backgroundLabel": "背景色", - "indexPatternManagement.color.deleteAria": "删除", - "indexPatternManagement.color.deleteTitle": "删除颜色格式", - "indexPatternManagement.color.exampleLabel": "示例", - "indexPatternManagement.color.patternLabel": "模式(正则表达式)", - "indexPatternManagement.color.rangeLabel": "范围(最小值:最大值)", - "indexPatternManagement.color.textColorLabel": "文本颜色", + "indexPatternFieldEditor.color.actions": "操作", + "indexPatternFieldEditor.color.addColorButton": "添加颜色", + "indexPatternFieldEditor.color.backgroundLabel": "背景色", + "indexPatternFieldEditor.color.deleteAria": "删除", + "indexPatternFieldEditor.color.deleteTitle": "删除颜色格式", + "indexPatternFieldEditor.color.exampleLabel": "示例", + "indexPatternFieldEditor.color.patternLabel": "模式(正则表达式)", + "indexPatternFieldEditor.color.rangeLabel": "范围(最小值:最大值)", + "indexPatternFieldEditor.color.textColorLabel": "文本颜色", "indexPatternManagement.createHeader": "创建脚本字段", "indexPatternManagement.createIndexPattern.betaLabel": "公测版", "indexPatternManagement.createIndexPattern.description": "索引模式可以匹配单个源,例如 {single} 或 {multiple} 个数据源、{star}。", @@ -2667,10 +2667,10 @@ "indexPatternManagement.createIndexPatternHeader": "创建 {indexPatternName}", "indexPatternManagement.customLabel": "定制标签", "indexPatternManagement.dataStreamLabel": "数据流", - "indexPatternManagement.date.documentationLabel": "文档", - "indexPatternManagement.date.momentLabel": "Moment.js 格式模式(默认值:{defaultPattern})", - "indexPatternManagement.defaultErrorMessage": "尝试使用此格式配置时发生错误:{message}", - "indexPatternManagement.defaultFormatDropDown": "- 默认值 -", + "indexPatternFieldEditor.date.documentationLabel": "文档", + "indexPatternFieldEditor.date.momentLabel": "Moment.js 格式模式(默认值:{defaultPattern})", + "indexPatternFieldEditor.defaultErrorMessage": "尝试使用此格式配置时发生错误:{message}", + "indexPatternFieldEditor.defaultFormatDropDown": "- 默认值 -", "indexPatternManagement.defaultFormatHeader": "格式(默认值:{defaultFormat})", "indexPatternManagement.deleteField.cancelButton": "取消", "indexPatternManagement.deleteField.deleteButton": "删除", @@ -2680,11 +2680,11 @@ "indexPatternManagement.deleteFieldLabel": "您无法恢复已删除字段。{separator}确定要执行此操作?", "indexPatternManagement.disabledCallOutHeader": "脚本已禁用", "indexPatternManagement.disabledCallOutLabel": "所有内联脚本在 Elasticsearch 中已禁用。必须至少为一种语言启用内联脚本,才能在 Kibana 中使用脚本字段。", - "indexPatternManagement.duration.decimalPlacesLabel": "小数位数", - "indexPatternManagement.duration.inputFormatLabel": "输入格式", - "indexPatternManagement.duration.outputFormatLabel": "输出格式", - "indexPatternManagement.duration.showSuffixLabel": "显示后缀", - "indexPatternManagement.durationErrorMessage": "小数位数必须介于 0 和 20 之间", + "indexPatternFieldEditor.duration.decimalPlacesLabel": "小数位数", + "indexPatternFieldEditor.duration.inputFormatLabel": "输入格式", + "indexPatternFieldEditor.duration.outputFormatLabel": "输出格式", + "indexPatternFieldEditor.duration.showSuffixLabel": "显示后缀", + "indexPatternFieldEditor.durationErrorMessage": "小数位数必须介于 0 和 20 之间", "indexPatternManagement.editHeader": "编辑 {fieldName}", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonDescription": "对任何数据执行完全聚合", "indexPatternManagement.editIndexPattern.createIndex.defaultButtonText": "标准索引模式", @@ -2769,7 +2769,6 @@ "indexPatternManagement.editIndexPattern.tabs.sourceHeader": "字段筛选", "indexPatternManagement.editIndexPattern.timeFilterHeader": "时间字段:“{timeFieldName}”", "indexPatternManagement.editIndexPattern.timeFilterLabel.mappingAPILink": "映射 API", - "indexPatternManagement.editIndexPattern.timeFilterLabel.timeFilterDetail": "此页根据 Elasticsearch 的记录列出“{indexPatternTitle}”索引中的每个字段以及字段的关联核心类型。要更改字段类型,请使用 Elasticsearch", "indexPatternManagement.editIndexPatternLiveRegionAriaLabel": "索引模式", "indexPatternManagement.emptyIndexPatternPrompt.documentation": "阅读文档", "indexPatternManagement.emptyIndexPatternPrompt.indexPatternExplanation": "Kibana 需要索引模式,以识别您要浏览的索引。索引模式可以指向特定索引(例如昨天的日志数据),或包含日志数据的所有索引。", @@ -2778,7 +2777,6 @@ "indexPatternManagement.emptyIndexPatternPrompt.youHaveData": "您在 Elasticsearch 中有数据。", "indexPatternManagement.fieldTypeConflict": "字段类型冲突", "indexPatternManagement.formatHeader": "格式", - "indexPatternManagement.formatLabel": "设置格式允许您控制特定值的显示方式。其还会导致值完全更改,并阻止 Discover 中的突出显示起作用。", "indexPatternManagement.frozenLabel": "已冻结", "indexPatternManagement.indexLabel": "索引", "indexPatternManagement.indexNameLabel": "索引名称", @@ -2795,19 +2793,6 @@ "indexPatternManagement.indexPatternTable.indexPatternExplanation": "创建和管理帮助您从 Elasticsearch 中检索数据的索引模式。", "indexPatternManagement.indexPatternTable.title": "索引模式", "indexPatternManagement.labelHelpText": "设置此字段在 Discover、Maps 和 Visualize 中显示时要使用的定制标签。当前查询和筛选不支持定制标签,将使用原始字段名称。", - "indexPatternManagement.labelTemplate.example.idLabel": "用户 #{value}", - "indexPatternManagement.labelTemplate.example.output.idLabel": "用户", - "indexPatternManagement.labelTemplate.example.output.pathLabel": "查看资产", - "indexPatternManagement.labelTemplate.example.pathLabel": "查看资产", - "indexPatternManagement.labelTemplate.examplesHeader": "示例", - "indexPatternManagement.labelTemplate.inputHeader": "输入", - "indexPatternManagement.labelTemplate.labelHeader": "标签模板", - "indexPatternManagement.labelTemplate.outputHeader": "输出", - "indexPatternManagement.labelTemplate.urlHeader": "URL 模板", - "indexPatternManagement.labelTemplate.urlLabel": "格式化 URL", - "indexPatternManagement.labelTemplate.valueLabel": "字段值", - "indexPatternManagement.labelTemplateHeader": "标签模板", - "indexPatternManagement.labelTemplateLabel": "如果此字段中的 URL 很长,为 URL 的文本版本提供备选模板可能会很有用。该文本将会显示,而非显示该 url,但仍会链接到该 URL。该格式是使用双大括号表示法 {doubleCurlyBraces} 来注入值的字符串。可以访问以下值:", "indexPatternManagement.languageLabel": "语言", "indexPatternManagement.mappingConflictLabel.mappingConflictDetail": "{mappingConflict}您已经有名称为 {fieldName} 的字段。使用相同的名称命名您的脚本字段意味着您将无法同时查找两个字段。", "indexPatternManagement.mappingConflictLabel.mappingConflictLabel": "映射冲突:", @@ -2815,27 +2800,27 @@ "indexPatternManagement.nameErrorMessage": "“名称”必填", "indexPatternManagement.nameLabel": "名称", "indexPatternManagement.namePlaceholder": "新建脚本字段", - "indexPatternManagement.number.documentationLabel": "文档", - "indexPatternManagement.number.numeralLabel": "Numeral.js 格式模式(默认值:{defaultPattern})", + "indexPatternFieldEditor.number.documentationLabel": "文档", + "indexPatternFieldEditor.number.numeralLabel": "Numeral.js 格式模式(默认值:{defaultPattern})", "indexPatternManagement.popularityLabel": "常见度", - "indexPatternManagement.samples.inputHeader": "输入", - "indexPatternManagement.samples.outputHeader": "输出", - "indexPatternManagement.samplesHeader": "样例", + "indexPatternFieldEditor.samples.inputHeader": "输入", + "indexPatternFieldEditor.samples.outputHeader": "输出", + "indexPatternFieldEditor.samplesHeader": "样例", "indexPatternManagement.script.accessWithLabel": "使用 {code} 访问字段。", "indexPatternManagement.script.getHelpLabel": "获取该语法的帮助,预览脚本的结果。", "indexPatternManagement.scriptingLanguages.errorFetchingToastDescription": "从 Elasticsearch 获取可用的脚本语言时出错", "indexPatternManagement.scriptInvalidErrorMessage": "脚本无效。查看脚本预览以了解详情", "indexPatternManagement.scriptLabel": "脚本", "indexPatternManagement.scriptRequiredErrorMessage": "“脚本”必填", - "indexPatternManagement.staticLookup.actions": "操作", - "indexPatternManagement.staticLookup.addEntryButton": "添加条目", - "indexPatternManagement.staticLookup.deleteAria": "删除", - "indexPatternManagement.staticLookup.deleteTitle": "删除条目", - "indexPatternManagement.staticLookup.keyLabel": "键", - "indexPatternManagement.staticLookup.leaveBlankPlaceholder": "留空可使值保持原样", - "indexPatternManagement.staticLookup.unknownKeyLabel": "未知键的值", - "indexPatternManagement.staticLookup.valueLabel": "值", - "indexPatternManagement.string.transformLabel": "转换", + "indexPatternFieldEditor.staticLookup.actions": "操作", + "indexPatternFieldEditor.staticLookup.addEntryButton": "添加条目", + "indexPatternFieldEditor.staticLookup.deleteAria": "删除", + "indexPatternFieldEditor.staticLookup.deleteTitle": "删除条目", + "indexPatternFieldEditor.staticLookup.keyLabel": "键", + "indexPatternFieldEditor.staticLookup.leaveBlankPlaceholder": "留空可使值保持原样", + "indexPatternFieldEditor.staticLookup.unknownKeyLabel": "未知键的值", + "indexPatternFieldEditor.staticLookup.valueLabel": "值", + "indexPatternFieldEditor.string.transformLabel": "转换", "indexPatternManagement.syntax.default.formatLabel": "doc['some_field'].value", "indexPatternManagement.syntax.defaultLabel.defaultDetail": "默认情况下,Kibana 脚本字段使用 {painless}(一种简单且安全的脚本语言,专用于 Elasticsearch)通过以下格式访问文档中的值:", "indexPatternManagement.syntax.defaultLabel.painlessLink": "Painless", @@ -2866,27 +2851,18 @@ "indexPatternManagement.testScript.resultsLabel": "前 10 个结果", "indexPatternManagement.testScript.resultsTitle": "预览结果", "indexPatternManagement.testScript.submitButtonLabel": "运行脚本", - "indexPatternManagement.truncate.lengthLabel": "字段长度", + "indexPatternFieldEditor.truncate.lengthLabel": "字段长度", "indexPatternManagement.typeLabel": "类型", - "indexPatternManagement.url.heightLabel": "高", - "indexPatternManagement.url.labelTemplateHelpText": "标签模板帮助", - "indexPatternManagement.url.labelTemplateLabel": "标签模板", - "indexPatternManagement.url.offLabel": "关闭", - "indexPatternManagement.url.onLabel": "开启", - "indexPatternManagement.url.openTabLabel": "在新选项卡中打开", - "indexPatternManagement.url.template.helpLinkText": "URL 模板帮助", - "indexPatternManagement.url.typeLabel": "类型", - "indexPatternManagement.url.urlTemplateLabel": "URL 模板", - "indexPatternManagement.url.widthLabel": "宽", - "indexPatternManagement.urlTemplate.examplesHeader": "示例", - "indexPatternManagement.urlTemplate.inputHeader": "输入", - "indexPatternManagement.urlTemplate.outputHeader": "输出", - "indexPatternManagement.urlTemplate.rawValueLabel": "非转义值", - "indexPatternManagement.urlTemplate.templateHeader": "模板", - "indexPatternManagement.urlTemplate.valueLabel": "URI 转义值", - "indexPatternManagement.urlTemplateHeader": "Url 模板", - "indexPatternManagement.urlTemplateLabel.fieldDetail": "如果字段仅包含 URL 的一部分,则 {strongUrlTemplate} 可用于将该值格式化为完整的 URL。该格式是使用双大括号表示法 {doubleCurlyBraces} 来注入值的字符串。可以访问以下值:", - "indexPatternManagement.urlTemplateLabel.strongUrlTemplateLabel": "Url 模板", + "indexPatternFieldEditor.url.heightLabel": "高", + "indexPatternFieldEditor.url.labelTemplateHelpText": "标签模板帮助", + "indexPatternFieldEditor.url.labelTemplateLabel": "标签模板", + "indexPatternFieldEditor.url.offLabel": "关闭", + "indexPatternFieldEditor.url.onLabel": "开启", + "indexPatternFieldEditor.url.openTabLabel": "在新选项卡中打开", + "indexPatternFieldEditor.url.template.helpLinkText": "URL 模板帮助", + "indexPatternFieldEditor.url.typeLabel": "类型", + "indexPatternFieldEditor.url.urlTemplateLabel": "URL 模板", + "indexPatternFieldEditor.url.widthLabel": "宽", "indexPatternManagement.warningCallOut.descriptionLabel": "脚本字段可用于显示并聚合计算值。因此,它们会很慢,如果操作不当,会导致 Kibana 不可用。此处没有安全网。如果拼写错误,则在任何地方都会引发异常!", "indexPatternManagement.warningCallOutHeader": "谨慎操作", "indexPatternManagement.warningCallOutLabel.callOutDetail": "请先熟悉{scripFields}以及{scriptsInAggregation},然后再使用脚本字段。", diff --git a/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js b/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js index 0cad97e268dda0..4fd7c2cc2f0679 100644 --- a/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js +++ b/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js @@ -99,7 +99,7 @@ export default function ({ getService, getPageObjects }) { // ensure all fields are available await PageObjects.settings.clickIndexPatternByName(rollupIndexPatternName); const fields = await PageObjects.settings.getFieldNames(); - expect(fields).to.eql(['_source', '_id', '_type', '_index', '_score', '@timestamp']); + expect(fields).to.eql(['@timestamp', '_id', '_index', '_score', '_source', '_type']); }); after(async () => { From 96bc9e868de56e09c0a03e9a6d40cb2e4905b0ed Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 18 Feb 2021 13:42:40 -0500 Subject: [PATCH 09/43] [CI] Ping assignees on Github PR comments (#91871) --- vars/githubPr.groovy | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/vars/githubPr.groovy b/vars/githubPr.groovy index eead00c082ba75..d024eb7346f8f7 100644 --- a/vars/githubPr.groovy +++ b/vars/githubPr.groovy @@ -235,6 +235,13 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { messages << "To update your PR or re-run it, just comment with:\n`@elasticmachine merge upstream`" + catchErrors { + def assignees = getAssignees() + if (assignees) { + messages << "cc " + assignees.collect { "@${it}"}.join(" ") + } + } + info.builds << [ status: status, url: env.BUILD_URL, @@ -329,3 +336,19 @@ def shouldCheckCiMetricSuccess() { return true } + +def getPR() { + withGithubCredentials { + def path = "repos/elastic/kibana/pulls/${env.ghprbPullId}" + return githubApi.get(path) + } +} + +def getAssignees() { + def pr = getPR() + if (!pr) { + return [] + } + + return pr.assignees.collect { it.login } +} From 0a685dbb63ea77b82e9eb8a7e55c7a92f3ebf748 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Thu, 18 Feb 2021 13:49:26 -0500 Subject: [PATCH 10/43] [Time to Visualize] Dashboard Save As New by Default (#91761) * changed dashboard save as to have save as new switch on by default --- .../top_nav/__snapshots__/save_modal.test.js.snap | 1 + .../dashboard/public/application/top_nav/save_modal.tsx | 1 + test/functional/page_objects/dashboard_page.ts | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/plugins/dashboard/public/application/top_nav/__snapshots__/save_modal.test.js.snap b/src/plugins/dashboard/public/application/top_nav/__snapshots__/save_modal.test.js.snap index bc4ed477d9eea0..f8ba1b38685274 100644 --- a/src/plugins/dashboard/public/application/top_nav/__snapshots__/save_modal.test.js.snap +++ b/src/plugins/dashboard/public/application/top_nav/__snapshots__/save_modal.test.js.snap @@ -2,6 +2,7 @@ exports[`renders DashboardSaveModal 1`] = ` { onClose={this.props.onClose} title={this.props.title} showCopyOnSave={this.props.showCopyOnSave} + initialCopyOnSave={this.props.showCopyOnSave} objectType="dashboard" options={this.renderDashboardSaveOptions()} showDescription={false} diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 9c571f0f0ef86b..465deed4d9039b 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -425,8 +425,9 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide await this.setStoreTimeWithDashboard(saveOptions.storeTimeWithDashboard); } - if (saveOptions.saveAsNew !== undefined) { - await this.setSaveAsNewCheckBox(saveOptions.saveAsNew); + const saveAsNewCheckboxExists = await testSubjects.exists('saveAsNewCheckbox'); + if (saveAsNewCheckboxExists) { + await this.setSaveAsNewCheckBox(Boolean(saveOptions.saveAsNew)); } if (saveOptions.tags) { From 4ce0b6c14fbbbf6e51ff5c9f997edc80f9f0b15b Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Thu, 18 Feb 2021 13:03:50 -0600 Subject: [PATCH 11/43] [DOCS] Adds and updates Visualization advanced settings (#91904) --- docs/management/advanced-options.asciidoc | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index dc0405b22942f6..c7d5242da69de4 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -457,7 +457,7 @@ of buckets to try to represent. [horizontal] [[visualization-visualize-chartslibrary]]`visualization:visualize:legacyChartsLibrary`:: -Enables legacy charts library for area, line and bar charts in visualize. +Enables the legacy charts library for aggregation-based area, line, and bar charts in *Visualize*. [[visualization-colormapping]]`visualization:colorMapping`:: **This setting is deprecated and will not be supported as of 8.0.** @@ -465,24 +465,25 @@ Maps values to specific colors in *Visualize* charts and *TSVB*. This setting do [[visualization-dimmingopacity]]`visualization:dimmingOpacity`:: The opacity of the chart items that are dimmed when highlighting another element -of the chart. The lower this number, the more the highlighted element stands out. -This must be a number between 0 and 1. +of the chart. Use numbers between 0 and 1. The lower the number, the more the highlighted element stands out. + +[[visualization-heatmap-maxbuckets]]`visualization:heatmap:maxBuckets`:: +The maximum number of buckets a datasource can return. High numbers can have a negative impact on your browser rendering performance. [[visualization-regionmap-showwarnings]]`visualization:regionmap:showWarnings`:: Shows a warning in a region map when terms cannot be joined to a shape. [[visualization-tilemap-wmsdefaults]]`visualization:tileMap:WMSdefaults`:: -The default properties for the WMS map server support in the coordinate map. +The default properties for the WMS map server supported in the coordinate map. [[visualization-tilemap-maxprecision]]`visualization:tileMap:maxPrecision`:: -The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, -and 12 is the maximum. See this -{ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[explanation of cell dimensions]. +The maximum geoHash precision displayed in tile maps. 7 is high, 10 is very high, +and 12 is the maximum. For more information, refer to +{ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[Cell dimensions at the equator]. [[visualize-enablelabs]]`visualize:enableLabs`:: -Enables users to create, view, and edit experimental visualizations. If disabled, -only visualizations that are considered production-ready are available to the -user. +Enables users to create, view, and edit experimental visualizations. When disabled, +only production-ready visualizations are available to users. [float] From 043848787d43ef2ae8efba77c899144291f2d14e Mon Sep 17 00:00:00 2001 From: Brandon Morelli Date: Thu, 18 Feb 2021 11:06:13 -0800 Subject: [PATCH 12/43] docs: add PHP agent info to docs (#91773) --- docs/apm/agent-configuration.asciidoc | 1 + docs/apm/filters.asciidoc | 3 ++- docs/apm/service-maps.asciidoc | 15 ++++++++------- docs/apm/troubleshooting.asciidoc | 1 + 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/apm/agent-configuration.asciidoc b/docs/apm/agent-configuration.asciidoc index d911c2154ea4c5..aaaca867a5a01b 100644 --- a/docs/apm/agent-configuration.asciidoc +++ b/docs/apm/agent-configuration.asciidoc @@ -46,6 +46,7 @@ Go Agent:: {apm-go-ref}/configuration.html[Configuration reference] Java Agent:: {apm-java-ref}/configuration.html[Configuration reference] .NET Agent:: {apm-dotnet-ref}/configuration.html[Configuration reference] Node.js Agent:: {apm-node-ref}/configuration.html[Configuration reference] +PHP Agent:: _Not yet supported_ Python Agent:: {apm-py-ref}/configuration.html[Configuration reference] Ruby Agent:: {apm-ruby-ref}/configuration.html[Configuration reference] Real User Monitoring (RUM) Agent:: {apm-rum-ref}/configuration.html[Configuration reference] diff --git a/docs/apm/filters.asciidoc b/docs/apm/filters.asciidoc index c405ea10ade3d2..3fe9146658eefc 100644 --- a/docs/apm/filters.asciidoc +++ b/docs/apm/filters.asciidoc @@ -52,8 +52,9 @@ See the documentation for each agent you're using to learn how to configure serv * *Go:* {apm-go-ref}/configuration.html#config-environment[`ELASTIC_APM_ENVIRONMENT`] * *Java:* {apm-java-ref}/config-core.html#config-environment[`environment`] -* *.NET* {apm-dotnet-ref}/config-core.html#config-environment[`Environment`] +* *.NET:* {apm-dotnet-ref}/config-core.html#config-environment[`Environment`] * *Node.js:* {apm-node-ref}/configuration.html#environment[`environment`] +* *PHP:* {apm-php-ref}/configuration-reference.html#config-environment[`environment`] * *Python:* {apm-py-ref}/configuration.html#config-environment[`environment`] * *Ruby:* {apm-ruby-ref}/configuration.html#config-environment[`environment`] * *Real User Monitoring:* {apm-rum-ref}/configuration.html#environment[`environment`] diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc index 7cc4da8a1fc1de..a3ac62a4c83436 100644 --- a/docs/apm/service-maps.asciidoc +++ b/docs/apm/service-maps.asciidoc @@ -87,10 +87,11 @@ Type and subtype are based on `span.type`, and `span.subtype`. Service maps are supported for the following Agent versions: [horizontal] -Go Agent:: ≥ v1.7.0 -Java Agent:: ≥ v1.13.0 -.NET Agent:: ≥ v1.3.0 -Node.js Agent:: ≥ v3.6.0 -Python Agent:: ≥ v5.5.0 -Ruby Agent:: ≥ v3.6.0 -Real User Monitoring (RUM) Agent:: ≥ v4.7.0 +Go agent:: ≥ v1.7.0 +Java agent:: ≥ v1.13.0 +.NET agent:: ≥ v1.3.0 +Node.js agent:: ≥ v3.6.0 +PHP agent:: _Not yet supported_ +Python agent:: ≥ v5.5.0 +Ruby agent:: ≥ v3.6.0 +Real User Monitoring (RUM) agent:: ≥ v4.7.0 diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index 465a3d652046db..5049321363f88e 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -17,6 +17,7 @@ don't forget to check our other troubleshooting guides or discussion forum: * {apm-go-ref}/troubleshooting.html[Go agent troubleshooting] * {apm-java-ref}/trouble-shooting.html[Java agent troubleshooting] * {apm-node-ref}/troubleshooting.html[Node.js agent troubleshooting] +* {apm-php-ref}/troubleshooting.html[PHP agent troubleshooting] * {apm-py-ref}/troubleshooting.html[Python agent troubleshooting] * {apm-ruby-ref}/debugging.html[Ruby agent troubleshooting] * {apm-rum-ref/troubleshooting.html[RUM troubleshooting] From 03206b688ab239ce837b469a8a690eab30a0a6ff Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 18 Feb 2021 14:13:23 -0500 Subject: [PATCH 13/43] [CI] Build and publish storybooks (#87701) --- .ci/.storybook/main.js | 28 +++++++ .eslintignore | 1 + packages/kbn-storybook/lib/default_config.ts | 7 ++ .../kbn-storybook/lib/templates/index.ejs | 10 +-- src/dev/storybook/aliases.ts | 2 + test/scripts/jenkins_storybook.sh | 23 +++++ vars/githubCommitStatus.groovy | 6 +- vars/githubPr.groovy | 9 ++ vars/kibanaPipeline.groovy | 1 + vars/storybooks.groovy | 83 +++++++++++++++++++ vars/tasks.groovy | 6 ++ .../canvas/storybook/preview-head.html | 4 +- 12 files changed, 171 insertions(+), 9 deletions(-) create mode 100644 .ci/.storybook/main.js create mode 100755 test/scripts/jenkins_storybook.sh create mode 100644 vars/storybooks.groovy diff --git a/.ci/.storybook/main.js b/.ci/.storybook/main.js new file mode 100644 index 00000000000000..e399ec087e1687 --- /dev/null +++ b/.ci/.storybook/main.js @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const config = require('@kbn/storybook').defaultConfig; +const aliases = require('../../src/dev/storybook/aliases.ts').storybookAliases; + +config.refs = {}; + +for (const alias of Object.keys(aliases).filter((a) => a !== 'ci_composite')) { + // snake_case -> Title Case + const title = alias + .replace(/_/g, ' ') + .split(' ') + .map((n) => n[0].toUpperCase() + n.slice(1)) + .join(' '); + + config.refs[alias] = { + title: title, + url: `${process.env.STORYBOOK_BASE_URL}/${alias}`, + }; +} + +module.exports = config; diff --git a/.eslintignore b/.eslintignore index ea8ab55ad77269..4559711bb9dd31 100644 --- a/.eslintignore +++ b/.eslintignore @@ -15,6 +15,7 @@ node_modules target snapshots.js +!/.ci !/.eslintrc.js !.storybook diff --git a/packages/kbn-storybook/lib/default_config.ts b/packages/kbn-storybook/lib/default_config.ts index 53c51e9cf29fe6..1b049761a3a985 100644 --- a/packages/kbn-storybook/lib/default_config.ts +++ b/packages/kbn-storybook/lib/default_config.ts @@ -14,4 +14,11 @@ export const defaultConfig: StorybookConfig = { typescript: { reactDocgen: false, }, + webpackFinal: (config, options) => { + if (process.env.CI) { + config.parallelism = 4; + config.cache = true; + } + return config; + }, }; diff --git a/packages/kbn-storybook/lib/templates/index.ejs b/packages/kbn-storybook/lib/templates/index.ejs index a4f8204c95d7a2..b193c87824d40f 100644 --- a/packages/kbn-storybook/lib/templates/index.ejs +++ b/packages/kbn-storybook/lib/templates/index.ejs @@ -16,12 +16,12 @@ - - - - + + + + <% if (typeof headHtmlSnippet !== 'undefined') { %> <%= headHtmlSnippet %> <% } %> <% diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index c72c81f489fb9d..f1a3737747573d 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -6,10 +6,12 @@ * Side Public License, v 1. */ +// Please also add new aliases to test/scripts/jenkins_storybook.sh export const storybookAliases = { apm: 'x-pack/plugins/apm/.storybook', canvas: 'x-pack/plugins/canvas/storybook', codeeditor: 'src/plugins/kibana_react/public/code_editor/.storybook', + ci_composite: '.ci/.storybook', url_template_editor: 'src/plugins/kibana_react/public/url_template_editor/.storybook', dashboard: 'src/plugins/dashboard/.storybook', dashboard_enhanced: 'x-pack/plugins/dashboard_enhanced/.storybook', diff --git a/test/scripts/jenkins_storybook.sh b/test/scripts/jenkins_storybook.sh new file mode 100755 index 00000000000000..8ebfc1035fe1f1 --- /dev/null +++ b/test/scripts/jenkins_storybook.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +cd "$XPACK_DIR/plugins/canvas" +node scripts/storybook --dll + +cd "$KIBANA_DIR" + +# yarn storybook --site apm # TODO re-enable after being fixed +yarn storybook --site canvas +yarn storybook --site ci_composite +yarn storybook --site url_template_editor +yarn storybook --site codeeditor +yarn storybook --site dashboard +yarn storybook --site dashboard_enhanced +yarn storybook --site data_enhanced +yarn storybook --site embeddable +yarn storybook --site infra +yarn storybook --site security_solution +yarn storybook --site ui_actions_enhanced +yarn storybook --site observability +yarn storybook --site presentation diff --git a/vars/githubCommitStatus.groovy b/vars/githubCommitStatus.groovy index 248d226169a61e..175dbe0c90542f 100644 --- a/vars/githubCommitStatus.groovy +++ b/vars/githubCommitStatus.groovy @@ -41,13 +41,15 @@ def trackBuild(commit, context, Closure closure) { } // state: error|failure|pending|success -def create(sha, state, description, context) { +def create(sha, state, description, context, targetUrl = null) { + targetUrl = targetUrl ?: env.BUILD_URL + withGithubCredentials { return githubApi.post("repos/elastic/kibana/statuses/${sha}", [ state: state, description: description, context: context, - target_url: env.BUILD_URL + target_url: targetUrl.toString() ]) } } diff --git a/vars/githubPr.groovy b/vars/githubPr.groovy index d024eb7346f8f7..a2a3a81f253a02 100644 --- a/vars/githubPr.groovy +++ b/vars/githubPr.groovy @@ -169,12 +169,18 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { ? getBuildStatusIncludingMetrics() : buildUtils.getBuildStatus() + def storybooksUrl = buildState.get('storybooksUrl') + def storybooksMessage = storybooksUrl ? "* [Storybooks Preview](${storybooksUrl})" : "* Storybooks not built" + if (!isFinal) { + storybooksMessage = storybooksUrl ? storybooksMessage : "* Storybooks not built yet" + def failuresPart = status != 'SUCCESS' ? ', with failures' : '' messages << """ ## :hourglass_flowing_sand: Build in-progress${failuresPart} * [continuous-integration/kibana-ci/pull-request](${env.BUILD_URL}) * Commit: ${getCommitHash()} + ${storybooksMessage} * This comment will update when the build is complete """ } else if (status == 'SUCCESS') { @@ -182,6 +188,7 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { ## :green_heart: Build Succeeded * [continuous-integration/kibana-ci/pull-request](${env.BUILD_URL}) * Commit: ${getCommitHash()} + ${storybooksMessage} ${getDocsChangesLink()} """ } else if(status == 'UNSTABLE') { @@ -189,6 +196,7 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { ## :yellow_heart: Build succeeded, but was flaky * [continuous-integration/kibana-ci/pull-request](${env.BUILD_URL}) * Commit: ${getCommitHash()} + ${storybooksMessage} ${getDocsChangesLink()} """.stripIndent() @@ -204,6 +212,7 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { ## :broken_heart: Build Failed * [continuous-integration/kibana-ci/pull-request](${env.BUILD_URL}) * Commit: ${getCommitHash()} + ${storybooksMessage} * [Pipeline Steps](${env.BUILD_URL}flowGraphTable) (look for red circles / failed steps) * [Interpreting CI Failures](https://www.elastic.co/guide/en/kibana/current/interpreting-ci-failures.html) ${getDocsChangesLink()} diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 7adf755bfc5834..1fe1d78658669a 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -460,6 +460,7 @@ def allCiTasks() { tasks.test() tasks.functionalOss() tasks.functionalXpack() + tasks.storybooksCi() } }, jest: { diff --git a/vars/storybooks.groovy b/vars/storybooks.groovy new file mode 100644 index 00000000000000..f3c4a97a7d4364 --- /dev/null +++ b/vars/storybooks.groovy @@ -0,0 +1,83 @@ +def getStorybooksBucket() { + return "ci-artifacts.kibana.dev/storybooks" +} + +def getDestinationDir() { + return env.ghprbPullId ? "pr-${env.ghprbPullId}" : buildState.get('checkoutInfo').branch.replace("/", "__") +} + +def getUrl() { + return "https://${getStorybooksBucket()}/${getDestinationDir()}" +} + +def getUrlLatest() { + return "${getUrl()}/latest" +} + +def getUrlForCommit() { + return "${getUrl()}/${buildState.get('checkoutInfo').commit}" +} + +def upload() { + dir("built_assets/storybook") { + sh "mv ci_composite composite" + + def storybooks = sh( + script: 'ls -1d */', + returnStdout: true + ).trim() + .split('\n') + .collect { it.replace('/', '') } + .findAll { it != 'composite' } + + def listHtml = storybooks.collect { """
  • ${it}
  • """ }.join("\n") + + def html = """ + + +

    Storybooks

    +

    Composite Storybook

    +

    All

    +
      + ${listHtml} +
    + + + """ + + writeFile(file: 'index.html', text: html) + + withGcpServiceAccount.fromVaultSecret('secret/kibana-issues/dev/ci-artifacts-key', 'value') { + kibanaPipeline.bash(""" + gsutil -q -m cp -r -z js,css,html,json,map,txt,svg '*' 'gs://${getStorybooksBucket()}/${getDestinationDir()}/${buildState.get('checkoutInfo').commit}/' + gsutil -h "Cache-Control:no-cache, max-age=0, no-transform" cp -z html 'index.html' 'gs://${getStorybooksBucket()}/${getDestinationDir()}/latest/' + """, "Upload Storybooks to GCS") + } + + buildState.set('storybooksUrl', getUrlForCommit()) + } +} + +def build() { + withEnv(["STORYBOOK_BASE_URL=${getUrlForCommit()}"]) { + kibanaPipeline.bash('test/scripts/jenkins_storybook.sh', 'Build Storybooks') + } +} + +def buildAndUpload() { + def sha = buildState.get('checkoutInfo').commit + def context = 'Build and Publish Storybooks' + + githubCommitStatus.create(sha, 'pending', 'Building Storybooks', context) + + try { + build() + upload() + githubCommitStatus.create(sha, 'success', 'Storybooks built', context, getUrlForCommit()) + } catch(ex) { + githubCommitStatus.create(sha, 'error', 'Building Storybooks failed', context) + throw ex + } +} + +return this diff --git a/vars/tasks.groovy b/vars/tasks.groovy index 7c40966ff5e04c..846eed85fb0762 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -124,4 +124,10 @@ def functionalXpack(Map params = [:]) { } } +def storybooksCi() { + task { + storybooks.buildAndUpload() + } +} + return this diff --git a/x-pack/plugins/canvas/storybook/preview-head.html b/x-pack/plugins/canvas/storybook/preview-head.html index bef08a5120a36b..f8a7de6ddbaf1a 100644 --- a/x-pack/plugins/canvas/storybook/preview-head.html +++ b/x-pack/plugins/canvas/storybook/preview-head.html @@ -2,5 +2,5 @@ This file is looked for by Storybook and included in the HEAD element if it exists. This is how we load the DLL content into the Storybook UI. --> - - + + From a82b13d147d9c4124266eb856bacf8cdb9e85d21 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 18 Feb 2021 14:52:50 -0500 Subject: [PATCH 14/43] [FTSR] Convert to tasks and add jest/api integration suites (#91770) --- .ci/Jenkinsfile_flaky | 137 +++++++++++++++++++++---------------- vars/kibanaPipeline.groovy | 11 +-- 2 files changed, 85 insertions(+), 63 deletions(-) diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index b9880c410fc686..7eafc66465bc72 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -3,47 +3,39 @@ library 'kibana-pipeline-library' kibanaLibrary.load() -def CI_GROUP_PARAM = params.CI_GROUP +def TASK_PARAM = params.TASK ?: params.CI_GROUP // Looks like 'oss:ciGroup:1', 'oss:firefoxSmoke' -def JOB_PARTS = CI_GROUP_PARAM.split(':') +def JOB_PARTS = TASK_PARAM.split(':') def IS_XPACK = JOB_PARTS[0] == 'xpack' -def JOB = JOB_PARTS[1] +def JOB = JOB_PARTS.size() > 1 ? JOB_PARTS[1] : JOB_PARTS[0] def CI_GROUP = JOB_PARTS.size() > 2 ? JOB_PARTS[2] : '' def EXECUTIONS = params.NUMBER_EXECUTIONS.toInteger() def AGENT_COUNT = getAgentCount(EXECUTIONS) - -def worker = getWorkerFromParams(IS_XPACK, JOB, CI_GROUP) - -def workerFailures = [] +def NEED_BUILD = JOB != 'jestIntegration' && JOB != 'apiIntegration' currentBuild.displayName += trunc(" ${params.GITHUB_OWNER}:${params.branch_specifier}", 24) currentBuild.description = "${params.CI_GROUP}
    Agents: ${AGENT_COUNT}
    Executions: ${params.NUMBER_EXECUTIONS}" kibanaPipeline(timeoutMinutes: 180) { def agents = [:] + def workerFailures = [] + + def worker = getWorkerFromParams(IS_XPACK, JOB, CI_GROUP) + for(def agentNumber = 1; agentNumber <= AGENT_COUNT; agentNumber++) { - def agentNumberInside = agentNumber def agentExecutions = floor(EXECUTIONS/AGENT_COUNT) + (agentNumber <= EXECUTIONS%AGENT_COUNT ? 1 : 0) + agents["agent-${agentNumber}"] = { - catchErrors { - print "Agent ${agentNumberInside} - ${agentExecutions} executions" - - withEnv([ - 'IGNORE_SHIP_CI_STATS_ERROR=true', - ]) { - workers.functional('flaky-test-runner', { - if (!IS_XPACK) { - kibanaPipeline.buildOss() - if (CI_GROUP == '1') { - runbld("./test/scripts/jenkins_build_kbn_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") - } - } else { - kibanaPipeline.buildXpack() - } - }, getWorkerMap(agentNumberInside, agentExecutions, worker, workerFailures))() - } - } + agentProcess( + agentNumber: agentNumber, + agentExecutions: agentExecutions, + worker: worker, + workerFailures: workerFailures, + needBuild: NEED_BUILD, + isXpack: IS_XPACK, + ciGroup: CI_GROUP + ) } } @@ -59,14 +51,70 @@ kibanaPipeline(timeoutMinutes: 180) { } } +def agentProcess(Map params = [:]) { + def config = [ + agentNumber: 1, + agentExecutions: 0, + worker: {}, + workerFailures: [], + needBuild: false, + isXpack: false, + ciGroup: null, + ] + params + + catchErrors { + print "Agent ${config.agentNumber} - ${config.agentExecutions} executions" + + withEnv([ + 'IGNORE_SHIP_CI_STATS_ERROR=true', + ]) { + kibanaPipeline.withTasks([ + parallel: 20, + ]) { + task { + if (config.needBuild) { + if (!config.isXpack) { + kibanaPipeline.buildOss() + } else { + kibanaPipeline.buildXpack() + } + } + + for(def i = 0; i < config.agentExecutions; i++) { + def taskNumber = i + task({ + withEnv([ + "REMOVE_KIBANA_INSTALL_DIR=1", + ]) { + catchErrors { + try { + config.worker() + } catch (ex) { + config.workerFailures << "agent-${config.agentNumber}-${taskNumber}" + throw ex + } + } + } + }) + } + } + } + } + } +} + def getWorkerFromParams(isXpack, job, ciGroup) { if (!isXpack) { if (job == 'accessibility') { return kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh') } else if (job == 'firefoxSmoke') { return kibanaPipeline.functionalTestProcess('firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh') - } else if(job == 'visualRegression') { + } else if (job == 'visualRegression') { return kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh') + } else if (job == 'jestIntegration') { + return kibanaPipeline.scriptTaskDocker('Jest Integration Tests', 'test/scripts/test/jest_integration.sh') + } else if (job == 'apiIntegration') { + return kibanaPipeline.scriptTask('API Integration Tests', 'test/scripts/test/api_integration.sh') } else { return kibanaPipeline.ossCiGroupProcess(ciGroup) } @@ -76,45 +124,16 @@ def getWorkerFromParams(isXpack, job, ciGroup) { return kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh') } else if (job == 'firefoxSmoke') { return kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh') - } else if(job == 'visualRegression') { + } else if (job == 'visualRegression') { return kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh') } else { return kibanaPipeline.xpackCiGroupProcess(ciGroup) } } -def getWorkerMap(agentNumber, numberOfExecutions, worker, workerFailures, maxWorkerProcesses = 12) { - def workerMap = [:] - def numberOfWorkers = Math.min(numberOfExecutions, maxWorkerProcesses) - - for(def i = 1; i <= numberOfWorkers; i++) { - def workerExecutions = floor(numberOfExecutions/numberOfWorkers + (i <= numberOfExecutions%numberOfWorkers ? 1 : 0)) - - workerMap["agent-${agentNumber}-worker-${i}"] = { workerNumber -> - for(def j = 0; j < workerExecutions; j++) { - print "Execute agent-${agentNumber} worker-${workerNumber}: ${j}" - withEnv([ - "REMOVE_KIBANA_INSTALL_DIR=1", - ]) { - catchErrors { - try { - worker(workerNumber) - } catch (ex) { - workerFailures << "agent-${agentNumber} worker-${workerNumber}-${j}" - throw ex - } - } - } - } - } - } - - return workerMap -} - def getAgentCount(executions) { - // Increase agent count every 24 worker processess, up to 3 agents maximum - return Math.min(3, 1 + floor(executions/24)) + // Increase agent count every 20 worker processess, up to 3 agents maximum + return Math.min(3, 1 + floor(executions/20)) } def trunc(str, length) { diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 1fe1d78658669a..466a04d9b6b39f 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -425,12 +425,13 @@ def buildXpackPlugins() { runbld('./test/scripts/jenkins_xpack_build_plugins.sh', 'Build X-Pack Plugins') } -def withTasks(Map params = [worker: [:]], Closure closure) { +def withTasks(Map params = [:], Closure closure) { catchErrors { - def config = [name: 'ci-worker', size: 'xxl', ramDisk: true] + (params.worker ?: [:]) + def config = [setupWork: {}, worker: [:], parallel: 24] + params + def workerConfig = [name: 'ci-worker', size: 'xxl', ramDisk: true] + config.worker - workers.ci(config) { - withCiTaskQueue(parallel: 24) { + workers.ci(workerConfig) { + withCiTaskQueue([parallel: config.parallel]) { parallel([ docker: { retry(2) { @@ -443,6 +444,8 @@ def withTasks(Map params = [worker: [:]], Closure closure) { xpackPlugins: { buildXpackPlugins() }, ]) + config.setupWork() + catchErrors { closure() } From 863a2d06a43ff77baf6c8978925a0cd75946924d Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Thu, 18 Feb 2021 13:58:55 -0600 Subject: [PATCH 15/43] Use correct environment in anomaly detection setup link (#91877) This was still using `uiFilters.environment` instead of environment, so the warning would never show. --- .../application/action_menu/anomaly_detection_setup_link.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx index 0c1eacb6e800ca..3cb31aa10c4feb 100644 --- a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx +++ b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx @@ -30,8 +30,9 @@ export type AnomalyDetectionApiResponse = APIReturnType<'GET /api/apm/settings/a const DEFAULT_DATA = { jobs: [], hasLegacyJobs: false }; export function AnomalyDetectionSetupLink() { - const { uiFilters } = useUrlParams(); - const environment = uiFilters.environment; + const { + urlParams: { environment }, + } = useUrlParams(); const { core } = useApmPluginContext(); const canGetJobs = !!core.application.capabilities.ml?.canGetJobs; const license = useLicenseContext(); From da25d2753b2ec4b608cddf8c2a80a1e7a1f3b4f0 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 18 Feb 2021 12:36:25 -0800 Subject: [PATCH 16/43] [Alerts][Docs] Added API documentation for alerts plugin (#91067) * Added API documentation for alerts plugin * Added link to user api * fixed links * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/create.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/delete.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/delete.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/disable.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/enable.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/disable.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/update.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/enable.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/find.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/find.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/find.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/find.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/find.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/find.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/get.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/get.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/health.asciidoc Co-authored-by: Gidi Meir Morris * Update docs/api/alerts/health.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/health.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/health.asciidoc Co-authored-by: Gidi Meir Morris * Update docs/api/alerts/health.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/health.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/health.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/list.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/health.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/health.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/list.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/list.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/api/alerts/list.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * fixed due to comments * fixed due to comments * fixed due to comments * fixed links * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * fixed due to comments Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: Gidi Meir Morris --- docs/api/alerts.asciidoc | 42 +++++++ docs/api/alerts/create.asciidoc | 189 ++++++++++++++++++++++++++++ docs/api/alerts/delete.asciidoc | 36 ++++++ docs/api/alerts/disable.asciidoc | 34 +++++ docs/api/alerts/enable.asciidoc | 34 +++++ docs/api/alerts/find.asciidoc | 117 +++++++++++++++++ docs/api/alerts/get.asciidoc | 70 +++++++++++ docs/api/alerts/health.asciidoc | 85 +++++++++++++ docs/api/alerts/list.asciidoc | 127 +++++++++++++++++++ docs/api/alerts/mute.asciidoc | 37 ++++++ docs/api/alerts/mute_all.asciidoc | 34 +++++ docs/api/alerts/unmute.asciidoc | 37 ++++++ docs/api/alerts/unmute_all.asciidoc | 34 +++++ docs/api/alerts/update.asciidoc | 134 ++++++++++++++++++++ docs/user/api.asciidoc | 1 + 15 files changed, 1011 insertions(+) create mode 100644 docs/api/alerts.asciidoc create mode 100644 docs/api/alerts/create.asciidoc create mode 100644 docs/api/alerts/delete.asciidoc create mode 100644 docs/api/alerts/disable.asciidoc create mode 100644 docs/api/alerts/enable.asciidoc create mode 100644 docs/api/alerts/find.asciidoc create mode 100644 docs/api/alerts/get.asciidoc create mode 100644 docs/api/alerts/health.asciidoc create mode 100644 docs/api/alerts/list.asciidoc create mode 100644 docs/api/alerts/mute.asciidoc create mode 100644 docs/api/alerts/mute_all.asciidoc create mode 100644 docs/api/alerts/unmute.asciidoc create mode 100644 docs/api/alerts/unmute_all.asciidoc create mode 100644 docs/api/alerts/update.asciidoc diff --git a/docs/api/alerts.asciidoc b/docs/api/alerts.asciidoc new file mode 100644 index 00000000000000..a19c538bcb4d7b --- /dev/null +++ b/docs/api/alerts.asciidoc @@ -0,0 +1,42 @@ +[[alerts-api]] +== Alerts APIs + +The following APIs are available for managing {kib} alerts. + +* <> to create an alert + +* <> to update the attributes for existing alerts + +* <> to retrieve a single alert by ID + +* <> to permanently remove an alert + +* <> to retrieve a paginated set of alerts by condition + +* <> to retrieve a list of all alert types + +* <> to enable a single alert by ID + +* <> to disable a single alert by ID + +* <> to mute alert instances for a single alert by ID + +* <> to unmute alert instances for a single alert by ID + +* <> to unmute all alert instances for a single alert by ID + +* <> to retrieve the health of the alerts framework + +include::alerts/create.asciidoc[] +include::alerts/update.asciidoc[] +include::alerts/get.asciidoc[] +include::alerts/delete.asciidoc[] +include::alerts/find.asciidoc[] +include::alerts/list.asciidoc[] +include::alerts/enable.asciidoc[] +include::alerts/disable.asciidoc[] +include::alerts/mute_all.asciidoc[] +include::alerts/mute.asciidoc[] +include::alerts/unmute_all.asciidoc[] +include::alerts/unmute.asciidoc[] +include::alerts/health.asciidoc[] diff --git a/docs/api/alerts/create.asciidoc b/docs/api/alerts/create.asciidoc new file mode 100644 index 00000000000000..9e188b971c9b54 --- /dev/null +++ b/docs/api/alerts/create.asciidoc @@ -0,0 +1,189 @@ +[[alerts-api-create]] +=== Create alert API +++++ +Create alert +++++ + +Create {kib} alerts. + +[[alerts-api-create-request]] +==== Request + +`POST :/api/alerts/alert` + +[[alerts-api-create-request-body]] +==== Request body + +`name`:: + (Required, string) A name to reference and search. + +`tags`:: + (Optional, string array) A list of keywords to reference and search. + +`alertTypeId`:: + (Required, string) The ID of the alert type that you want to call when the alert is scheduled to run. + +`schedule`:: + (Required, object) The schedule specifying when this alert should be run, using one of the available schedule formats specified under ++ +._Schedule Formats_. +[%collapsible%open] +===== +A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. + +We currently support the _Interval format_ which specifies the interval in seconds, minutes, hours or days at which the alert should execute. +Example: `{ interval: "10s" }`, `{ interval: "5m" }`, `{ interval: "1h" }`, `{ interval: "1d" }`. + +There are plans to support multiple other schedule formats in the near future. +===== + +`throttle`:: + (Optional, string) How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a `schedule` of 1 minute stays in a triggered state for 90 minutes, setting a `throttle` of `10m` or `1h` will prevent it from sending 90 notifications during this period. + +`notifyWhen`:: + (Required, string) The condition for throttling the notification: `onActionGroupChange`, `onActiveAlert`, or `onThrottleInterval`. + +`enabled`:: + (Optional, boolean) Indicates if you want to run the alert on an interval basis after it is created. + +`consumer`:: + (Required, string) The name of the application that owns the alert. This name has to match the Kibana Feature name, as that dictates the required RBAC privileges. + +`params`:: + (Required, object) The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined. + +`actions`:: + (Optional, object array) An array of the following action objects. ++ +.Properties of the action objects: +[%collapsible%open] +===== + `group`::: + (Required, string) Grouping actions is recommended for escalations for different types of alert instances. If you don't need this, set this value to `default`. + + `id`::: + (Required, string) The ID of the action saved object to execute. + + `actionTypeId`::: + (Required, string) The ID of the <>. + + `params`::: + (Required, object) The map to the `params` that the <> will receive. ` params` are handled as Mustache templates and passed a default set of context. +===== + + +[[alerts-api-create-request-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[alerts-api-create-example]] +==== Example + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerts/alert -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' +{ + "params":{ + "aggType":"avg", + "termSize":6, + "thresholdComparator":">", + "timeWindowSize":5, + "timeWindowUnit":"m", + "groupBy":"top", + "threshold":[ + 1000 + ], + "index":[ + ".test-index" + ], + "timeField":"@timestamp", + "aggField":"sheet.version", + "termField":"name.keyword" + }, + "consumer":"alerts", + "alertTypeId":".index-threshold", + "schedule":{ + "interval":"1m" + }, + "actions":[ + { + "id":"dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2", + "actionTypeId":".server-log", + "group":"threshold met", + "params":{ + "level":"info", + "message":"alert '{{alertName}}' is active for group '{{context.group}}':\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}" + } + } + ], + "tags":[ + "cpu" + ], + "notifyWhen":"onActionGroupChange", + "name":"my alert" +}' +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "id": "41893910-6bca-11eb-9e0d-85d233e3ee35", + "notifyWhen": "onActionGroupChange", + "params": { + "aggType": "avg", + "termSize": 6, + "thresholdComparator": ">", + "timeWindowSize": 5, + "timeWindowUnit": "m", + "groupBy": "top", + "threshold": [ + 1000 + ], + "index": [ + ".kibana" + ], + "timeField": "@timestamp", + "aggField": "sheet.version", + "termField": "name.keyword" + }, + "consumer": "alerts", + "alertTypeId": ".index-threshold", + "schedule": { + "interval": "1m" + }, + "actions": [ + { + "actionTypeId": ".server-log", + "group": "threshold met", + "params": { + "level": "info", + "message": "alert {{alertName}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}" + }, + "id": "dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2" + } + ], + "tags": [ + "cpu" + ], + "name": "my alert", + "enabled": true, + "throttle": null, + "apiKeyOwner": "elastic", + "createdBy": "elastic", + "updatedBy": "elastic", + "muteAll": false, + "mutedInstanceIds": [], + "updatedAt": "2021-02-10T18:03:19.961Z", + "createdAt": "2021-02-10T18:03:19.961Z", + "scheduledTaskId": "425b0800-6bca-11eb-9e0d-85d233e3ee35", + "executionStatus": { + "lastExecutionDate": "2021-02-10T18:03:19.966Z", + "status": "pending" + } +} +-------------------------------------------------- diff --git a/docs/api/alerts/delete.asciidoc b/docs/api/alerts/delete.asciidoc new file mode 100644 index 00000000000000..b51005daae658d --- /dev/null +++ b/docs/api/alerts/delete.asciidoc @@ -0,0 +1,36 @@ +[[alerts-api-delete]] +=== Delete alert API +++++ +Delete alert +++++ + +Permanently remove an alert. + +WARNING: Once you delete an alert, you cannot recover it. + +[[alerts-api-delete-request]] +==== Request + +`DELETE :/api/alerts/alert/` + +[[alerts-api-delete-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the alert that you want to remove. + +[[alerts-api-delete-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Delete an alert with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X DELETE api/alerts/alert/41893910-6bca-11eb-9e0d-85d233e3ee35 +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerts/disable.asciidoc b/docs/api/alerts/disable.asciidoc new file mode 100644 index 00000000000000..5f74c333794095 --- /dev/null +++ b/docs/api/alerts/disable.asciidoc @@ -0,0 +1,34 @@ +[[alerts-api-disable]] +=== Disable alert API +++++ +Disable alert +++++ + +Disable an alert. + +[[alerts-api-disable-request]] +==== Request + +`POST :/api/alerts/alert//_disable` + +[[alerts-api-disable-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the alert that you want to disable. + +[[alerts-api-disable-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Disable an alert with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerts/alert/41893910-6bca-11eb-9e0d-85d233e3ee35/_disable +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerts/enable.asciidoc b/docs/api/alerts/enable.asciidoc new file mode 100644 index 00000000000000..a10383f2a440d1 --- /dev/null +++ b/docs/api/alerts/enable.asciidoc @@ -0,0 +1,34 @@ +[[alerts-api-enable]] +=== Enable alert API +++++ +Enable alert +++++ + +Enable an alert. + +[[alerts-api-enable-request]] +==== Request + +`POST :/api/alerts/alert//_enable` + +[[alerts-api-enable-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the alert that you want to enable. + +[[alerts-api-enable-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Enable an alert with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerts/alert/41893910-6bca-11eb-9e0d-85d233e3ee35/_enable +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerts/find.asciidoc b/docs/api/alerts/find.asciidoc new file mode 100644 index 00000000000000..97cd9f4c19ba75 --- /dev/null +++ b/docs/api/alerts/find.asciidoc @@ -0,0 +1,117 @@ +[[alerts-api-find]] +=== Find alerts API +++++ +Find alerts +++++ + +Retrieve a paginated set of alerts based on condition. + +[[alerts-api-find-request]] +==== Request + +`GET :/api/alerts/_find` + +[[alerts-api-find-query-params]] +==== Query Parameters + +`per_page`:: + (Optional, number) The number of alerts to return per page. + +`page`:: + (Optional, number) The page number. + +`search`:: + (Optional, string) An Elasticsearch {ref}/query-dsl-simple-query-string-query.html[simple_query_string] query that filters the alerts in the response. + +`default_search_operator`:: + (Optional, string) The operator to use for the `simple_query_string`. The default is 'OR'. + +`search_fields`:: + (Optional, array|string) The fields to perform the `simple_query_string` parsed query against. + +`fields`:: + (Optional, array|string) The fields to return in the `attributes` key of the response. + +`sort_field`:: + (Optional, string) Sorts the response. Could be an alert fields returned in the `attributes` key of the response. + +`sort_order`:: + (Optional, string) Sort direction, either `asc` or `desc`. + +`has_reference`:: + (Optional, object) Filters the alerts that have a relations with the reference objects with the specific "type" and "ID". + +`filter`:: + (Optional, string) A <> string that you filter with an attribute from your saved object. + It should look like savedObjectType.attributes.title: "myTitle". However, If you used a direct attribute of a saved object, such as `updatedAt`, + you will have to define your filter, for example, savedObjectType.updatedAt > 2018-12-22. + +NOTE: As alerts change in {kib}, the results on each page of the response also +change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data. + +[[alerts-api-find-request-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Examples + +Find alerts with names that start with `my`: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/alerts/_find?search_fields=name&search=my* +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "page": 1, + "perPage": 10, + "total": 1, + "data": [ + { + "id": "0a037d60-6b62-11eb-9e0d-85d233e3ee35", + "notifyWhen": "onActionGroupChange", + "params": { + "aggType": "avg", + }, + "consumer": "alerts", + "alertTypeId": "test.alert.type", + "schedule": { + "interval": "1m" + }, + "actions": [], + "tags": [], + "name": "test alert", + "enabled": true, + "throttle": null, + "apiKeyOwner": "elastic", + "createdBy": "elastic", + "updatedBy": "elastic", + "muteAll": false, + "mutedInstanceIds": [], + "updatedAt": "2021-02-10T05:37:19.086Z", + "createdAt": "2021-02-10T05:37:19.086Z", + "scheduledTaskId": "0b092d90-6b62-11eb-9e0d-85d233e3ee35", + "executionStatus": { + "lastExecutionDate": "2021-02-10T17:55:14.262Z", + "status": "ok" + } + }, + ] +} +-------------------------------------------------- + +For parameters that accept multiple values (e.g. `fields`), repeat the +query parameter for each value: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/alerts/_find?fields=id&fields=name +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerts/get.asciidoc b/docs/api/alerts/get.asciidoc new file mode 100644 index 00000000000000..934d7466dec3d7 --- /dev/null +++ b/docs/api/alerts/get.asciidoc @@ -0,0 +1,70 @@ +[[alerts-api-get]] +=== Get alert API +++++ +Get alert +++++ + +Retrieve an alert by ID. + +[[alerts-api-get-request]] +==== Request + +`GET :/api/alerts/alert/` + +[[alerts-api-get-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the alert to retrieve. + +[[alerts-api-get-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[alerts-api-get-example]] +==== Example + +Retrieve the alert object with the ID `41893910-6bca-11eb-9e0d-85d233e3ee35`: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/alerts/alert/41893910-6bca-11eb-9e0d-85d233e3ee35 +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "id": "0a037d60-6b62-11eb-9e0d-85d233e3ee35", + "notifyWhen": "onActionGroupChange", + "params": { + "aggType": "avg", + }, + "consumer": "alerts", + "alertTypeId": "test.alert.type", + "schedule": { + "interval": "1m" + }, + "actions": [], + "tags": [], + "name": "test alert", + "enabled": true, + "throttle": null, + "apiKeyOwner": "elastic", + "createdBy": "elastic", + "updatedBy": "elastic", + "muteAll": false, + "mutedInstanceIds": [], + "updatedAt": "2021-02-10T05:37:19.086Z", + "createdAt": "2021-02-10T05:37:19.086Z", + "scheduledTaskId": "0b092d90-6b62-11eb-9e0d-85d233e3ee35", + "executionStatus": { + "lastExecutionDate": "2021-02-10T17:55:14.262Z", + "status": "ok" + } +} +-------------------------------------------------- diff --git a/docs/api/alerts/health.asciidoc b/docs/api/alerts/health.asciidoc new file mode 100644 index 00000000000000..3710ccf4249454 --- /dev/null +++ b/docs/api/alerts/health.asciidoc @@ -0,0 +1,85 @@ +[[alerts-api-health]] +=== Get Alerting framework health API +++++ +Get Alerting framework health +++++ + +Retrieve the health status of the Alerting framework. + +[[alerts-api-health-request]] +==== Request + +`GET :/api/alerts/_health` + +[[alerts-api-health-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[alerts-api-health-example]] +==== Example + +Retrieve the health status of the Alerting framework: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/alerts/_health +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "isSufficientlySecure":true, + "hasPermanentEncryptionKey":true, + "alertingFrameworkHeath":{ + "decryptionHealth":{ + "status":"ok", + "timestamp":"2021-02-10T23:35:04.949Z" + }, + "executionHealth":{ + "status":"ok", + "timestamp":"2021-02-10T23:35:04.949Z" + }, + "readHealth":{ + "status":"ok", + "timestamp":"2021-02-10T23:35:04.949Z" + } + } +} +-------------------------------------------------- + +The health API response contains the following properties: + +[cols="2*<"] +|=== + +| `isSufficientlySecure` +| Returns `false` if security is enabled, but TLS is not. + +| `hasPermanentEncryptionKey` +| Return the state `false` if Encrypted Saved Object plugin has not a permanent encryption Key. + +| `alertingFrameworkHeath` +| This state property has three substates that identify the health of the alerting framework API: `decryptionHealth`, `executionHealth`, and `readHealth`. + +|=== + +`alertingFrameworkHeath` consists of the following properties: + +[cols="2*<"] +|=== + +| `decryptionHealth` +| Returns the timestamp and status of the alert decryption: `ok`, `warn` or `error` . + +| `executionHealth` +| Returns the timestamp and status of the alert execution: `ok`, `warn` or `error`. + +| `readHealth` +| Returns the timestamp and status of the alert reading events: `ok`, `warn` or `error`. + +|=== diff --git a/docs/api/alerts/list.asciidoc b/docs/api/alerts/list.asciidoc new file mode 100644 index 00000000000000..0bc3e158ec2634 --- /dev/null +++ b/docs/api/alerts/list.asciidoc @@ -0,0 +1,127 @@ +[[alerts-api-list]] +=== List alert types API +++++ +List all alert types API +++++ + +Retrieve a list of all alert types. + +[[alerts-api-list-request]] +==== Request + +`GET :/api/alerts/list_alert_types` + +[[alerts-api-list-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[alerts-api-list-example]] +==== Example + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/alerts/list_alert_types +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +[ + { + "id":".index-threshold", + "name":"Index threshold", + "actionGroups":[ + { + "id":"threshold met", + "name":"Threshold met" + }, + { + "id":"recovered", + "name":"Recovered" + } + ], + "recoveryActionGroup":{ + "id":"recovered", + "name":"Recovered" + }, + "defaultActionGroupId":"threshold met", + "actionVariables":{ + "context":[ + { + "name":"message", + "description":"A pre-constructed message for the alert." + }, + ], + "state":[], + "params":[ + { + "name":"threshold", + "description":"An array of values to use as the threshold; 'between' and 'notBetween' require two values, the others require one." + }, + { + "name":"index", + "description":"index" + }, + ] + }, + "producer":"stackAlerts", + "minimumLicenseRequired":"basic", + "enabledInLicense":true, + "authorizedConsumers":{ + "alerts":{ + "read":true, + "all":true + }, + "stackAlerts":{ + "read":true, + "all":true + }, + "uptime":{ + "read":true, + "all":true + } + } + } +] +-------------------------------------------------- + +Each alert type contains the following properties: + +[cols="2*<"] +|=== + +| `name` +| The descriptive name of the alert type. + +| `id` +| The unique ID of the alert type. + +| `minimumLicenseRequired` +| The license required to use the alert type. + +| `enabledInLicense` +| Whether the alert type is enabled or disabled based on the license. + +| `actionGroups` +| An explicit list of groups for which the alert type can schedule actions, each with the action group's unique ID and human readable name. Alert `actions` validation will use this configuration to ensure that groups are valid. Use `kbn-i18n` to translate the names of the action group when registering the alert type. + +| `recoveryActionGroup` +| An action group to use when an alert instance goes from an active state, to an inactive one. Do not specify this action group under the `actionGroups` property. If `recoveryActionGroup` is not specified, the default `recovered` action group is used. + +| `defaultActionGroupId` +| The default ID for the alert type group. + +| `actionVariables` +| An explicit list of action variables that the alert type makes available via context and state in action parameter templates, and a short human readable description. The Alert UI will use this information to prompt users for these variables in action parameter editors. Use `kbn-i18n` to translate the descriptions. + +| `producer` +| The ID of the application producing this alert type. + +| `authorizedConsumers` +| The list of the plugins IDs that have access to the alert type. + +|=== diff --git a/docs/api/alerts/mute.asciidoc b/docs/api/alerts/mute.asciidoc new file mode 100644 index 00000000000000..9279786deae4cf --- /dev/null +++ b/docs/api/alerts/mute.asciidoc @@ -0,0 +1,37 @@ +[[alerts-api-mute]] +=== Mute alert instance API +++++ +Mute alert instance +++++ + +Mute an alert instance. + +[[alerts-api-mute-request]] +==== Request + +`POST :/api/alerts/alert//alert_instance//_mute` + +[[alerts-api-mute-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the alert whose instance you want to mute. + +`alert_instance_id`:: + (Required, string) The ID of the alert instance that you want to mute. + +[[alerts-api-mute-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Mute alert instance with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerts/alert/41893910-6bca-11eb-9e0d-85d233e3ee35/alert_instance/dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2/_mute +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerts/mute_all.asciidoc b/docs/api/alerts/mute_all.asciidoc new file mode 100644 index 00000000000000..f8a8c137240c6b --- /dev/null +++ b/docs/api/alerts/mute_all.asciidoc @@ -0,0 +1,34 @@ +[[alerts-api-mute-all]] +=== Mute all alert instances API +++++ +Mute all alert instances +++++ + +Mute all alert instances. + +[[alerts-api-mute-all-request]] +==== Request + +`POST :/api/alerts/alert//_mute_all` + +[[alerts-api-mute-all-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the alert whose instances you want to mute. + +[[alerts-api-mute-all-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Mute all alert instances with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerts/alert/41893910-6bca-11eb-9e0d-85d233e3ee35/_mute_all +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerts/unmute.asciidoc b/docs/api/alerts/unmute.asciidoc new file mode 100644 index 00000000000000..f091ae3f453257 --- /dev/null +++ b/docs/api/alerts/unmute.asciidoc @@ -0,0 +1,37 @@ +[[alerts-api-unmute]] +=== Unmute alert instance API +++++ +Unmute alert instance +++++ + +Unmute an alert instance. + +[[alerts-api-unmute-request]] +==== Request + +`POST :/api/alerts/alert//alert_instance//_unmute` + +[[alerts-api-unmute-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the alert whose instance you want to mute.. + +`alert_instance_id`:: + (Required, string) The ID of the alert instance that you want to unmute. + +[[alerts-api-unmute-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Unmute alert instance with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerts/alert/41893910-6bca-11eb-9e0d-85d233e3ee35/alert_instance/dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2/_unmute +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerts/unmute_all.asciidoc b/docs/api/alerts/unmute_all.asciidoc new file mode 100644 index 00000000000000..2359d120cf2602 --- /dev/null +++ b/docs/api/alerts/unmute_all.asciidoc @@ -0,0 +1,34 @@ +[[alerts-api-unmute-all]] +=== Unmute all alert instances API +++++ +Unmute all alert instances +++++ + +Unmute all alert instances. + +[[alerts-api-unmute-all-request]] +==== Request + +`POST :/api/alerts/alert//_unmute_all` + +[[alerts-api-unmute-all-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the alert whose instances you want to unmute. + +[[alerts-api-unmute-all-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +Unmute all alert instances with ID: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/alerts/alert/41893910-6bca-11eb-9e0d-85d233e3ee35/_unmute_all +-------------------------------------------------- +// KIBANA diff --git a/docs/api/alerts/update.asciidoc b/docs/api/alerts/update.asciidoc new file mode 100644 index 00000000000000..aee2dd049a66f7 --- /dev/null +++ b/docs/api/alerts/update.asciidoc @@ -0,0 +1,134 @@ +[[alerts-api-update]] +=== Update alert API +++++ +Update alert +++++ + +Update the attributes for an existing alert. + +[[alerts-api-update-request]] +==== Request + +`PUT :/api/alerts/alert/` + +[[alerts-api-update-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the alert that you want to update. + +[[alerts-api-update-request-body]] +==== Request body + +`name`:: + (Required, string) A name to reference and search. + +`tags`:: + (Optional, string array) A list of keywords to reference and search. + +`schedule`:: + (Required, object) When to run this alert. Use one of the available schedule formats. ++ +._Schedule Formats_. +[%collapsible%open] +===== +A schedule uses a key: value format. {kib} currently supports the _Interval format_ , which specifies the interval in seconds, minutes, hours, or days at which to execute the alert. + +Example: `{ interval: "10s" }`, `{ interval: "5m" }`, `{ interval: "1h" }`, `{ interval: "1d" }`. + +===== + +`throttle`:: + (Optional, string) How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a `schedule` of 1 minute stays in a triggered state for 90 minutes, setting a `throttle` of `10m` or `1h` will prevent it from sending 90 notifications during this period. + +`notifyWhen`:: + (Required, string) The condition for throttling the notification: `onActionGroupChange`, `onActiveAlert`, or `onThrottleInterval`. + +`params`:: + (Required, object) The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined. + +`actions`:: + (Optional, object array) An array of the following action objects. ++ +.Properties of the action objects: +[%collapsible%open] +===== + `group`::: + (Required, string) Grouping actions is recommended for escalations for different types of alert instances. If you don't need this, set the value to `default`. + + `id`::: + (Required, string) The ID of the action that saved object executes. + + `actionTypeId`::: + (Required, string) The id of the <>. + + `params`::: + (Required, object) The map to the `params` that the <> will receive. `params` are handled as Mustache templates and passed a default set of context. +===== + + +[[alerts-api-update-errors-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[alerts-api-update-example]] +==== Example + +Update an alert with ID `ac4e6b90-6be7-11eb-ba0d-9b1c1f912d74` with a different name: + +[source,sh] +-------------------------------------------------- +$ curl -X PUT api/alerts/alert/ac4e6b90-6be7-11eb-ba0d-9b1c1f912d74 + +{ + "notifyWhen": "onActionGroupChange", + "params": { + "aggType": "avg", + }, + "schedule": { + "interval": "1m" + }, + "actions": [], + "tags": [], + "name": "new name", + "throttle": null, +} +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "id": "ac4e6b90-6be7-11eb-ba0d-9b1c1f912d74", + "notifyWhen": "onActionGroupChange", + "params": { + "aggType": "avg", + }, + "consumer": "alerts", + "alertTypeId": "test.alert.type", + "schedule": { + "interval": "1m" + }, + "actions": [], + "tags": [], + "name": "new name", + "enabled": true, + "throttle": null, + "apiKeyOwner": "elastic", + "createdBy": "elastic", + "updatedBy": "elastic", + "muteAll": false, + "mutedInstanceIds": [], + "updatedAt": "2021-02-10T05:37:19.086Z", + "createdAt": "2021-02-10T05:37:19.086Z", + "scheduledTaskId": "0b092d90-6b62-11eb-9e0d-85d233e3ee35", + "executionStatus": { + "lastExecutionDate": "2021-02-10T17:55:14.262Z", + "status": "ok" + } +} +-------------------------------------------------- diff --git a/docs/user/api.asciidoc b/docs/user/api.asciidoc index 2ae83bee1e06c7..9916ab42186dc6 100644 --- a/docs/user/api.asciidoc +++ b/docs/user/api.asciidoc @@ -36,6 +36,7 @@ include::{kib-repo-dir}/api/features.asciidoc[] include::{kib-repo-dir}/api/spaces-management.asciidoc[] include::{kib-repo-dir}/api/role-management.asciidoc[] include::{kib-repo-dir}/api/saved-objects.asciidoc[] +include::{kib-repo-dir}/api/alerts.asciidoc[] include::{kib-repo-dir}/api/actions-and-connectors.asciidoc[] include::{kib-repo-dir}/api/dashboard-api.asciidoc[] include::{kib-repo-dir}/api/logstash-configuration-management.asciidoc[] From 0760bfb8701870c0991c853918ae6f981546ce6a Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 18 Feb 2021 15:34:50 -0600 Subject: [PATCH 17/43] [Fleet] Bootstrap functional test suite (#91898) --- .../components/agent_policy_section.tsx | 2 +- .../overview/components/agent_section.tsx | 2 +- .../components/datastream_section.tsx | 2 +- .../components/integration_section.tsx | 2 +- .../test/fleet_functional/apps/fleet/index.ts | 17 ++++++++ .../apps/fleet/overview_page.ts | 38 +++++++++++++++++ x-pack/test/fleet_functional/config.ts | 41 +++++++++++++++++++ .../ftr_provider_context.d.ts | 13 ++++++ .../fleet_functional/page_objects/index.ts | 14 +++++++ .../page_objects/overview_page.ts | 41 +++++++++++++++++++ .../test/fleet_functional/services/index.ts | 12 ++++++ 11 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 x-pack/test/fleet_functional/apps/fleet/index.ts create mode 100644 x-pack/test/fleet_functional/apps/fleet/overview_page.ts create mode 100644 x-pack/test/fleet_functional/config.ts create mode 100644 x-pack/test/fleet_functional/ftr_provider_context.d.ts create mode 100644 x-pack/test/fleet_functional/page_objects/index.ts create mode 100644 x-pack/test/fleet_functional/page_objects/overview_page.ts create mode 100644 x-pack/test/fleet_functional/services/index.ts diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/overview/components/agent_policy_section.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/overview/components/agent_policy_section.tsx index 5bf1a383423b28..c3b59458abf0ac 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/overview/components/agent_policy_section.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/overview/components/agent_policy_section.tsx @@ -31,7 +31,7 @@ export const OverviewPolicySection: React.FC<{ agentPolicies: AgentPolicy[] }> = }); return ( - + { const agentStatusRequest = useGetAgentStatus({}); return ( - + { } return ( - + { (item) => 'savedObject' in item && item.version > item.savedObject.attributes.version )?.length ?? 0; return ( - + { + before(async () => { + await overviewPage.navigateToOverview(); + }); + + it('should show the Integrations section', async () => { + await overviewPage.integrationsSectionExistsOrFail(); + }); + + it('should show the Agents section', async () => { + await overviewPage.agentSectionExistsOrFail(); + }); + + it('should show the Agent policies section', async () => { + await overviewPage.agentPolicySectionExistsOrFail(); + }); + + it('should show the Data streams section', async () => { + await overviewPage.datastreamSectionExistsOrFail(); + }); + }); + }); +} diff --git a/x-pack/test/fleet_functional/config.ts b/x-pack/test/fleet_functional/config.ts new file mode 100644 index 00000000000000..386f39d7ec6683 --- /dev/null +++ b/x-pack/test/fleet_functional/config.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { resolve } from 'path'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { pageObjects } from './page_objects'; +import { services } from './services'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + + return { + ...xpackFunctionalConfig.getAll(), + pageObjects, + testFiles: [resolve(__dirname, './apps/fleet')], + junit: { + reportName: 'X-Pack Fleet Functional Tests', + }, + services, + apps: { + ...xpackFunctionalConfig.get('apps'), + ['fleet']: { + pathname: '/app/fleet', + }, + }, + kbnTestServer: { + ...xpackFunctionalConfig.get('kbnTestServer'), + serverArgs: [ + ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), + '--xpack.fleet.enabled=true', + ], + }, + layout: { + fixedHeaderHeight: 200, + }, + }; +} diff --git a/x-pack/test/fleet_functional/ftr_provider_context.d.ts b/x-pack/test/fleet_functional/ftr_provider_context.d.ts new file mode 100644 index 00000000000000..ec28c00e72e47f --- /dev/null +++ b/x-pack/test/fleet_functional/ftr_provider_context.d.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { pageObjects } from './page_objects'; +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/fleet_functional/page_objects/index.ts b/x-pack/test/fleet_functional/page_objects/index.ts new file mode 100644 index 00000000000000..2c534285146e54 --- /dev/null +++ b/x-pack/test/fleet_functional/page_objects/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { pageObjects as xpackFunctionalPageObjects } from '../../functional/page_objects'; +import { OverviewPage } from './overview_page'; + +export const pageObjects = { + ...xpackFunctionalPageObjects, + overviewPage: OverviewPage, +}; diff --git a/x-pack/test/fleet_functional/page_objects/overview_page.ts b/x-pack/test/fleet_functional/page_objects/overview_page.ts new file mode 100644 index 00000000000000..ca58acd0a7b6a3 --- /dev/null +++ b/x-pack/test/fleet_functional/page_objects/overview_page.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; +import { PLUGIN_ID } from '../../../plugins/fleet/common'; + +// NOTE: import path below should be the deep path to the actual module - else we get CI errors +import { pagePathGetters } from '../../../plugins/fleet/public/applications/fleet/constants/page_paths'; + +export function OverviewPage({ getService, getPageObjects }: FtrProviderContext) { + const pageObjects = getPageObjects(['common']); + const testSubjects = getService('testSubjects'); + + return { + async navigateToOverview() { + await pageObjects.common.navigateToApp(PLUGIN_ID, { + hash: pagePathGetters.overview(), + }); + }, + + async integrationsSectionExistsOrFail() { + await testSubjects.existOrFail('fleet-integrations-section'); + }, + + async agentPolicySectionExistsOrFail() { + await testSubjects.existOrFail('fleet-agent-policy-section'); + }, + + async agentSectionExistsOrFail() { + await testSubjects.existOrFail('fleet-agent-section'); + }, + + async datastreamSectionExistsOrFail() { + await testSubjects.existOrFail('fleet-datastream-section'); + }, + }; +} diff --git a/x-pack/test/fleet_functional/services/index.ts b/x-pack/test/fleet_functional/services/index.ts new file mode 100644 index 00000000000000..f5cfb8a32d34ec --- /dev/null +++ b/x-pack/test/fleet_functional/services/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { services as xPackFunctionalServices } from '../../functional/services'; + +export const services = { + ...xPackFunctionalServices, +}; From 2408d003254f2352037381fdaf5797850fed1551 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Thu, 18 Feb 2021 14:42:17 -0700 Subject: [PATCH 18/43] [data.search] Use incrementCounter for search telemetry (#91230) * [data.search] Use incrementCounter for search telemetry * Update reported type * Retry conflicts * Fix telemetry check * Use saved object migration to drop previous document * Review feedback * Fix import Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../saved_objects/migrations/to_v7_12_0.ts | 17 +++++ .../server/saved_objects/search_telemetry.ts | 4 + .../data/server/search/collectors/fetch.ts | 12 ++- .../data/server/search/collectors/register.ts | 10 ++- .../data/server/search/collectors/usage.ts | 73 +++++++++---------- 5 files changed, 70 insertions(+), 46 deletions(-) create mode 100644 src/plugins/data/server/saved_objects/migrations/to_v7_12_0.ts diff --git a/src/plugins/data/server/saved_objects/migrations/to_v7_12_0.ts b/src/plugins/data/server/saved_objects/migrations/to_v7_12_0.ts new file mode 100644 index 00000000000000..955028c0f9bf20 --- /dev/null +++ b/src/plugins/data/server/saved_objects/migrations/to_v7_12_0.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SavedObjectMigrationFn } from 'kibana/server'; + +/** + * Drop the previous document's attributes, which report `averageDuration` incorrectly. + * @param doc + */ +export const migrate712: SavedObjectMigrationFn = (doc) => { + return { ...doc, attributes: {} }; +}; diff --git a/src/plugins/data/server/saved_objects/search_telemetry.ts b/src/plugins/data/server/saved_objects/search_telemetry.ts index 24f884c85b7c53..33ad4b74f31697 100644 --- a/src/plugins/data/server/saved_objects/search_telemetry.ts +++ b/src/plugins/data/server/saved_objects/search_telemetry.ts @@ -7,6 +7,7 @@ */ import { SavedObjectsType } from 'kibana/server'; +import { migrate712 } from './migrations/to_v7_12_0'; export const searchTelemetry: SavedObjectsType = { name: 'search-telemetry', @@ -16,4 +17,7 @@ export const searchTelemetry: SavedObjectsType = { dynamic: false, properties: {}, }, + migrations: { + '7.12.0': migrate712, + }, }; diff --git a/src/plugins/data/server/search/collectors/fetch.ts b/src/plugins/data/server/search/collectors/fetch.ts index 05e5558157b3ce..6dfc29e2cf2a66 100644 --- a/src/plugins/data/server/search/collectors/fetch.ts +++ b/src/plugins/data/server/search/collectors/fetch.ts @@ -11,14 +11,14 @@ import { first } from 'rxjs/operators'; import { SharedGlobalConfig } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; -import { Usage } from './register'; +import { CollectedUsage, ReportedUsage } from './register'; interface SearchTelemetry { - 'search-telemetry': Usage; + 'search-telemetry': CollectedUsage; } type ESResponse = SearchResponse; export function fetchProvider(config$: Observable) { - return async ({ esClient }: CollectorFetchContext): Promise => { + return async ({ esClient }: CollectorFetchContext): Promise => { const config = await config$.pipe(first()).toPromise(); const { body: esResponse } = await esClient.search( { @@ -37,6 +37,10 @@ export function fetchProvider(config$: Observable) { averageDuration: null, }; } - return esResponse.hits.hits[0]._source['search-telemetry']; + const { successCount, errorCount, totalDuration } = esResponse.hits.hits[0]._source[ + 'search-telemetry' + ]; + const averageDuration = totalDuration / successCount; + return { successCount, errorCount, averageDuration }; }; } diff --git a/src/plugins/data/server/search/collectors/register.ts b/src/plugins/data/server/search/collectors/register.ts index 2a5637d86e1bf9..a370377c30eeaf 100644 --- a/src/plugins/data/server/search/collectors/register.ts +++ b/src/plugins/data/server/search/collectors/register.ts @@ -10,7 +10,13 @@ import { PluginInitializerContext } from 'kibana/server'; import { UsageCollectionSetup } from '../../../../usage_collection/server'; import { fetchProvider } from './fetch'; -export interface Usage { +export interface CollectedUsage { + successCount: number; + errorCount: number; + totalDuration: number; +} + +export interface ReportedUsage { successCount: number; errorCount: number; averageDuration: number | null; @@ -21,7 +27,7 @@ export async function registerUsageCollector( context: PluginInitializerContext ) { try { - const collector = usageCollection.makeUsageCollector({ + const collector = usageCollection.makeUsageCollector({ type: 'search', isReady: () => true, fetch: fetchProvider(context.config.legacy.globalConfig$), diff --git a/src/plugins/data/server/search/collectors/usage.ts b/src/plugins/data/server/search/collectors/usage.ts index c5dc2414c0e808..c9f0a5bf249442 100644 --- a/src/plugins/data/server/search/collectors/usage.ts +++ b/src/plugins/data/server/search/collectors/usage.ts @@ -6,11 +6,13 @@ * Side Public License, v 1. */ +import { once } from 'lodash'; import type { CoreSetup, Logger } from 'kibana/server'; +import { SavedObjectsErrorHelpers } from '../../../../../core/server'; import type { IEsSearchResponse } from '../../../common'; -import type { Usage } from './register'; const SAVED_OBJECT_ID = 'search-telemetry'; +const MAX_RETRY_COUNT = 3; export interface SearchUsage { trackError(): Promise; @@ -18,51 +20,42 @@ export interface SearchUsage { } export function usageProvider(core: CoreSetup): SearchUsage { - const getTracker = (eventType: keyof Usage) => { - return async (duration?: number) => { - const repository = await core - .getStartServices() - .then(([coreStart]) => coreStart.savedObjects.createInternalRepository()); + const getRepository = once(async () => { + const [coreStart] = await core.getStartServices(); + return coreStart.savedObjects.createInternalRepository(); + }); - let attributes: Usage; - let doesSavedObjectExist: boolean = true; - - try { - const response = await repository.get(SAVED_OBJECT_ID, SAVED_OBJECT_ID); - attributes = response.attributes; - } catch (e) { - doesSavedObjectExist = false; - attributes = { - successCount: 0, - errorCount: 0, - averageDuration: 0, - }; - } - - attributes[eventType]++; - - // Only track the average duration for successful requests - if (eventType === 'successCount') { - attributes.averageDuration = - ((duration ?? 0) + (attributes.averageDuration ?? 0)) / (attributes.successCount ?? 1); + const trackSuccess = async (duration: number, retryCount = 0) => { + const repository = await getRepository(); + try { + await repository.incrementCounter(SAVED_OBJECT_ID, SAVED_OBJECT_ID, [ + { fieldName: 'successCount' }, + { + fieldName: 'totalDuration', + incrementBy: duration, + }, + ]); + } catch (e) { + if (SavedObjectsErrorHelpers.isConflictError(e) && retryCount < MAX_RETRY_COUNT) { + setTimeout(() => trackSuccess(duration, retryCount + 1), 1000); } + } + }; - try { - if (doesSavedObjectExist) { - await repository.update(SAVED_OBJECT_ID, SAVED_OBJECT_ID, attributes); - } else { - await repository.create(SAVED_OBJECT_ID, attributes, { id: SAVED_OBJECT_ID }); - } - } catch (e) { - // Version conflict error, swallow + const trackError = async (retryCount = 0) => { + const repository = await getRepository(); + try { + await repository.incrementCounter(SAVED_OBJECT_ID, SAVED_OBJECT_ID, [ + { fieldName: 'errorCount' }, + ]); + } catch (e) { + if (SavedObjectsErrorHelpers.isConflictError(e) && retryCount < MAX_RETRY_COUNT) { + setTimeout(() => trackError(retryCount + 1), 1000); } - }; + } }; - return { - trackError: () => getTracker('errorCount')(), - trackSuccess: getTracker('successCount'), - }; + return { trackSuccess, trackError }; } /** From 0f804677de62e484920a7ef6ce357de2ab624aa9 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Thu, 18 Feb 2021 13:43:03 -0800 Subject: [PATCH 19/43] [Fleet] Silently swallow 404 errors when deleting ingest pipelines (#91778) * Only show transform logs when there are transforms * Silently swallow 404 errors when deleting ingest pipelines * Change to IngestManagerError --- .../epm/elasticsearch/ingest_pipeline/remove.ts | 7 ++++++- .../services/epm/elasticsearch/transform/install.ts | 10 +++++++--- .../services/epm/elasticsearch/transform/remove.ts | 4 +++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/remove.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/remove.ts index f12d68190b4acb..4acc4767de5255 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/remove.ts @@ -8,6 +8,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { appContextService } from '../../../'; import { CallESAsCurrentUser, ElasticsearchAssetType } from '../../../../types'; +import { IngestManagerError } from '../../../../errors'; import { getInstallation } from '../../packages/get'; import { PACKAGES_SAVED_OBJECT_TYPE, EsAssetReference } from '../../../../../common'; @@ -61,7 +62,11 @@ export async function deletePipeline(callCluster: CallESAsCurrentUser, id: strin try { await callCluster('ingest.deletePipeline', { id }); } catch (err) { - throw new Error(`error deleting pipeline ${id}`); + // Only throw if error is not a 404 error. Sometimes the pipeline is already deleted, but we have + // duplicate references to them, see https://github.com/elastic/kibana/issues/91192 + if (err.statusCode !== 404) { + throw new IngestManagerError(`error deleting pipeline ${id}: ${err}`); + } } } } diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts index 57e1090f8954b4..948a9c56746f36 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts @@ -42,9 +42,13 @@ export const installTransform = async ( previousInstalledTransformEsAssets = installation.installed_es.filter( ({ type, id }) => type === ElasticsearchAssetType.transform ); - logger.info( - `Found previous transform references:\n ${JSON.stringify(previousInstalledTransformEsAssets)}` - ); + if (previousInstalledTransformEsAssets.length) { + logger.info( + `Found previous transform references:\n ${JSON.stringify( + previousInstalledTransformEsAssets + )}` + ); + } } // delete all previous transform diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts index b08b7cb7f1ec8d..0e947e0f0b90bb 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts @@ -26,7 +26,9 @@ export const deleteTransforms = async ( transformIds: string[] ) => { const logger = appContextService.getLogger(); - logger.info(`Deleting currently installed transform ids ${transformIds}`); + if (transformIds.length) { + logger.info(`Deleting currently installed transform ids ${transformIds}`); + } await Promise.all( transformIds.map(async (transformId) => { // get the index the transform From fe35e0de3b337e47bf36f5af8bdc2a00e437f9af Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 18 Feb 2021 18:45:15 -0500 Subject: [PATCH 20/43] [Fleet] Install Elastic Agent integration by default during setup (#91676) --- x-pack/plugins/fleet/common/constants/epm.ts | 1 + .../test/fleet_api_integration/apis/fleet_setup.ts | 14 ++++++++++++++ x-pack/test/fleet_api_integration/config.ts | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts index b223139803257f..aa17b16b3763ce 100644 --- a/x-pack/plugins/fleet/common/constants/epm.ts +++ b/x-pack/plugins/fleet/common/constants/epm.ts @@ -15,6 +15,7 @@ export const FLEET_SERVER_PACKAGE = 'fleet_server'; export const requiredPackages = { System: 'system', Endpoint: 'endpoint', + ElasticAgent: 'elastic_agent', } as const; // these are currently identical. we can separate if they later diverge diff --git a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts index 31d620cd34931f..d9f55d9fa0b740 100644 --- a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts +++ b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts @@ -105,5 +105,19 @@ export default function (providerContext: FtrProviderContext) { transient_metadata: { enabled: true }, }); }); + + it('should install default packages', async () => { + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxxx').expect(200); + + const { body: apiResponse } = await supertest + .get(`/api/fleet/epm/packages?experimental=true`) + .expect(200); + const installedPackages = apiResponse.response + .filter((p: any) => p.status === 'installed') + .map((p: any) => p.name) + .sort(); + + expect(installedPackages).to.eql(['elastic_agent', 'endpoint', 'system']); + }); }); } diff --git a/x-pack/test/fleet_api_integration/config.ts b/x-pack/test/fleet_api_integration/config.ts index 444b8c3a687765..b4833d96c407e4 100644 --- a/x-pack/test/fleet_api_integration/config.ts +++ b/x-pack/test/fleet_api_integration/config.ts @@ -15,7 +15,7 @@ import { defineDockerServersConfig } from '@kbn/test'; // example: https://beats-ci.elastic.co/blue/organizations/jenkins/Ingest-manager%2Fpackage-storage/detail/snapshot/74/pipeline/257#step-302-log-1. // It should be updated any time there is a new Docker image published for the Snapshot Distribution of the Package Registry. export const dockerImage = - 'docker.elastic.co/package-registry/distribution:5314869e2f6bc01d37b8652f7bda89248950b3a4'; + 'docker.elastic.co/package-registry/distribution:99dadb957d76b704637150d34a7219345cc0aeef'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); From 539f33e53beb9847f2ff4136ea8bba8519c9f375 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 18 Feb 2021 18:43:43 -0600 Subject: [PATCH 21/43] Revert "[SOM] fix flaky suites (#91809)" This reverts commit 386afdca8ffc8b5c61aa0c2ce0ea1a3476fd0aa7. --- test/functional/apps/management/_import_objects.ts | 1 + .../apps/saved_objects_management/edit_saved_object.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index a3daaf86294939..ca8d8c392ce49b 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -23,6 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const log = getService('log'); + // FLAKY: https://github.com/elastic/kibana/issues/89478 describe('import objects', function describeIndexTests() { describe('.ndjson file', () => { beforeEach(async function () { diff --git a/test/functional/apps/saved_objects_management/edit_saved_object.ts b/test/functional/apps/saved_objects_management/edit_saved_object.ts index 89889088bd73ba..81569c5bfc498a 100644 --- a/test/functional/apps/saved_objects_management/edit_saved_object.ts +++ b/test/functional/apps/saved_objects_management/edit_saved_object.ts @@ -55,7 +55,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await button.click(); }; - describe('saved objects edition page', () => { + // Flaky: https://github.com/elastic/kibana/issues/68400 + describe.skip('saved objects edition page', () => { beforeEach(async () => { await esArchiver.load('saved_objects_management/edit_saved_object'); }); From a1644112868b226f36661ac258716542558efb46 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 18 Feb 2021 21:00:19 -0500 Subject: [PATCH 22/43] [CI] backportrc can skip CI (#91886) --- vars/prChanges.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/vars/prChanges.groovy b/vars/prChanges.groovy index d082672c065a8c..8484df82102592 100644 --- a/vars/prChanges.groovy +++ b/vars/prChanges.groovy @@ -14,6 +14,7 @@ def getSkippablePaths() { /^.ci\/Jenkinsfile_[^\/]+$/, /^\.github\//, /\.md$/, + /^\.backportrc\.json$/ ] } From 8d9ac0058fbaba325932344f35186d1a7e39745c Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 18 Feb 2021 21:25:01 -0700 Subject: [PATCH 23/43] [Security Solutions] Fixes Cypress tests for indicator match by making the selectors more specific (#91947) ## Summary Fixes the indicator match rules cypress e2e tests by making the selectors more specific. Previously other rules and forms code which live on the DOM beside the indicator match rules could interfere when moving around on the DOM. Now with more specific selectors this should be less likely to happen. If it does happen again I will make the selectors even more specific. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../indicator_match_rule.spec.ts | 3 +-- .../cypress/screens/create_new_rule.ts | 15 ++++++++++++ .../cypress/tasks/create_new_rule.ts | 24 ++++++++++++------- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index bc52be678347aa..db29f44ceb98c8 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -98,8 +98,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { DETECTIONS_URL, RULE_CREATION } from '../../urls/navigation'; -// Skipped for 7.12 FF - flaky tests -describe.skip('indicator match', () => { +describe('indicator match', () => { describe('Detection rules, Indicator Match', () => { const expectedUrls = newThreatIndicatorRule.referenceUrls.join(''); const expectedFalsePositives = newThreatIndicatorRule.falsePositivesExamples.join(''); diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index a2fb94e462023e..3c7da2e2988475 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -36,9 +36,24 @@ export const CREATE_AND_ACTIVATE_BTN = '[data-test-subj="create-activate"]'; export const CUSTOM_QUERY_INPUT = '[data-test-subj="queryInput"]'; +export const THREAT_MAPPING_COMBO_BOX_INPUT = + '[data-test-subj="threatMatchInput"] [data-test-subj="fieldAutocompleteComboBox"]'; + +export const THREAT_MATCH_CUSTOM_QUERY_INPUT = + '[data-test-subj="detectionEngineStepDefineRuleQueryBar"] [data-test-subj="queryInput"]'; + +export const THREAT_MATCH_INDICATOR_QUERY_INPUT = + '[data-test-subj="detectionEngineStepDefineRuleThreatMatchIndices"] [data-test-subj="queryInput"]'; + export const THREAT_MATCH_QUERY_INPUT = '[data-test-subj="detectionEngineStepDefineThreatRuleQueryBar"] [data-test-subj="queryInput"]'; +export const THREAT_MATCH_INDICATOR_INDEX = + '[data-test-subj="detectionEngineStepDefineRuleIndices"] [data-test-subj="comboBoxInput"]'; + +export const THREAT_MATCH_INDICATOR_INDICATOR_INDEX = + '[data-test-subj="detectionEngineStepDefineRuleThreatMatchIndices"] [data-test-subj="comboBoxInput"]'; + export const THREAT_MATCH_AND_BUTTON = '[data-test-subj="andButton"]'; export const THREAT_ITEM_ENTRY_DELETE_BUTTON = '[data-test-subj="itemEntryDeleteButton"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 02ba3937ed542a..11d98f7b808edb 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -80,6 +80,11 @@ import { CUSTOM_QUERY_REQUIRED, RULES_CREATION_FORM, RULES_CREATION_PREVIEW, + THREAT_MATCH_INDICATOR_INDEX, + THREAT_MATCH_INDICATOR_INDICATOR_INDEX, + THREAT_MATCH_CUSTOM_QUERY_INPUT, + THREAT_MATCH_QUERY_INPUT, + THREAT_MAPPING_COMBO_BOX_INPUT, } from '../screens/create_new_rule'; import { TOAST_ERROR } from '../screens/shared'; import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; @@ -325,17 +330,17 @@ export const fillIndicatorMatchRow = ({ }) => { const computedRowNumber = rowNumber == null ? 1 : rowNumber; const computedValueRows = validColumns == null ? 'both' : validColumns; - const OFFSET = 2; - cy.get(COMBO_BOX_INPUT) - .eq(computedRowNumber * OFFSET + 1) + cy.get(THREAT_MAPPING_COMBO_BOX_INPUT) + .eq(computedRowNumber * 2 - 2) + .eq(0) .type(indexField); if (computedValueRows === 'indexField' || computedValueRows === 'both') { cy.get(`button[title="${indexField}"]`) .should('be.visible') .then(([e]) => e.click()); } - cy.get(COMBO_BOX_INPUT) - .eq(computedRowNumber * OFFSET + 2) + cy.get(THREAT_MAPPING_COMBO_BOX_INPUT) + .eq(computedRowNumber * 2 - 1) .type(indicatorIndexField); if (computedValueRows === 'indicatorField' || computedValueRows === 'both') { @@ -393,19 +398,20 @@ export const getAboutContinueButton = () => cy.get(ABOUT_CONTINUE_BTN); export const getDefineContinueButton = () => cy.get(DEFINE_CONTINUE_BUTTON); /** Returns the indicator index pattern */ -export const getIndicatorIndex = () => cy.get(COMBO_BOX_INPUT).eq(0); +export const getIndicatorIndex = () => cy.get(THREAT_MATCH_INDICATOR_INDEX).eq(0); /** Returns the indicator's indicator index */ -export const getIndicatorIndicatorIndex = () => cy.get(COMBO_BOX_INPUT).eq(2); +export const getIndicatorIndicatorIndex = () => + cy.get(THREAT_MATCH_INDICATOR_INDICATOR_INDEX).eq(0); /** Returns the index pattern's clear button */ export const getIndexPatternClearButton = () => cy.get(COMBO_BOX_CLEAR_BTN); /** Returns the custom query input */ -export const getCustomQueryInput = () => cy.get(CUSTOM_QUERY_INPUT).eq(0); +export const getCustomQueryInput = () => cy.get(THREAT_MATCH_CUSTOM_QUERY_INPUT).eq(0); /** Returns the custom query input */ -export const getCustomIndicatorQueryInput = () => cy.get(CUSTOM_QUERY_INPUT).eq(1); +export const getCustomIndicatorQueryInput = () => cy.get(THREAT_MATCH_QUERY_INPUT).eq(0); /** Returns custom query required content */ export const getCustomQueryInvalidationText = () => cy.contains(CUSTOM_QUERY_REQUIRED); From 494d1decf5a5c595ea034a335e0116d9ba76330a Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Fri, 19 Feb 2021 11:54:28 +0200 Subject: [PATCH 24/43] [Indexpattern management] Use indexPatterns Service instead of savedObjects client (#91839) * [Index pattern management] Use indexPatterns Service instead of savedObjects client * Minor fixes * Keep the same test setup --- .../step_index_pattern.test.tsx | 4 +- .../step_index_pattern/step_index_pattern.tsx | 19 +---- .../edit_index_pattern/edit_index_pattern.tsx | 9 +-- .../index_pattern_table.tsx | 14 +--- .../public/components/utils.test.ts | 39 +++++----- .../public/components/utils.ts | 76 +++++++++---------- .../mount_management_section.tsx | 3 +- .../index_pattern_management/public/mocks.ts | 10 +-- .../index_pattern_management/public/types.ts | 2 - 9 files changed, 62 insertions(+), 114 deletions(-) diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx index ac025dba95bcd3..8b4f751a4e3a3d 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx @@ -7,7 +7,6 @@ */ import React from 'react'; -import { SavedObjectsFindResponsePublic } from 'kibana/public'; import { StepIndexPattern, canPreselectTimeField } from './step_index_pattern'; import { Header } from './components/header'; import { IndexPatternCreationConfig } from '../../../../../../../plugins/index_pattern_management/public'; @@ -43,8 +42,7 @@ const goToNextStep = () => {}; const mockContext = mockManagementPlugin.createIndexPatternManagmentContext(); -mockContext.savedObjects.client.find = async () => - Promise.resolve(({ savedObjects: [] } as unknown) as SavedObjectsFindResponsePublic); +mockContext.data.indexPatterns.getTitles = async () => Promise.resolve([]); mockContext.uiSettings.get.mockReturnValue(''); describe('StepIndexPattern', () => { diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx index d7038a754fc6be..052e454041181e 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx @@ -10,11 +10,7 @@ import React, { Component } from 'react'; import { EuiSpacer, EuiCallOut, EuiSwitchEvent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - indexPatterns, - IndexPatternAttributes, - UI_SETTINGS, -} from '../../../../../../../plugins/data/public'; +import { indexPatterns, UI_SETTINGS } from '../../../../../../../plugins/data/public'; import { getIndices, containsIllegalCharacters, @@ -118,18 +114,7 @@ export class StepIndexPattern extends Component { - const { - savedObjects, - } = await this.context.services.savedObjects.client.find({ - type: 'index-pattern', - fields: ['title'], - perPage: 10000, - }); - - const existingIndexPatterns = savedObjects.map((obj) => - obj && obj.attributes ? obj.attributes.title : '' - ) as string[]; - + const existingIndexPatterns = await this.context.services.data.indexPatterns.getTitles(); this.setState({ existingIndexPatterns }); }; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index 30edc430f6b959..e314c00bc8176f 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -26,8 +26,6 @@ import { useKibana } from '../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../types'; import { Tabs } from './tabs'; import { IndexHeader } from './index_header'; -import { IndexPatternTableItem } from '../types'; -import { getIndexPatterns } from '../utils'; export interface EditIndexPatternProps extends RouteComponentProps { indexPattern: IndexPattern; @@ -62,7 +60,6 @@ export const EditIndexPattern = withRouter( uiSettings, indexPatternManagementStart, overlays, - savedObjects, chrome, data, } = useKibana().services; @@ -97,11 +94,7 @@ export const EditIndexPattern = withRouter( const removePattern = () => { async function doRemove() { if (indexPattern.id === defaultIndex) { - const indexPatterns: IndexPatternTableItem[] = await getIndexPatterns( - savedObjects.client, - uiSettings.get('defaultIndex'), - indexPatternManagementStart - ); + const indexPatterns = await data.indexPatterns.getIdsWithTitle(); uiSettings.remove('defaultIndex'); const otherPatterns = filter(indexPatterns, (pattern) => { return pattern.id !== indexPattern.id; diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx index a2c30ea2884459..b09246b5af8adc 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -69,13 +69,13 @@ interface Props extends RouteComponentProps { export const IndexPatternTable = ({ canSave, history }: Props) => { const { setBreadcrumbs, - savedObjects, uiSettings, indexPatternManagementStart, chrome, docLinks, application, http, + data, getMlCardState, } = useKibana().services; const [indexPatterns, setIndexPatterns] = useState([]); @@ -92,21 +92,15 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { history.push ); const gettedIndexPatterns: IndexPatternTableItem[] = await getIndexPatterns( - savedObjects.client, uiSettings.get('defaultIndex'), - indexPatternManagementStart + indexPatternManagementStart, + data.indexPatterns ); setIsLoadingIndexPatterns(false); setCreationOptions(options); setIndexPatterns(gettedIndexPatterns); })(); - }, [ - history.push, - indexPatterns.length, - indexPatternManagementStart, - uiSettings, - savedObjects.client, - ]); + }, [history.push, indexPatterns.length, indexPatternManagementStart, uiSettings, data]); const removeAliases = (item: MatchedItem) => !((item as unknown) as ResolveIndexResponseItemAlias).indices; diff --git a/src/plugins/index_pattern_management/public/components/utils.test.ts b/src/plugins/index_pattern_management/public/components/utils.test.ts index a1e60a4507b3c9..15e0a65390f4d3 100644 --- a/src/plugins/index_pattern_management/public/components/utils.test.ts +++ b/src/plugins/index_pattern_management/public/components/utils.test.ts @@ -5,36 +5,33 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import { IndexPatternsContract } from 'src/plugins/data/public'; import { getIndexPatterns } from './utils'; -import { coreMock } from '../../../../core/public/mocks'; import { mockManagementPlugin } from '../mocks'; -const { savedObjects } = coreMock.createStart(); -const mockManagementPluginStart = mockManagementPlugin.createStartContract(); - -(savedObjects.client.find as jest.Mock).mockResolvedValue({ - savedObjects: [ - { - id: 'test', - get: () => { - return 'test name'; +const indexPatternContractMock = ({ + getIdsWithTitle: jest.fn().mockReturnValue( + Promise.resolve([ + { + id: 'test', + title: 'test name', }, - }, - { - id: 'test1', - get: () => { - return 'test name 1'; + { + id: 'test1', + title: 'test name 1', }, - }, - ], -}); + ]) + ), + get: jest.fn().mockReturnValue(Promise.resolve({})), +} as unknown) as jest.Mocked; + +const mockManagementPluginStart = mockManagementPlugin.createStartContract(); test('getting index patterns', async () => { const indexPatterns = await getIndexPatterns( - savedObjects.client, 'test', - mockManagementPluginStart + mockManagementPluginStart, + indexPatternContractMock ); expect(indexPatterns).toMatchSnapshot(); }); diff --git a/src/plugins/index_pattern_management/public/components/utils.ts b/src/plugins/index_pattern_management/public/components/utils.ts index 59766e398e54e3..5701a1e3752044 100644 --- a/src/plugins/index_pattern_management/public/components/utils.ts +++ b/src/plugins/index_pattern_management/public/components/utils.ts @@ -6,54 +6,46 @@ * Side Public License, v 1. */ -import { IIndexPattern } from 'src/plugins/data/public'; -import { SavedObjectsClientContract } from 'src/core/public'; +import { IndexPatternsContract } from 'src/plugins/data/public'; import { IndexPatternManagementStart } from '../plugin'; export async function getIndexPatterns( - savedObjectsClient: SavedObjectsClientContract, defaultIndex: string, - indexPatternManagementStart: IndexPatternManagementStart + indexPatternManagementStart: IndexPatternManagementStart, + indexPatternsService: IndexPatternsContract ) { - return ( - savedObjectsClient - .find({ - type: 'index-pattern', - fields: ['title', 'type'], - perPage: 10000, - }) - .then((response) => - response.savedObjects - .map((pattern) => { - const id = pattern.id; - const title = pattern.get('title'); - const isDefault = defaultIndex === id; + const existingIndexPatterns = await indexPatternsService.getIdsWithTitle(); + const indexPatternsListItems = await Promise.all( + existingIndexPatterns.map(async ({ id, title }) => { + const isDefault = defaultIndex === id; + const pattern = await indexPatternsService.get(id); + const tags = (indexPatternManagementStart as IndexPatternManagementStart).list.getIndexPatternTags( + pattern, + isDefault + ); - const tags = (indexPatternManagementStart as IndexPatternManagementStart).list.getIndexPatternTags( - pattern, - isDefault - ); + return { + id, + title, + default: isDefault, + tags, + // the prepending of 0 at the default pattern takes care of prioritization + // so the sorting will but the default index on top + // or on bottom of a the table + sort: `${isDefault ? '0' : '1'}${title}`, + }; + }) + ); - return { - id, - title, - default: isDefault, - tags, - // the prepending of 0 at the default pattern takes care of prioritization - // so the sorting will but the default index on top - // or on bottom of a the table - sort: `${isDefault ? '0' : '1'}${title}`, - }; - }) - .sort((a, b) => { - if (a.sort < b.sort) { - return -1; - } else if (a.sort > b.sort) { - return 1; - } else { - return 0; - } - }) - ) || [] + return ( + indexPatternsListItems.sort((a, b) => { + if (a.sort < b.sort) { + return -1; + } else if (a.sort > b.sort) { + return 1; + } else { + return 0; + } + }) || [] ); } diff --git a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx index e47f60ad6fcdd6..355f529fe0f759 100644 --- a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx @@ -41,7 +41,7 @@ export async function mountManagementSection( getMlCardState: () => MlCardState ) { const [ - { chrome, application, savedObjects, uiSettings, notifications, overlays, http, docLinks }, + { chrome, application, uiSettings, notifications, overlays, http, docLinks }, { data, indexPatternFieldEditor }, indexPatternManagementStart, ] = await getStartServices(); @@ -54,7 +54,6 @@ export async function mountManagementSection( const deps: IndexPatternManagmentContext = { chrome, application, - savedObjects, uiSettings, notifications, overlays, diff --git a/src/plugins/index_pattern_management/public/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts index 309d5a5611cd62..606f9edafbca97 100644 --- a/src/plugins/index_pattern_management/public/mocks.ts +++ b/src/plugins/index_pattern_management/public/mocks.ts @@ -75,14 +75,7 @@ const docLinks = { const createIndexPatternManagmentContext = (): { [key in keyof IndexPatternManagmentContext]: any; } => { - const { - chrome, - application, - savedObjects, - uiSettings, - notifications, - overlays, - } = coreMock.createStart(); + const { chrome, application, uiSettings, notifications, overlays } = coreMock.createStart(); const { http } = coreMock.createSetup(); const data = dataPluginMock.createStartContract(); const indexPatternFieldEditor = indexPatternFieldEditorPluginMock.createStartContract(); @@ -90,7 +83,6 @@ const createIndexPatternManagmentContext = (): { return { chrome, application, - savedObjects, uiSettings, notifications, overlays, diff --git a/src/plugins/index_pattern_management/public/types.ts b/src/plugins/index_pattern_management/public/types.ts index 62ee18ababc0bd..58a138df633fd3 100644 --- a/src/plugins/index_pattern_management/public/types.ts +++ b/src/plugins/index_pattern_management/public/types.ts @@ -11,7 +11,6 @@ import { ApplicationStart, IUiSettingsClient, OverlayStart, - SavedObjectsStart, NotificationsStart, DocLinksStart, HttpSetup, @@ -25,7 +24,6 @@ import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/p export interface IndexPatternManagmentContext { chrome: ChromeStart; application: ApplicationStart; - savedObjects: SavedObjectsStart; uiSettings: IUiSettingsClient; notifications: NotificationsStart; overlays: OverlayStart; From bf7fdfc87cb04ecb5f6081d9056102d707997e9b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 19 Feb 2021 11:30:40 +0100 Subject: [PATCH 25/43] [Lens] Pass used histogram interval to chart (#91370) --- ...ibana-plugin-plugins-data-public.search.md | 1 + .../search/aggs/buckets/histogram.test.ts | 22 +++++ .../common/search/aggs/buckets/histogram.ts | 42 ++++++-- .../get_number_histogram_interval.test.ts | 98 +++++++++++++++++++ .../utils/get_number_histogram_interval.ts | 28 ++++++ .../data/common/search/aggs/utils/index.ts | 1 + src/plugins/data/public/index.ts | 2 + src/plugins/data/public/public.api.md | 31 +++--- .../__snapshots__/expression.test.tsx.snap | 49 ++++++++++ .../xy_visualization/expression.test.tsx | 43 +++++++- .../public/xy_visualization/expression.tsx | 29 +++--- 11 files changed, 308 insertions(+), 38 deletions(-) create mode 100644 src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.test.ts create mode 100644 src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md index 4b3c915b49c2dc..440fd25993d643 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md @@ -46,6 +46,7 @@ search: { boundLabel: string; intervalLabel: string; })[]; + getNumberHistogramIntervalByDatatableColumn: (column: import("../../expressions").DatatableColumn) => number | undefined; }; getRequestInspectorStats: typeof getRequestInspectorStats; getResponseInspectorStats: typeof getResponseInspectorStats; diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts index bddc7060af4401..23693eaf5fca52 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts @@ -12,6 +12,7 @@ import { AggTypesDependencies } from '../agg_types'; import { BUCKET_TYPES } from './bucket_agg_types'; import { IBucketHistogramAggConfig, getHistogramBucketAgg, AutoBounds } from './histogram'; import { BucketAggType } from './bucket_agg_type'; +import { SerializableState } from 'src/plugins/expressions/common'; describe('Histogram Agg', () => { let aggTypesDependencies: AggTypesDependencies; @@ -230,6 +231,27 @@ describe('Histogram Agg', () => { expect(params.interval).toBeNaN(); }); + test('will serialize the auto interval along with the actually chosen interval and deserialize correctly', () => { + const aggConfigs = getAggConfigs({ + interval: 'auto', + field: { + name: 'field', + }, + }); + (aggConfigs.aggs[0] as IBucketHistogramAggConfig).setAutoBounds({ min: 0, max: 1000 }); + const serializedAgg = aggConfigs.aggs[0].serialize(); + const serializedIntervalParam = (serializedAgg.params as SerializableState).used_interval; + expect(serializedIntervalParam).toBe(500); + const freshHistogramAggConfig = getAggConfigs({ + interval: 100, + field: { + name: 'field', + }, + }).aggs[0]; + freshHistogramAggConfig.setParams(serializedAgg.params); + expect(freshHistogramAggConfig.getParam('interval')).toEqual('auto'); + }); + describe('interval scaling', () => { const getInterval = ( maxBars: number, diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.ts b/src/plugins/data/common/search/aggs/buckets/histogram.ts index 5d6d7d509f08e8..e04ebfe494ba91 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.ts @@ -8,6 +8,7 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { IUiSettingsClient } from 'kibana/public'; import { KBN_FIELD_TYPES, UI_SETTINGS } from '../../../../common'; import { AggTypesDependencies } from '../agg_types'; @@ -39,6 +40,7 @@ export interface IBucketHistogramAggConfig extends IBucketAggConfig { export interface AggParamsHistogram extends BaseAggParams { field: string; interval: number | string; + used_interval?: number | string; maxBars?: number; intervalBase?: number; min_doc_count?: boolean; @@ -141,17 +143,22 @@ export const getHistogramBucketAgg = ({ }); }, write(aggConfig, output) { - const values = aggConfig.getAutoBounds(); - - output.params.interval = calculateHistogramInterval({ - values, - interval: aggConfig.params.interval, - maxBucketsUiSettings: getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS), - maxBucketsUserInput: aggConfig.params.maxBars, - intervalBase: aggConfig.params.intervalBase, - esTypes: aggConfig.params.field?.spec?.esTypes || [], - }); + output.params.interval = calculateInterval(aggConfig, getConfig); + }, + }, + { + name: 'used_interval', + default: autoInterval, + shouldShow() { + return false; }, + write: () => {}, + serialize(val, aggConfig) { + if (!aggConfig) return undefined; + // store actually used auto interval in serialized agg config to be able to read it from the result data table meta information + return calculateInterval(aggConfig, getConfig); + }, + toExpressionAst: () => undefined, }, { name: 'maxBars', @@ -193,3 +200,18 @@ export const getHistogramBucketAgg = ({ }, ], }); + +function calculateInterval( + aggConfig: IBucketHistogramAggConfig, + getConfig: IUiSettingsClient['get'] +): any { + const values = aggConfig.getAutoBounds(); + return calculateHistogramInterval({ + values, + interval: aggConfig.params.interval, + maxBucketsUiSettings: getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS), + maxBucketsUserInput: aggConfig.params.maxBars, + intervalBase: aggConfig.params.intervalBase, + esTypes: aggConfig.params.field?.spec?.esTypes || [], + }); +} diff --git a/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.test.ts b/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.test.ts new file mode 100644 index 00000000000000..9b08426b551aa3 --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.test.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getNumberHistogramIntervalByDatatableColumn } from '.'; +import { BUCKET_TYPES } from '../buckets'; + +describe('getNumberHistogramIntervalByDatatableColumn', () => { + it('returns nothing on column from other data source', () => { + expect( + getNumberHistogramIntervalByDatatableColumn({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'essql', + }, + }) + ).toEqual(undefined); + }); + + it('returns nothing on non histogram column', () => { + expect( + getNumberHistogramIntervalByDatatableColumn({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.TERMS, + }, + }, + }) + ).toEqual(undefined); + }); + + it('returns interval on resolved auto interval', () => { + expect( + getNumberHistogramIntervalByDatatableColumn({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.HISTOGRAM, + params: { + interval: 'auto', + used_interval: 20, + }, + }, + }, + }) + ).toEqual(20); + }); + + it('returns interval on fixed interval', () => { + expect( + getNumberHistogramIntervalByDatatableColumn({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.HISTOGRAM, + params: { + interval: 7, + used_interval: 7, + }, + }, + }, + }) + ).toEqual(7); + }); + + it('returns undefined if information is not available', () => { + expect( + getNumberHistogramIntervalByDatatableColumn({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.HISTOGRAM, + params: {}, + }, + }, + }) + ).toEqual(undefined); + }); +}); diff --git a/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.ts b/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.ts new file mode 100644 index 00000000000000..e1c0cf2d69c60c --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DatatableColumn } from 'src/plugins/expressions/common'; +import type { AggParamsHistogram } from '../buckets'; +import { BUCKET_TYPES } from '../buckets/bucket_agg_types'; + +/** + * Helper function returning the used interval for data table column created by the histogramm agg type. + * "auto" will get expanded to the actually used interval. + * If the column is not a column created by a histogram aggregation of the esaggs data source, + * this function will return undefined. + */ +export const getNumberHistogramIntervalByDatatableColumn = (column: DatatableColumn) => { + if (column.meta.source !== 'esaggs') return; + if (column.meta.sourceParams?.type !== BUCKET_TYPES.HISTOGRAM) return; + const params = (column.meta.sourceParams.params as unknown) as AggParamsHistogram; + + if (!params.used_interval || typeof params.used_interval === 'string') { + return undefined; + } + return params.used_interval; +}; diff --git a/src/plugins/data/common/search/aggs/utils/index.ts b/src/plugins/data/common/search/aggs/utils/index.ts index 40451a0f66e0c9..f90e8f88546f46 100644 --- a/src/plugins/data/common/search/aggs/utils/index.ts +++ b/src/plugins/data/common/search/aggs/utils/index.ts @@ -7,6 +7,7 @@ */ export * from './calculate_auto_time_expression'; +export { getNumberHistogramIntervalByDatatableColumn } from './get_number_histogram_interval'; export * from './date_interval_utils'; export * from './get_format_with_aggs'; export * from './ipv4_address'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index df799ede08a310..00bf0385487d81 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -308,6 +308,7 @@ import { parseInterval, toAbsoluteDates, boundsDescendingRaw, + getNumberHistogramIntervalByDatatableColumn, // expressions utils getRequestInspectorStats, getResponseInspectorStats, @@ -417,6 +418,7 @@ export const search = { termsAggFilter, toAbsoluteDates, boundsDescendingRaw, + getNumberHistogramIntervalByDatatableColumn, }, getRequestInspectorStats, getResponseInspectorStats, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 67423295dfe5e3..36e34479ad2d1f 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -2238,6 +2238,7 @@ export const search: { boundLabel: string; intervalLabel: string; })[]; + getNumberHistogramIntervalByDatatableColumn: (column: import("../../expressions").DatatableColumn) => number | undefined; }; getRequestInspectorStats: typeof getRequestInspectorStats; getResponseInspectorStats: typeof getResponseInspectorStats; @@ -2649,21 +2650,21 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:425:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/search/session/session_service.ts:42:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap index a2047b7bae669b..9a32f1c3311522 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap @@ -26,6 +26,13 @@ exports[`xy_expression XYChart component it renders area 1`] = ` "headerFormatter": [Function], } } + xDomain={ + Object { + "max": undefined, + "min": undefined, + "minInterval": 50, + } + } /> { }} /> ); - expect(component.find(Settings).prop('xDomain')).toBeUndefined(); + const xDomain = component.find(Settings).prop('xDomain'); + expect(xDomain).toEqual( + expect.objectContaining({ + min: undefined, + max: undefined, + }) + ); + }); + + test('it uses min interval if passed in', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + + ); + expect(component.find(Settings).prop('xDomain')).toEqual({ minInterval: 101 }); }); test('it renders bar', () => { @@ -1881,6 +1904,24 @@ describe('xy_expression', () => { expect(result).toEqual(5 * 60 * 1000); }); + it('should return interval of number histogram if available on first x axis columns', async () => { + xyProps.args.layers[0].xScaleType = 'linear'; + xyProps.data.tables.first.columns[2].meta = { + source: 'esaggs', + type: 'number', + field: 'someField', + sourceParams: { + type: 'histogram', + params: { + interval: 'auto', + used_interval: 5, + }, + }, + }; + const result = await calculateMinInterval(xyProps, jest.fn().mockResolvedValue(undefined)); + expect(result).toEqual(5); + }); + it('should return undefined if data table is empty', async () => { xyProps.data.tables.first.rows = []; const result = await calculateMinInterval( diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 7f6414b40cb90e..eda08715b394e9 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -199,13 +199,20 @@ export async function calculateMinInterval( const filteredLayers = getFilteredLayers(layers, data); if (filteredLayers.length === 0) return; const isTimeViz = data.dateRange && filteredLayers.every((l) => l.xScaleType === 'time'); - - if (!isTimeViz) return; - const dateColumn = data.tables[filteredLayers[0].layerId].columns.find( + const xColumn = data.tables[filteredLayers[0].layerId].columns.find( (column) => column.id === filteredLayers[0].xAccessor ); - if (!dateColumn) return; - const dateMetaData = await getIntervalByColumn(dateColumn); + + if (!xColumn) return; + if (!isTimeViz) { + const histogramInterval = search.aggs.getNumberHistogramIntervalByDatatableColumn(xColumn); + if (typeof histogramInterval === 'number') { + return histogramInterval; + } else { + return undefined; + } + } + const dateMetaData = await getIntervalByColumn(xColumn); if (!dateMetaData) return; const intervalDuration = search.aggs.parseInterval(dateMetaData.interval); if (!intervalDuration) return; @@ -381,13 +388,11 @@ export function XYChart({ const isTimeViz = data.dateRange && filteredLayers.every((l) => l.xScaleType === 'time'); const isHistogramViz = filteredLayers.every((l) => l.isHistogram); - const xDomain = isTimeViz - ? { - min: data.dateRange?.fromDate.getTime(), - max: data.dateRange?.toDate.getTime(), - minInterval, - } - : undefined; + const xDomain = { + min: isTimeViz ? data.dateRange?.fromDate.getTime() : undefined, + max: isTimeViz ? data.dateRange?.toDate.getTime() : undefined, + minInterval, + }; const getYAxesTitles = ( axisSeries: Array<{ layer: string; accessor: string }>, From 5bfcc096a6b6f24fd42eea321636181efbb4fe64 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Fri, 19 Feb 2021 07:40:16 -0500 Subject: [PATCH 26/43] [Fleet] Don't error on missing package_assets value (#91744) ## Summary closes https://github.com/elastic/kibana/issues/89111 * Update TS type to make `package_assets` key in EPM packages saved object optional * Update two places in code to deal with optional vs required property ### Checklist - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios #### Manual testing 1. checkout `7.10` branch 1. **start ES:** `nvm use; yarn kbn bootstrap; yarn es snapshot --version 7.10.3 --license=trial -E xpack.security.authc.api_key.enabled=true -E path.data=../data` 1. **start Kibana**: `yarn start --no-base-path` 1. **run** `curl -X POST -H 'kbn-xsrf: 1234' --user elastic:changeme localhost:5601/api/fleet/setup` 2. **observe** `{"is_initialized: true}` 1. checkout `7.11` branch 1. **start ES:** `nvm use; yarn kbn bootstrap; yarn es snapshot --version 7.11.1 --license=trial -E xpack.security.authc.api_key.enabled=true -E path.data=../data` 1. **start Kibana**: `yarn start --no-base-path` 1. **run** `curl -X POST -H 'kbn-xsrf: 1234' --user elastic:changeme localhost:5601/api/fleet/setup` 1. **observe** `{"is_initialized: true}` 1. checkout `master` branch 1. **start ES:** `nvm use; yarn kbn bootstrap; yarn es snapshot --version 8.0.0 --license=trial -E xpack.security.authc.api_key.enabled=true -E path.data=../data` 1. **start Kibana**: `yarn start --no-base-path` 1. **run** `curl -X POST -H 'kbn-xsrf: 1234' --user elastic:changeme localhost:5601/api/fleet/setup` 1. **observe error** {"statusCode":500,"error":"Internal Server Error","message":"Cannot read property 'map' of undefined"} 1. checkout this PR `8911-fleet-startup-error` 1. **start ES:** `nvm use; yarn kbn bootstrap; yarn es snapshot --version 8.0.0 --license=trial -E xpack.security.authc.api_key.enabled=true -E path.data=../data` 1. **start Kibana**: `yarn start --no-base-path` 1. **run** `curl -X POST -H 'kbn-xsrf: 1234' --user elastic:changeme localhost:5601/api/fleet/setup` 1. **observe success** `{"is_initialized: true}` **_Notes_** * _you might need to do a `yarn kbn clean` when starting kibana if it fails. There have been some big changes in the tooling recently_ --- x-pack/plugins/fleet/common/types/models/epm.ts | 2 +- .../plugins/fleet/server/services/epm/archive/storage.ts | 3 ++- x-pack/plugins/fleet/server/services/epm/packages/get.ts | 8 ++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index e7e5a931b7429f..5c99831eaac34a 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -273,7 +273,7 @@ export type PackageInfo = export interface Installation extends SavedObjectAttributes { installed_kibana: KibanaAssetReference[]; installed_es: EsAssetReference[]; - package_assets: PackageAssetReference[]; + package_assets?: PackageAssetReference[]; es_index_patterns: Record; name: string; version: string; diff --git a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts index 4144146896628f..20e1e8825fbd8f 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts @@ -83,9 +83,10 @@ export async function archiveEntryToESDocument(opts: { export async function removeArchiveEntries(opts: { savedObjectsClient: SavedObjectsClientContract; - refs: PackageAssetReference[]; + refs?: PackageAssetReference[]; }) { const { savedObjectsClient, refs } = opts; + if (!refs) return; const results = await Promise.all( refs.map((ref) => savedObjectsClient.delete(ASSETS_SAVED_OBJECT_TYPE, ref.id)) ); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index 0fac68426b73e0..c07b88a45e6dc9 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -16,6 +16,7 @@ import { import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { ArchivePackage, RegistryPackage, EpmPackageAdditions } from '../../../../common/types'; import { Installation, PackageInfo, KibanaAssetType } from '../../../types'; +import { IngestManagerError } from '../../../errors'; import * as Registry from '../registry'; import { createInstallableFrom, isRequiredPackage } from './index'; import { getEsPackage } from '../archive/storage'; @@ -185,7 +186,8 @@ export async function getPackageFromSource(options: { name: pkgName, version: pkgVersion, }); - if (!res) { + + if (!res && installedPkg.package_assets) { res = await getEsPackage( pkgName, pkgVersion, @@ -207,7 +209,9 @@ export async function getPackageFromSource(options: { // else package is not installed or installed and missing from cache and storage and installed from registry res = await Registry.getRegistryPackage(pkgName, pkgVersion); } - if (!res) throw new Error(`package info for ${pkgName}-${pkgVersion} does not exist`); + if (!res) { + throw new IngestManagerError(`package info for ${pkgName}-${pkgVersion} does not exist`); + } return { paths: res.paths, packageInfo: res.packageInfo, From f8fd08fbcd3b75f7b34c13eb1b954523fe2a2abe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Fri, 19 Feb 2021 14:34:52 +0100 Subject: [PATCH 27/43] Refactored component edit policy tests into separate folders and using client integration testing setup (#91657) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/policy_table.test.tsx.snap | 0 .../edit_policy/constants.ts | 31 + .../edit_policy/edit_policy.helpers.tsx | 183 ++-- .../edit_policy/edit_policy.test.ts | 22 +- .../cold_phase_validation.test.ts | 125 +++ .../delete_phase_validation.ts | 81 ++ .../hot_phase_validation.test.ts | 174 ++++ .../policy_name_validation.test.ts | 100 ++ .../warm_phase_validation.test.ts | 171 ++++ .../reactive_form/node_allocation.test.ts | 382 +++++++ .../reactive_form/reactive_form.test.ts | 143 +++ .../helpers/http_requests.ts | 15 +- .../__jest__/components/README.md | 8 - .../__jest__/components/edit_policy.test.tsx | 967 ------------------ .../components/helpers/edit_policy.ts | 31 - .../components/helpers/http_requests.ts | 60 -- .../__jest__/components/helpers/index.ts | 12 - .../{components => }/policy_table.test.tsx | 14 +- .../common/types/api.ts | 10 + 19 files changed, 1370 insertions(+), 1159 deletions(-) rename x-pack/plugins/index_lifecycle_management/__jest__/{components => }/__snapshots__/policy_table.test.tsx.snap (100%) create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/components/README.md delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts rename x-pack/plugins/index_lifecycle_management/__jest__/{components => }/policy_table.test.tsx (92%) diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap rename to x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts index a63203656dc46d..2c8fbfc749a82d 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import moment from 'moment-timezone'; + import { PolicyFromES } from '../../../common/types'; export const POLICY_NAME = 'my_policy'; @@ -234,3 +236,32 @@ export const POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS = ({ }, name: POLICY_NAME, } as any) as PolicyFromES; + +export const getGeneratedPolicies = (): PolicyFromES[] => { + const policy = { + phases: { + hot: { + min_age: '0s', + actions: { + rollover: { + max_size: '1gb', + }, + }, + }, + }, + }; + const policies: PolicyFromES[] = []; + for (let i = 0; i < 105; i++) { + policies.push({ + version: i, + modified_date: moment().subtract(i, 'days').toISOString(), + linkedIndices: i % 2 === 0 ? [`index${i}`] : undefined, + name: `testy${i}`, + policy: { + ...policy, + name: `testy${i}`, + }, + }); + } + return policies; +}; 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 83a13f0523a403..a9845c23156049 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 @@ -21,10 +21,12 @@ import { KibanaContextProvider } from '../../../public/shared_imports'; import { AppServicesContext } from '../../../public/types'; import { createBreadcrumbsMock } from '../../../public/application/services/breadcrumbs.mock'; +import { TestSubjects } from '../helpers'; +import { POLICY_NAME } from './constants'; + type Phases = keyof PolicyPhases; -import { POLICY_NAME } from './constants'; -import { TestSubjects } from '../helpers'; +window.scrollTo = jest.fn(); jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -46,14 +48,17 @@ jest.mock('@elastic/eui', () => { }; }); -const testBedConfig: TestBedConfig = { - memoryRouter: { - initialEntries: [`/policies/edit/${POLICY_NAME}`], - componentRoutePath: `/policies/edit/:policyName`, - }, - defaultProps: { - getUrlForApp: () => {}, - }, +const getTestBedConfig = (testBedConfigArgs?: Partial): TestBedConfig => { + return { + memoryRouter: { + initialEntries: [`/policies/edit/${POLICY_NAME}`], + componentRoutePath: `/policies/edit/:policyName`, + }, + defaultProps: { + getUrlForApp: () => {}, + }, + ...testBedConfigArgs, + }; }; const breadcrumbService = createBreadcrumbsMock(); @@ -72,13 +77,22 @@ const MyComponent = ({ appServicesContext, ...rest }: any) => { ); }; -const initTestBed = registerTestBed(MyComponent, testBedConfig); +const initTestBed = (arg?: { + appServicesContext?: Partial; + testBedConfig?: Partial; +}) => { + const { testBedConfig: testBedConfigArgs, ...rest } = arg || {}; + return registerTestBed(MyComponent, getTestBedConfig(testBedConfigArgs))(rest); +}; type SetupReturn = ReturnType; export type EditPolicyTestBed = SetupReturn extends Promise ? U : SetupReturn; -export const setup = async (arg?: { appServicesContext: Partial }) => { +export const setup = async (arg?: { + appServicesContext?: Partial; + testBedConfig?: Partial; +}) => { const testBed = await initTestBed(arg); const { find, component, form, exists } = testBed; @@ -169,34 +183,15 @@ export const setup = async (arg?: { appServicesContext: Partial createFormToggleAction(`enablePhaseSwitch-${phase}`); - const setMinAgeValue = (phase: Phases) => createFormSetValueAction(`${phase}-selectedMinimumAge`); - - const setMinAgeUnits = (phase: Phases) => - createFormSetValueAction(`${phase}-selectedMinimumAgeUnits`); - - const setDataAllocation = (phase: Phases) => async (value: DataTierAllocationType) => { - act(() => { - find(`${phase}-dataTierAllocationControls.dataTierSelect`).simulate('click'); - }); - component.update(); - await act(async () => { - switch (value) { - case 'node_roles': - find(`${phase}-dataTierAllocationControls.defaultDataAllocationOption`).simulate('click'); - break; - case 'node_attrs': - find(`${phase}-dataTierAllocationControls.customDataAllocationOption`).simulate('click'); - break; - default: - find(`${phase}-dataTierAllocationControls.noneDataAllocationOption`).simulate('click'); - } - }); - component.update(); + const createMinAgeActions = (phase: Phases) => { + return { + hasMinAgeInput: () => exists(`${phase}-selectedMinimumAge`), + setMinAgeValue: createFormSetValueAction(`${phase}-selectedMinimumAge`), + setMinAgeUnits: createFormSetValueAction(`${phase}-selectedMinimumAgeUnits`), + hasRolloverTipOnMinAge: () => exists(`${phase}-rolloverMinAgeInputIconTip`), + }; }; - const setSelectedNodeAttribute = (phase: Phases) => - createFormSetValueAction(`${phase}-selectedNodeAttrs`); - const setReplicas = (phase: Phases) => async (value: string) => { if (!exists(`${phase}-selectedReplicaCount`)) { await createFormToggleAction(`${phase}-setReplicasSwitch`)(true); @@ -216,8 +211,12 @@ export const setup = async (arg?: { appServicesContext: Partial exists('freezeSwitch'); - const setReadonly = (phase: Phases) => async (value: boolean) => { - await createFormToggleAction(`${phase}-readonlySwitch`)(value); + const createReadonlyActions = (phase: Phases) => { + const toggleSelector = `${phase}-readonlySwitch`; + return { + readonlyExists: () => exists(toggleSelector), + toggleReadonly: createFormToggleAction(toggleSelector), + }; }; const createSearchableSnapshotActions = (phase: Phases) => { @@ -271,17 +270,93 @@ export const setup = async (arg?: { appServicesContext: Partial (): boolean => - exists(`${phase}-rolloverMinAgeInputIconTip`); + const hasRolloverSettingRequiredCallout = (): boolean => exists('rolloverSettingsRequired'); + + const createNodeAllocationActions = (phase: Phases) => { + const controlsSelector = `${phase}-dataTierAllocationControls`; + const dataTierSelector = `${controlsSelector}.dataTierSelect`; + const nodeAttrsSelector = `${phase}-selectedNodeAttrs`; + + return { + hasDataTierAllocationControls: () => exists(controlsSelector), + openNodeAttributesSection: async () => { + await act(async () => { + find(dataTierSelector).simulate('click'); + }); + component.update(); + }, + hasNodeAttributesSelect: (): boolean => exists(nodeAttrsSelector), + getNodeAttributesSelectOptions: () => find(nodeAttrsSelector).find('option'), + setDataAllocation: async (value: DataTierAllocationType) => { + act(() => { + find(dataTierSelector).simulate('click'); + }); + component.update(); + await act(async () => { + switch (value) { + case 'node_roles': + find(`${controlsSelector}.defaultDataAllocationOption`).simulate('click'); + break; + case 'node_attrs': + find(`${controlsSelector}.customDataAllocationOption`).simulate('click'); + break; + default: + find(`${controlsSelector}.noneDataAllocationOption`).simulate('click'); + } + }); + component.update(); + }, + setSelectedNodeAttribute: createFormSetValueAction(nodeAttrsSelector), + hasNoNodeAttrsWarning: () => exists('noNodeAttributesWarning'), + hasDefaultAllocationWarning: () => exists('defaultAllocationWarning'), + hasDefaultAllocationNotice: () => exists('defaultAllocationNotice'), + hasNodeDetailsFlyout: () => exists(`${phase}-viewNodeDetailsFlyoutButton`), + openNodeDetailsFlyout: async () => { + await act(async () => { + find(`${phase}-viewNodeDetailsFlyoutButton`).simulate('click'); + }); + component.update(); + }, + }; + }; + + const expectErrorMessages = (expectedMessages: string[]) => { + const errorMessages = component.find('.euiFormErrorText'); + expect(errorMessages.length).toBe(expectedMessages.length); + expectedMessages.forEach((expectedErrorMessage) => { + let foundErrorMessage; + for (let i = 0; i < errorMessages.length; i++) { + if (errorMessages.at(i).text() === expectedErrorMessage) { + foundErrorMessage = true; + } + } + expect(foundErrorMessage).toBe(true); + }); + }; + + /* + * For new we rely on a setTimeout to ensure that error messages have time to populate + * the form object before we look at the form object. See: + * x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx + * for where this logic lives. + */ + const runTimers = () => { + act(() => { + jest.runAllTimers(); + }); + component.update(); + }; return { ...testBed, + runTimers, actions: { saveAsNewPolicy: createFormToggleAction('saveAsNewSwitch'), setPolicyName: createFormSetValueAction('policyNameField'), setWaitForSnapshotPolicy, savePolicy, hasGlobalErrorCallout: () => exists('policyFormErrorsCallout'), + expectErrorMessages, timeline: { hasHotPhase: () => exists('ilmTimelineHotPhase'), hasWarmPhase: () => exists('ilmTimelineWarmPhase'), @@ -294,46 +369,40 @@ export const setup = async (arg?: { appServicesContext: Partial exists('phaseErrorIndicator-hot'), ...createForceMergeActions('hot'), ...createIndexPriorityActions('hot'), ...createShrinkActions('hot'), - setReadonly: setReadonly('hot'), + ...createReadonlyActions('hot'), ...createSearchableSnapshotActions('hot'), }, warm: { enable: enable('warm'), - setMinAgeValue: setMinAgeValue('warm'), - setMinAgeUnits: setMinAgeUnits('warm'), - setDataAllocation: setDataAllocation('warm'), - setSelectedNodeAttribute: setSelectedNodeAttribute('warm'), + ...createMinAgeActions('warm'), setReplicas: setReplicas('warm'), hasErrorIndicator: () => exists('phaseErrorIndicator-warm'), - hasRolloverTipOnMinAge: hasRolloverTipOnMinAge('warm'), ...createShrinkActions('warm'), ...createForceMergeActions('warm'), - setReadonly: setReadonly('warm'), + ...createReadonlyActions('warm'), ...createIndexPriorityActions('warm'), + ...createNodeAllocationActions('warm'), }, cold: { enable: enable('cold'), - setMinAgeValue: setMinAgeValue('cold'), - setMinAgeUnits: setMinAgeUnits('cold'), - setDataAllocation: setDataAllocation('cold'), - setSelectedNodeAttribute: setSelectedNodeAttribute('cold'), + ...createMinAgeActions('cold'), setReplicas: setReplicas('cold'), setFreeze, freezeExists, hasErrorIndicator: () => exists('phaseErrorIndicator-cold'), - hasRolloverTipOnMinAge: hasRolloverTipOnMinAge('cold'), ...createIndexPriorityActions('cold'), ...createSearchableSnapshotActions('cold'), + ...createNodeAllocationActions('cold'), }, delete: { + isShown: () => exists('delete-phaseContent'), ...createToggleDeletePhaseActions(), - hasRolloverTipOnMinAge: hasRolloverTipOnMinAge('delete'), - setMinAgeValue: setMinAgeValue('delete'), - setMinAgeUnits: setMinAgeUnits('delete'), + ...createMinAgeActions('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 859b4adce50285..7fe5c6f50d046b 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 @@ -25,8 +25,6 @@ import { getDefaultHotPhasePolicy, } from './constants'; -window.scrollTo = jest.fn(); - describe('', () => { let testBed: EditPolicyTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); @@ -127,7 +125,7 @@ describe('', () => { await actions.hot.setBestCompression(true); await actions.hot.toggleShrink(true); await actions.hot.setShrink('2'); - await actions.hot.setReadonly(true); + await actions.hot.toggleReadonly(true); await actions.hot.toggleIndexPriority(true); await actions.hot.setIndexPriority('123'); @@ -271,7 +269,7 @@ describe('', () => { await actions.warm.toggleForceMerge(true); await actions.warm.setForcemergeSegmentsCount('123'); await actions.warm.setBestCompression(true); - await actions.warm.setReadonly(true); + await actions.warm.toggleReadonly(true); await actions.warm.setIndexPriority('123'); await actions.savePolicy(); const latestRequest = server.requests[server.requests.length - 1]; @@ -918,6 +916,7 @@ describe('', () => { }); describe('policy error notifications', () => { + let runTimers: () => void; beforeAll(() => { jest.useFakeTimers(); }); @@ -925,6 +924,7 @@ describe('', () => { afterAll(() => { jest.useRealTimers(); }); + beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); httpRequestsMockHelpers.setListNodes({ @@ -940,19 +940,9 @@ describe('', () => { const { component } = testBed; component.update(); - }); - // For new we rely on a setTimeout to ensure that error messages have time to populate - // the form object before we look at the form object. See: - // x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx - // for where this logic lives. - const runTimers = () => { - const { component } = testBed; - act(() => { - jest.runAllTimers(); - }); - component.update(); - }; + ({ runTimers } = testBed); + }); test('shows phase error indicators correctly', async () => { // This test simulates a user configuring a policy phase by phase. The flow is the following: diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts new file mode 100644 index 00000000000000..c5c4bb1be87e0e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' cold phase validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('mypolicy'); + await actions.cold.enable(true); + + ({ runTimers } = testBed); + }); + + describe('timing', () => { + test(`doesn't allow empty timing`, async () => { + const { actions } = testBed; + + await actions.cold.setMinAgeValue(''); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for phase timing`, async () => { + const { actions } = testBed; + + await actions.cold.setMinAgeValue('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + + test(`doesn't allow -1 for timing`, async () => { + const { actions } = testBed; + + await actions.cold.setMinAgeValue('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + }); + + describe('replicas', () => { + test(`doesn't allow -1 for replicas`, async () => { + const { actions } = testBed; + + await actions.cold.setReplicas('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for replicas`, async () => { + const { actions } = testBed; + + await actions.cold.setReplicas('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + }); + + describe('index priority', () => { + test(`doesn't allow -1 for index priority`, async () => { + const { actions } = testBed; + + await actions.cold.setIndexPriority('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for index priority`, async () => { + const { actions } = testBed; + + await actions.cold.setIndexPriority('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts new file mode 100644 index 00000000000000..a13aaa02dcd068 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' delete phase validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('mypolicy'); + await actions.delete.enablePhase(); + + ({ runTimers } = testBed); + }); + + describe('timing', () => { + test(`doesn't allow empty timing`, async () => { + const { actions } = testBed; + + await actions.delete.setMinAgeValue(''); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for phase timing`, async () => { + const { actions } = testBed; + + await actions.delete.setMinAgeValue('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + + test(`doesn't allow -1 for timing`, async () => { + const { actions } = testBed; + + await actions.delete.setMinAgeValue('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts new file mode 100644 index 00000000000000..7c1d687b27e3d1 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' hot phase validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('mypolicy'); + + ({ runTimers } = testBed); + }); + + describe('rollover', () => { + test(`doesn't allow no max size, no max age and no max docs`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + expect(actions.hot.hasRolloverSettingRequiredCallout()).toBeFalsy(); + + await actions.hot.setMaxSize(''); + await actions.hot.setMaxAge(''); + await actions.hot.setMaxDocs(''); + + runTimers(); + + expect(actions.hot.hasRolloverSettingRequiredCallout()).toBeTruthy(); + }); + + test(`doesn't allow -1 for max size`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxSize('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow 0 for max size`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxSize('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow -1 for max age`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxAge('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow 0 for max age`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxAge('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow -1 for max docs`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxDocs('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow 0 for max docs`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxDocs('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('forcemerge', () => { + test(`doesn't allow 0 for forcemerge`, async () => { + const { actions } = testBed; + await actions.hot.toggleForceMerge(true); + await actions.hot.setForcemergeSegmentsCount('0'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow -1 for forcemerge`, async () => { + const { actions } = testBed; + await actions.hot.toggleForceMerge(true); + await actions.hot.setForcemergeSegmentsCount('-1'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('shrink', () => { + test(`doesn't allow 0 for shrink`, async () => { + const { actions } = testBed; + await actions.hot.toggleShrink(true); + await actions.hot.setShrink('0'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow -1 for shrink`, async () => { + const { actions } = testBed; + await actions.hot.toggleShrink(true); + await actions.hot.setShrink('-1'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('index priority', () => { + test(`doesn't allow -1 for index priority`, async () => { + const { actions } = testBed; + + await actions.hot.setIndexPriority('-1'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for index priority`, async () => { + const { actions } = testBed; + + await actions.hot.setIndexPriority('0'); + runTimers(); + actions.expectErrorMessages([]); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts new file mode 100644 index 00000000000000..0acb425b1d9758 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { getGeneratedPolicies } from '../constants'; + +describe(' policy name validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies(getGeneratedPolicies()); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + + ({ runTimers } = testBed); + }); + + test(`doesn't allow empty policy name`, async () => { + const { actions } = testBed; + await actions.savePolicy(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.policyNameRequiredMessage]); + }); + + test(`doesn't allow policy name with space`, async () => { + const { actions } = testBed; + await actions.setPolicyName('my policy'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]); + }); + + test(`doesn't allow policy name that is already used`, async () => { + const { actions } = testBed; + await actions.setPolicyName('testy0'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage]); + }); + + test(`doesn't allow to save as new policy but using the same name`, async () => { + await act(async () => { + testBed = await setup({ + testBedConfig: { + memoryRouter: { + initialEntries: [`/policies/edit/testy0`], + componentRoutePath: `/policies/edit/:policyName`, + }, + }, + }); + }); + const { component, actions } = testBed; + component.update(); + + ({ runTimers } = testBed); + + await actions.saveAsNewPolicy(true); + runTimers(); + await actions.savePolicy(); + actions.expectErrorMessages([ + i18nTexts.editPolicy.errors.policyNameMustBeDifferentErrorMessage, + ]); + }); + + test(`doesn't allow policy name with comma`, async () => { + const { actions } = testBed; + await actions.setPolicyName('my,policy'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]); + }); + + test(`doesn't allow policy name starting with underscore`, async () => { + const { actions } = testBed; + await actions.setPolicyName('_mypolicy'); + runTimers(); + actions.expectErrorMessages([ + i18nTexts.editPolicy.errors.policyNameStartsWithUnderscoreErrorMessage, + ]); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts new file mode 100644 index 00000000000000..2121dba8e06f63 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' warm phase validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('mypolicy'); + await actions.warm.enable(true); + + ({ runTimers } = testBed); + }); + + describe('timing', () => { + test(`doesn't allow empty timing`, async () => { + const { actions } = testBed; + + await actions.warm.setMinAgeValue(''); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for phase timing`, async () => { + const { actions } = testBed; + + await actions.warm.setMinAgeValue('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + + test(`doesn't allow -1 for timing`, async () => { + const { actions } = testBed; + + await actions.warm.setMinAgeValue('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + }); + + describe('replicas', () => { + test(`doesn't allow -1 for replicas`, async () => { + const { actions } = testBed; + + await actions.warm.setReplicas('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for replicas`, async () => { + const { actions } = testBed; + + await actions.warm.setReplicas('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + }); + + describe('shrink', () => { + test(`doesn't allow 0 for shrink`, async () => { + const { actions } = testBed; + + await actions.warm.toggleShrink(true); + await actions.warm.setShrink('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow -1 for shrink`, async () => { + const { actions } = testBed; + + await actions.warm.toggleShrink(true); + await actions.warm.setShrink('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('forcemerge', () => { + test(`doesn't allow 0 for forcemerge`, async () => { + const { actions } = testBed; + + await actions.warm.toggleForceMerge(true); + await actions.warm.setForcemergeSegmentsCount('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow -1 for forcemerge`, async () => { + const { actions } = testBed; + + await actions.warm.toggleForceMerge(true); + await actions.warm.setForcemergeSegmentsCount('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('index priority', () => { + test(`doesn't allow -1 for index priority`, async () => { + const { actions } = testBed; + + await actions.warm.setIndexPriority('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for index priority`, async () => { + const { actions } = testBed; + + await actions.warm.setIndexPriority('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts new file mode 100644 index 00000000000000..113698fdf6df2b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts @@ -0,0 +1,382 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' node allocation', () => { + let testBed: EditPolicyTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + server.respondImmediately = true; + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + describe('warm phase', () => { + test('shows spinner for node attributes input when loading', async () => { + server.respondImmediately = false; + + const { actions, component } = testBed; + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeTruthy(); + expect(actions.warm.hasDataTierAllocationControls()).toBeTruthy(); + + expect(component.find('.euiCallOut--warning').exists()).toBeFalsy(); + expect(actions.warm.hasNodeAttributesSelect()).toBeFalsy(); + }); + + test('shows warning instead of node attributes input when none exist', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + await actions.warm.setDataAllocation('node_attrs'); + expect(actions.warm.hasNoNodeAttrsWarning()).toBeTruthy(); + expect(actions.warm.hasNodeAttributesSelect()).toBeFalsy(); + }); + + test('shows node attributes input when attributes exist', async () => { + const { actions, component } = testBed; + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.warm.setDataAllocation('node_attrs'); + expect(actions.warm.hasNoNodeAttrsWarning()).toBeFalsy(); + expect(actions.warm.hasNodeAttributesSelect()).toBeTruthy(); + expect(actions.warm.getNodeAttributesSelectOptions().length).toBe(2); + }); + + test('shows view node attributes link when attribute selected and shows flyout when clicked', async () => { + const { actions, component } = testBed; + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.warm.setDataAllocation('node_attrs'); + expect(actions.warm.hasNoNodeAttrsWarning()).toBeFalsy(); + expect(actions.warm.hasNodeAttributesSelect()).toBeTruthy(); + + expect(actions.warm.hasNodeDetailsFlyout()).toBeFalsy(); + expect(actions.warm.getNodeAttributesSelectOptions().length).toBe(2); + await actions.warm.setSelectedNodeAttribute('attribute:true'); + + await actions.warm.openNodeDetailsFlyout(); + expect(actions.warm.hasNodeDetailsFlyout()).toBeTruthy(); + }); + + test('shows default allocation warning when no node roles are found', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.warm.hasDefaultAllocationWarning()).toBeTruthy(); + }); + + test('shows default allocation notice when hot tier exists, but not warm tier', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data_hot: ['test'], data_cold: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.warm.hasDefaultAllocationNotice()).toBeTruthy(); + }); + + test(`doesn't show default allocation notice when node with "data" role exists`, async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.warm.hasDefaultAllocationNotice()).toBeFalsy(); + }); + }); + + describe('cold phase', () => { + test('shows spinner for node attributes input when loading', async () => { + server.respondImmediately = false; + + const { actions, component } = testBed; + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeTruthy(); + expect(actions.cold.hasDataTierAllocationControls()).toBeTruthy(); + + expect(component.find('.euiCallOut--warning').exists()).toBeFalsy(); + expect(actions.cold.hasNodeAttributesSelect()).toBeFalsy(); + }); + + test('shows warning instead of node attributes input when none exist', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.cold.setDataAllocation('node_attrs'); + expect(actions.cold.hasNoNodeAttrsWarning()).toBeTruthy(); + expect(actions.cold.hasNodeAttributesSelect()).toBeFalsy(); + }); + + test('shows node attributes input when attributes exist', async () => { + const { actions, component } = testBed; + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.cold.setDataAllocation('node_attrs'); + expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); + expect(actions.cold.hasNodeAttributesSelect()).toBeTruthy(); + expect(actions.cold.getNodeAttributesSelectOptions().length).toBe(2); + }); + + test('shows view node attributes link when attribute selected and shows flyout when clicked', async () => { + const { actions, component } = testBed; + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.cold.setDataAllocation('node_attrs'); + expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); + expect(actions.cold.hasNodeAttributesSelect()).toBeTruthy(); + + expect(actions.cold.hasNodeDetailsFlyout()).toBeFalsy(); + expect(actions.cold.getNodeAttributesSelectOptions().length).toBe(2); + await actions.cold.setSelectedNodeAttribute('attribute:true'); + + await actions.cold.openNodeDetailsFlyout(); + expect(actions.cold.hasNodeDetailsFlyout()).toBeTruthy(); + }); + + test('shows default allocation warning when no node roles are found', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.cold.hasDefaultAllocationWarning()).toBeTruthy(); + }); + + test('shows default allocation notice when warm or hot tiers exists, but not cold tier', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.cold.hasDefaultAllocationNotice()).toBeTruthy(); + }); + + test(`doesn't show default allocation notice when node with "data" role exists`, async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.cold.hasDefaultAllocationNotice()).toBeFalsy(); + }); + }); + + describe('not on cloud', () => { + test('shows all allocation options, even if using legacy config', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + await act(async () => { + testBed = await setup(); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.warm.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + // Assert that default, custom and 'none' options exist + await actions.warm.openNodeAttributesSection(); + expect(exists('defaultDataAllocationOption')).toBeTruthy(); + expect(exists('customDataAllocationOption')).toBeTruthy(); + expect(exists('noneDataAllocationOption')).toBeTruthy(); + }); + }); + + describe('on cloud', () => { + describe('with deprecated data role config', () => { + test('should hide data tier option on cloud using legacy node role configuration', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + // On cloud, if using legacy config there will not be any "data_*" roles set. + nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.warm.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + // Assert that custom and 'none' options exist + await actions.warm.openNodeAttributesSection(); + expect(exists('defaultDataAllocationOption')).toBeFalsy(); + expect(exists('customDataAllocationOption')).toBeTruthy(); + expect(exists('noneDataAllocationOption')).toBeTruthy(); + }); + }); + + describe('with node role config', () => { + test('shows off, custom and data role options on cloud with data roles', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.warm.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + await actions.warm.openNodeAttributesSection(); + expect(exists('defaultDataAllocationOption')).toBeTruthy(); + expect(exists('customDataAllocationOption')).toBeTruthy(); + expect(exists('noneDataAllocationOption')).toBeTruthy(); + // We should not be showing the call-to-action for users to activate data tiers in cloud + expect(exists('cloudDataTierCallout')).toBeFalsy(); + }); + + test('shows cloud notice when cold tier nodes do not exist', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.cold.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + expect(exists('cloudDataTierCallout')).toBeTruthy(); + // Assert that other notices are not showing + expect(actions.cold.hasDefaultAllocationNotice()).toBeFalsy(); + expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); + }); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts new file mode 100644 index 00000000000000..9c23780f1d0213 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { DEFAULT_POLICY } from '../constants'; + +describe(' reactive form', () => { + let testBed: EditPolicyTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + describe('rollover', () => { + test('shows forcemerge when rollover enabled', async () => { + const { actions } = testBed; + expect(actions.hot.forceMergeFieldExists()).toBeTruthy(); + }); + test('hides forcemerge when rollover is disabled', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + expect(actions.hot.forceMergeFieldExists()).toBeFalsy(); + }); + + test('shows shrink input when rollover enabled', async () => { + const { actions } = testBed; + expect(actions.hot.shrinkExists()).toBeTruthy(); + }); + test('hides shrink input when rollover is disabled', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + expect(actions.hot.shrinkExists()).toBeFalsy(); + }); + test('shows readonly input when rollover enabled', async () => { + const { actions } = testBed; + expect(actions.hot.readonlyExists()).toBeTruthy(); + }); + test('hides readonly input when rollover is disabled', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + expect(actions.hot.readonlyExists()).toBeFalsy(); + }); + }); + + describe('timing', () => { + test('warm phase shows timing only when enabled', async () => { + const { actions } = testBed; + expect(actions.warm.hasMinAgeInput()).toBeFalsy(); + await actions.warm.enable(true); + expect(actions.warm.hasMinAgeInput()).toBeTruthy(); + }); + + test('cold phase shows timing only when enabled', async () => { + const { actions } = testBed; + expect(actions.cold.hasMinAgeInput()).toBeFalsy(); + await actions.cold.enable(true); + expect(actions.cold.hasMinAgeInput()).toBeTruthy(); + }); + + test('delete phase shows timing after it was enabled', async () => { + const { actions } = testBed; + expect(actions.delete.hasMinAgeInput()).toBeFalsy(); + await actions.delete.enablePhase(); + expect(actions.delete.hasMinAgeInput()).toBeTruthy(); + }); + }); + + describe('delete phase', () => { + test('is hidden when disabled', async () => { + const { actions } = testBed; + expect(actions.delete.isShown()).toBeFalsy(); + await actions.delete.enablePhase(); + expect(actions.delete.isShown()).toBeTruthy(); + }); + }); + + describe('json in flyout', () => { + test('renders a json in flyout for a default policy', async () => { + const { find, component } = testBed; + await act(async () => { + find('requestButton').simulate('click'); + }); + component.update(); + + const json = component.find(`code`).text(); + const expected = `PUT _ilm/policy/my_policy\n${JSON.stringify( + { + policy: { + phases: { + hot: { + min_age: '0ms', + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + }, + }, + }, + }, + }, + null, + 2 + )}`; + expect(json).toBe(expected); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts index 823138aad13b97..6ef2b4c231ce1a 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts @@ -7,7 +7,11 @@ import { fakeServer, SinonFakeServer } from 'sinon'; import { API_BASE_PATH } from '../../../common/constants'; -import { ListNodesRouteResponse, ListSnapshotReposResponse } from '../../../common/types'; +import { + ListNodesRouteResponse, + ListSnapshotReposResponse, + NodesDetailsResponse, +} from '../../../common/types'; export const init = () => { const server = fakeServer.create(); @@ -48,6 +52,14 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setNodesDetails = (nodeAttributes: string, body: NodesDetailsResponse) => { + server.respondWith('GET', `${API_BASE_PATH}/nodes/${nodeAttributes}/details`, [ + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + const setListSnapshotRepos = (body: ListSnapshotReposResponse) => { server.respondWith('GET', `${API_BASE_PATH}/snapshot_repositories`, [ 200, @@ -60,6 +72,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setLoadPolicies, setLoadSnapshotPolicies, setListNodes, + setNodesDetails, setListSnapshotRepos, }; }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/README.md b/x-pack/plugins/index_lifecycle_management/__jest__/components/README.md deleted file mode 100644 index ce1ea7aa396a62..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Deprecated - -This test folder contains useful test coverage, mostly error states for form validation. However, it is -not in keeping with other ES UI maintained plugins. See ../client_integration for the established pattern -of tests. - -The tests here should be migrated to the above pattern and should not be added to. Any new test coverage must -be added to ../client_integration. 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 deleted file mode 100644 index 7c199e2ced7651..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ /dev/null @@ -1,967 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { ReactElement } from 'react'; -import { act } from 'react-dom/test-utils'; -import moment from 'moment-timezone'; - -import { findTestSubject } from '@elastic/eui/lib/test'; -import { mountWithIntl } from '@kbn/test/jest'; -import { SinonFakeServer } from 'sinon'; -import { ReactWrapper } from 'enzyme'; -import axios from 'axios'; -import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import { createMemoryHistory } from 'history'; - -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 { 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 })); -initUiMetric(usageCollectionPluginMock.createSetupContract()); -initNotification( - notificationServiceMock.createSetupContract().toasts, - fatalErrorsServiceMock.createSetupContract() -); - -const history = createMemoryHistory(); -let server: SinonFakeServer; -let httpRequestsMockHelpers: editPolicyHelpers.EditPolicySetup['http']['httpRequestsMockHelpers']; -let http: editPolicyHelpers.EditPolicySetup['http']; -const policy = { - phases: { - hot: { - min_age: '0s', - actions: { - rollover: { - max_size: '1gb', - }, - }, - }, - }, -}; -const policies: PolicyFromES[] = []; -for (let i = 0; i < 105; i++) { - policies.push({ - version: i, - modified_date: moment().subtract(i, 'days').toISOString(), - linkedIndices: i % 2 === 0 ? [`index${i}`] : undefined, - name: `testy${i}`, - policy: { - ...policy, - name: `testy${i}`, - }, - }); -} -window.scrollTo = jest.fn(); - -jest.mock('@elastic/eui', () => { - const original = jest.requireActual('@elastic/eui'); - - return { - ...original, - EuiIcon: 'eui-icon', // using custom react-svg icon causes issues, mocking for now. - }; -}); - -let component: ReactElement; -const activatePhase = async (rendered: ReactWrapper, phase: string) => { - const testSubject = `enablePhaseSwitch-${phase}`; - await act(async () => { - await findTestSubject(rendered, testSubject).simulate('click'); - }); - rendered.update(); -}; -const activateDeletePhase = async (rendered: ReactWrapper) => { - const testSubject = `enableDeletePhaseButton`; - await act(async () => { - await findTestSubject(rendered, testSubject).simulate('click'); - }); - rendered.update(); -}; -const openNodeAttributesSection = async (rendered: ReactWrapper, phase: string) => { - const getControls = () => findTestSubject(rendered, `${phase}-dataTierAllocationControls`); - await act(async () => { - findTestSubject(getControls(), 'dataTierSelect').simulate('click'); - }); - rendered.update(); - await act(async () => { - findTestSubject(getControls(), 'customDataAllocationOption').simulate('click'); - }); - rendered.update(); -}; -const expectedErrorMessages = (rendered: ReactWrapper, expectedMessages: string[]) => { - const errorMessages = rendered.find('.euiFormErrorText'); - expect(errorMessages.length).toBe(expectedMessages.length); - expectedMessages.forEach((expectedErrorMessage) => { - let foundErrorMessage; - for (let i = 0; i < errorMessages.length; i++) { - if (errorMessages.at(i).text() === expectedErrorMessage) { - foundErrorMessage = true; - } - } - expect(foundErrorMessage).toBe(true); - }); -}; -const noDefaultRollover = async (rendered: ReactWrapper) => { - await act(async () => { - findTestSubject(rendered, 'useDefaultRolloverSwitch').simulate('click'); - }); - rendered.update(); -}; -const noRollover = async (rendered: ReactWrapper) => { - await noDefaultRollover(rendered); - await act(async () => { - findTestSubject(rendered, 'rolloverSwitch').simulate('click'); - }); - rendered.update(); -}; -const getNodeAttributeSelect = (rendered: ReactWrapper, phase: string) => { - return findTestSubject(rendered, `${phase}-selectedNodeAttrs`); -}; -const setPolicyName = async (rendered: ReactWrapper, policyName: string) => { - const policyNameField = findTestSubject(rendered, 'policyNameField'); - await act(async () => { - policyNameField.simulate('change', { target: { value: policyName } }); - }); - rendered.update(); -}; -const setPhaseAfter = async (rendered: ReactWrapper, phase: string, after: string | number) => { - const afterInput = findTestSubject(rendered, `${phase}-selectedMinimumAge`); - await act(async () => { - afterInput.simulate('change', { target: { value: after } }); - }); - rendered.update(); -}; -const setPhaseIndexPriority = async ( - rendered: ReactWrapper, - phase: string, - priority: string | number -) => { - const priorityInput = findTestSubject(rendered, `${phase}-indexPriority`); - await act(async () => { - priorityInput.simulate('change', { target: { value: priority } }); - }); - rendered.update(); -}; -const save = async (rendered: ReactWrapper) => { - const saveButton = findTestSubject(rendered, 'savePolicyButton'); - await act(async () => { - saveButton.simulate('click'); - }); - rendered.update(); -}; - -const MyComponent = ({ - isCloudEnabled, - isNewPolicy, - policy: _policy, - existingPolicies, - getUrlForApp, - policyName, -}: EditPolicyContextValue & { isCloudEnabled: boolean }) => { - return ( - - true, - }, - }} - > - - - - ); -}; - -describe('edit policy', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - afterAll(() => { - jest.useRealTimers(); - }); - - /** - * The form lib has a short delay (setTimeout) before running and rendering - * any validation errors. This helper advances timers and can trigger component - * state changes. - */ - const waitForFormLibValidation = (rendered: ReactWrapper) => { - act(() => { - jest.runAllTimers(); - }); - rendered.update(); - }; - - beforeEach(() => { - component = ( - true }} - /> - ); - - ({ http } = editPolicyHelpers.setup()); - ({ server, httpRequestsMockHelpers } = http); - - httpRequestsMockHelpers.setPoliciesResponse(policies); - }); - describe('top level form', () => { - test('should show error when trying to save empty form', async () => { - const rendered = mountWithIntl(component); - await save(rendered); - 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); - 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); - 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 = ( - true }} - /> - ); - const rendered = mountWithIntl(component); - findTestSubject(rendered, 'saveAsNewSwitch').simulate('click'); - rendered.update(); - 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); - 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); - await setPolicyName(rendered, '_mypolicy'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [ - i18nTexts.editPolicy.errors.policyNameStartsWithUnderscoreErrorMessage, - ]); - }); - test('should show correct json in policy flyout', async () => { - const rendered = mountWithIntl( - true }} - /> - ); - - await act(async () => { - findTestSubject(rendered, 'requestButton').simulate('click'); - }); - rendered.update(); - - const json = rendered.find(`code`).text(); - const expected = `PUT _ilm/policy/my-policy\n${JSON.stringify( - { - policy: { - phases: { - hot: { - actions: { - rollover: { - max_age: '30d', - max_size: '50gb', - }, - }, - min_age: '0ms', - }, - }, - }, - }, - null, - 2 - )}`; - expect(json).toBe(expected); - }); - }); - describe('hot phase', () => { - test('should show errors when trying to save with no max size, no max age and no max docs', async () => { - const rendered = mountWithIntl(component); - await noDefaultRollover(rendered); - expect(findTestSubject(rendered, 'rolloverSettingsRequired').exists()).toBeFalsy(); - await setPolicyName(rendered, 'mypolicy'); - const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored'); - await act(async () => { - maxSizeInput.simulate('change', { target: { value: '' } }); - }); - waitForFormLibValidation(rendered); - const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge'); - await act(async () => { - maxAgeInput.simulate('change', { target: { value: '' } }); - }); - waitForFormLibValidation(rendered); - const maxDocsInput = findTestSubject(rendered, 'hot-selectedMaxDocuments'); - await act(async () => { - maxDocsInput.simulate('change', { target: { value: '' } }); - }); - waitForFormLibValidation(rendered); - await save(rendered); - expect(findTestSubject(rendered, 'rolloverSettingsRequired').exists()).toBeTruthy(); - }); - test('should show number above 0 required error when trying to save with -1 for max size', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noDefaultRollover(rendered); - const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored'); - await act(async () => { - maxSizeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - rendered.update(); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show number above 0 required error when trying to save with 0 for max size', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noDefaultRollover(rendered); - const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored'); - await act(async () => { - maxSizeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show number above 0 required error when trying to save with -1 for max age', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noDefaultRollover(rendered); - const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge'); - await act(async () => { - maxAgeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show number above 0 required error when trying to save with 0 for max age', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noDefaultRollover(rendered); - const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge'); - await act(async () => { - maxAgeInput.simulate('change', { target: { value: '0' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show forcemerge input when rollover enabled', async () => { - const rendered = mountWithIntl(component); - 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); - 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); - await setPolicyName(rendered, 'mypolicy'); - act(() => { - findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click'); - }); - rendered.update(); - const forcemergeInput = findTestSubject(rendered, 'hot-selectedForceMergeSegments'); - await act(async () => { - forcemergeInput.simulate('change', { target: { value: '0' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - 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); - await setPolicyName(rendered, 'mypolicy'); - findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click'); - rendered.update(); - const forcemergeInput = findTestSubject(rendered, 'hot-selectedForceMergeSegments'); - await act(async () => { - forcemergeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - await save(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show positive number required error when trying to save with -1 for index priority', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - - await setPhaseIndexPriority(rendered, 'hot', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - - test("doesn't show min age input", async () => { - const rendered = mountWithIntl(component); - expect(findTestSubject(rendered, 'hot-selectedMinimumAge').exists()).toBeFalsy(); - }); - }); - describe('warm phase', () => { - beforeEach(() => { - server.respondImmediately = true; - http.setupNodeListResponse(); - httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [ - { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, - ]); - }); - - test('should show number required error when trying to save empty warm phase', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', ''); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - test('should allow 0 for phase timing', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '0'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, []); - }); - 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); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - 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); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '1'); - await setPhaseAfter(rendered, 'warm', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - 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); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - act(() => { - findTestSubject(rendered, 'warm-shrinkSwitch').simulate('click'); - }); - rendered.update(); - await setPhaseAfter(rendered, 'warm', '1'); - const shrinkInput = findTestSubject(rendered, 'warm-primaryShardCount'); - await act(async () => { - shrinkInput.simulate('change', { target: { value: '0' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - 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); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '1'); - act(() => { - findTestSubject(rendered, 'warm-shrinkSwitch').simulate('click'); - }); - rendered.update(); - const shrinkInput = findTestSubject(rendered, 'warm-primaryShardCount'); - await act(async () => { - shrinkInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - 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); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '1'); - act(() => { - findTestSubject(rendered, 'warm-forceMergeSwitch').simulate('click'); - }); - rendered.update(); - const forcemergeInput = findTestSubject(rendered, 'warm-selectedForceMergeSegments'); - await act(async () => { - forcemergeInput.simulate('change', { target: { value: '0' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - 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); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '1'); - await act(async () => { - findTestSubject(rendered, 'warm-forceMergeSwitch').simulate('click'); - }); - rendered.update(); - const forcemergeInput = findTestSubject(rendered, 'warm-selectedForceMergeSegments'); - await act(async () => { - forcemergeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show spinner for node attributes input when loading', async () => { - server.respondImmediately = false; - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'warm-dataTierAllocationControls').exists()).toBeTruthy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); - expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy(); - }); - test('should show warning instead of node attributes input when none exist', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['node1'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'warm'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy(); - expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy(); - }); - test('should show node attributes input when attributes exist', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'warm'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm'); - expect(nodeAttributesSelect.exists()).toBeTruthy(); - expect(nodeAttributesSelect.find('option').length).toBe(2); - }); - test('should show view node attributes link when attribute selected and show flyout when clicked', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'warm'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm'); - expect(nodeAttributesSelect.exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'warm-viewNodeDetailsFlyoutButton').exists()).toBeFalsy(); - expect(nodeAttributesSelect.find('option').length).toBe(2); - await act(async () => { - nodeAttributesSelect.simulate('change', { target: { value: 'attribute:true' } }); - }); - rendered.update(); - const flyoutButton = findTestSubject(rendered, 'warm-viewNodeDetailsFlyoutButton'); - expect(flyoutButton.exists()).toBeTruthy(); - await act(async () => { - await flyoutButton.simulate('click'); - }); - rendered.update(); - expect(rendered.find('.euiFlyout').exists()).toBeTruthy(); - }); - test('should show default allocation warning when no node roles are found', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: {}, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy(); - }); - test('should show default allocation notice when hot tier exists, but not warm tier', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data_hot: ['test'], data_cold: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeTruthy(); - }); - test('should not show default allocation notice when node with "data" role exists', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy(); - }); - - test('shows min age input only when enabled', async () => { - const rendered = mountWithIntl(component); - expect(findTestSubject(rendered, 'warm-selectedMinimumAge').exists()).toBeFalsy(); - await activatePhase(rendered, 'warm'); - expect(findTestSubject(rendered, 'warm-selectedMinimumAge').exists()).toBeTruthy(); - }); - }); - describe('cold phase', () => { - beforeEach(() => { - server.respondImmediately = true; - http.setupNodeListResponse(); - httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [ - { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, - ]); - }); - test('should allow 0 for phase timing', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - await setPhaseAfter(rendered, 'cold', '0'); - waitForFormLibValidation(rendered); - rendered.update(); - expectedErrorMessages(rendered, []); - }); - 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); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - await setPhaseAfter(rendered, 'cold', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - test('should show spinner for node attributes input when loading', async () => { - server.respondImmediately = false; - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'cold-dataTierAllocationControls').exists()).toBeTruthy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); - expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy(); - }); - test('should show warning instead of node attributes input when none exist', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['node1'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'cold'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy(); - expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy(); - }); - test('should show node attributes input when attributes exist', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'cold'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold'); - expect(nodeAttributesSelect.exists()).toBeTruthy(); - expect(nodeAttributesSelect.find('option').length).toBe(2); - }); - test('should show view node attributes link when attribute selected and show flyout when clicked', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'cold'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold'); - expect(nodeAttributesSelect.exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'cold-viewNodeDetailsFlyoutButton').exists()).toBeFalsy(); - expect(nodeAttributesSelect.find('option').length).toBe(2); - nodeAttributesSelect.simulate('change', { target: { value: 'attribute:true' } }); - rendered.update(); - const flyoutButton = findTestSubject(rendered, 'cold-viewNodeDetailsFlyoutButton'); - expect(flyoutButton.exists()).toBeTruthy(); - await act(async () => { - await flyoutButton.simulate('click'); - }); - rendered.update(); - expect(rendered.find('.euiFlyout').exists()).toBeTruthy(); - }); - test('should show positive number required error when trying to save with -1 for index priority', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - await setPhaseAfter(rendered, 'cold', '1'); - await setPhaseIndexPriority(rendered, 'cold', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - test('should show default allocation warning when no node roles are found', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: {}, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy(); - }); - test('should show default allocation notice when warm or hot tiers exists, but not cold tier', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeTruthy(); - }); - test('should not show default allocation notice when node with "data" role exists', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy(); - }); - - test('shows min age input only when enabled', async () => { - const rendered = mountWithIntl(component); - expect(findTestSubject(rendered, 'cold-selectedMinimumAge').exists()).toBeFalsy(); - await activatePhase(rendered, 'cold'); - expect(findTestSubject(rendered, 'cold-selectedMinimumAge').exists()).toBeTruthy(); - }); - }); - describe('delete phase', () => { - test('should allow 0 for phase timing', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activateDeletePhase(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); - await setPolicyName(rendered, 'mypolicy'); - await activateDeletePhase(rendered); - await setPhaseAfter(rendered, 'delete', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - - test('is hidden when disabled', async () => { - const rendered = mountWithIntl(component); - expect(findTestSubject(rendered, 'delete-phaseContent').exists()).toBeFalsy(); - await activateDeletePhase(rendered); - expect(findTestSubject(rendered, 'delete-phaseContent').exists()).toBeTruthy(); - }); - }); - describe('not on cloud', () => { - beforeEach(() => { - server.respondImmediately = true; - }); - test('should show all allocation options, even if using legacy config', async () => { - http.setupNodeListResponse({ - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: true, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - - // Assert that default, custom and 'none' options exist - findTestSubject(rendered, 'dataTierSelect').simulate('click'); - expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); - }); - }); - describe('on cloud', () => { - beforeEach(() => { - component = ( - true }} - /> - ); - ({ http } = editPolicyHelpers.setup()); - ({ server, httpRequestsMockHelpers } = http); - server.respondImmediately = true; - - httpRequestsMockHelpers.setPoliciesResponse(policies); - }); - - describe('with deprecated data role config', () => { - test('should hide data tier option on cloud using legacy node role configuration', async () => { - http.setupNodeListResponse({ - nodesByAttributes: { test: ['123'] }, - // On cloud, if using legacy config there will not be any "data_*" roles set. - nodesByRoles: { data: ['test'] }, - isUsingDeprecatedDataRoleConfig: true, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - - // Assert that default, custom and 'none' options exist - findTestSubject(rendered, 'dataTierSelect').simulate('click'); - expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); - }); - }); - - describe('with node role config', () => { - test('should show off, custom and data role options on cloud with data roles', async () => { - http.setupNodeListResponse({ - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - - findTestSubject(rendered, 'dataTierSelect').simulate('click'); - expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); - // We should not be showing the call-to-action for users to activate data tiers in cloud - expect(findTestSubject(rendered, 'cloudDataTierCallout').exists()).toBeFalsy(); - }); - - test('should show cloud notice when cold tier nodes do not exist', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'cloudDataTierCallout').exists()).toBeTruthy(); - // Assert that other notices are not showing - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - }); - }); - }); -}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts deleted file mode 100644 index 49fd651ca9453f..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { init as initHttpRequests } from './http_requests'; - -export type EditPolicySetup = ReturnType; - -export const setup = () => { - const { httpRequestsMockHelpers, server } = initHttpRequests(); - - const setupNodeListResponse = ( - response: Record = { - nodesByAttributes: { 'attribute:true': ['node1'] }, - nodesByRoles: { data: ['node1'] }, - } - ) => { - httpRequestsMockHelpers.setNodesListResponse(response); - }; - - return { - http: { - setupNodeListResponse, - httpRequestsMockHelpers, - server, - }, - }; -}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts deleted file mode 100644 index ea6e2af87a6d96..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import sinon, { SinonFakeServer } from 'sinon'; - -export type HttpResponse = Record | any[]; - -const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { - const setPoliciesResponse = (response: HttpResponse = []) => { - server.respondWith('/api/index_lifecycle_management/policies', [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]); - }; - - const setNodesListResponse = (response: HttpResponse = []) => { - server.respondWith('/api/index_lifecycle_management/nodes/list', [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]); - }; - - const setNodesDetailsResponse = (nodeAttributes: string, response: HttpResponse = []) => { - server.respondWith(`/api/index_lifecycle_management/nodes/${nodeAttributes}/details`, [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]); - }; - - return { - setPoliciesResponse, - setNodesListResponse, - setNodesDetailsResponse, - }; -}; - -export type HttpRequestMockHelpers = ReturnType; - -export const init = () => { - const server = sinon.fakeServer.create(); - - // Define default response for unhandled requests. - // We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry, - // and we can mock them all with a 200 instead of mocking each one individually. - server.respondWith([200, {}, 'DefaultSinonMockServerResponse']); - - const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server); - - return { - server, - httpRequestsMockHelpers, - }; -}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts deleted file mode 100644 index 95a45d12e23a2a..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as editPolicyHelpers from './edit_policy'; - -export { HttpRequestMockHelpers, init } from './http_requests'; - -export { editPolicyHelpers }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx similarity index 92% rename from x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx rename to x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx index 803560c67cf285..7733d547e34728 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx @@ -15,14 +15,14 @@ import { fatalErrorsServiceMock, injectedMetadataServiceMock, scopedHistoryMock, -} from '../../../../../src/core/public/mocks'; -import { HttpService } from '../../../../../src/core/public/http'; -import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks'; +} from '../../../../src/core/public/mocks'; +import { HttpService } from '../../../../src/core/public/http'; +import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/public/mocks'; -import { PolicyFromES } from '../../common/types'; -import { PolicyTable } from '../../public/application/sections/policy_table/policy_table'; -import { init as initHttp } from '../../public/application/services/http'; -import { init as initUiMetric } from '../../public/application/services/ui_metric'; +import { PolicyFromES } from '../common/types'; +import { PolicyTable } from '../public/application/sections/policy_table/policy_table'; +import { init as initHttp } from '../public/application/services/http'; +import { init as initUiMetric } from '../public/application/services/ui_metric'; initHttp( new HttpService().setup({ diff --git a/x-pack/plugins/index_lifecycle_management/common/types/api.ts b/x-pack/plugins/index_lifecycle_management/common/types/api.ts index 81190acd01ad1b..6d4e11c58f9bb2 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/api.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/api.ts @@ -20,6 +20,16 @@ export interface ListNodesRouteResponse { isUsingDeprecatedDataRoleConfig: boolean; } +export interface NodesDetails { + nodeId: string; + stats: { + name: string; + host: string; + }; +} + +export type NodesDetailsResponse = NodesDetails[]; + export interface ListSnapshotReposResponse { /** * An array of repository names From 1fa742d0ceeb543fae005bc576318f97e85df2a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 19 Feb 2021 08:57:14 -0500 Subject: [PATCH 28/43] [APM] Kql Search Bar suggests values outside the selected time range (#91918) --- .../apm/server/lib/index_pattern/get_dynamic_index_pattern.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index cb6183510ad168..8b81101fd2f39e 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -15,6 +15,7 @@ import { withApmSpan } from '../../utils/with_apm_span'; export interface IndexPatternTitleAndFields { title: string; + timeFieldName: string; fields: FieldDescriptor[]; } @@ -52,6 +53,7 @@ export const getDynamicIndexPattern = ({ const indexPattern: IndexPatternTitleAndFields = { fields, + timeFieldName: '@timestamp', title: indexPatternTitle, }; From 8b909cedc822569911cd794a35e172e11998dae6 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Fri, 19 Feb 2021 14:05:47 +0000 Subject: [PATCH 29/43] [Search Source] Do not request unmapped fields if source filters are provided (#91921) --- .../data/common/search/search_source/search_source.test.ts | 5 +---- .../data/common/search/search_source/search_source.ts | 7 +------ 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 030e620bea34b4..fd97a3d3381a99 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -418,10 +418,7 @@ describe('SearchSource', () => { searchSource.setField('fields', [{ field: '*', include_unmapped: 'true' }]); const request = await searchSource.getSearchRequestBody(); - expect(request.fields).toEqual([ - { field: 'field1', include_unmapped: 'true' }, - { field: 'field2', include_unmapped: 'true' }, - ]); + expect(request.fields).toEqual([{ field: 'field1' }, { field: 'field2' }]); }); test('returns all scripted fields when one fields entry is *', async () => { diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 118bb04c1742b4..486f2b36674536 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -503,12 +503,7 @@ export class SearchSource { // we need to get the list of fields from an index pattern return fields .filter((fld: IndexPatternField) => filterSourceFields(fld.name)) - .map((fld: IndexPatternField) => ({ - field: fld.name, - ...((wildcardField as Record)?.include_unmapped && { - include_unmapped: (wildcardField as Record).include_unmapped, - }), - })); + .map((fld: IndexPatternField) => ({ field: fld.name })); } private getFieldFromDocValueFieldsOrIndexPattern( From 4d34a13babd74d0c43066993468d4c69527254d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Fri, 19 Feb 2021 15:06:33 +0100 Subject: [PATCH 30/43] [Logs UI] Replace dependencies in the infra bundle (#91503) --- packages/kbn-optimizer/limits.yml | 2 +- x-pack/plugins/infra/common/formatters/index.ts | 3 +-- x-pack/plugins/infra/public/apps/legacy_app.tsx | 10 ++++------ x-pack/plugins/infra/public/hooks/use_link_props.tsx | 11 +++++------ 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 1a157624d7a8ab..1ebd0a9b83bd04 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -34,7 +34,7 @@ pageLoadAssetSize: indexLifecycleManagement: 107090 indexManagement: 140608 indexPatternManagement: 28222 - infra: 204800 + infra: 184320 fleet: 415829 ingestPipelines: 58003 inputControlVis: 172675 diff --git a/x-pack/plugins/infra/common/formatters/index.ts b/x-pack/plugins/infra/common/formatters/index.ts index 61e01aa7e68376..a4aeee80848241 100644 --- a/x-pack/plugins/infra/common/formatters/index.ts +++ b/x-pack/plugins/infra/common/formatters/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import Mustache from 'mustache'; import { createBytesFormatter } from './bytes'; import { formatNumber } from './number'; import { formatPercent } from './percent'; @@ -34,5 +33,5 @@ export const createFormatter = (format: InventoryFormatterType, template: string } const fmtFn = FORMATTERS[format]; const value = fmtFn(Number(val)); - return Mustache.render(template, { value }); + return template.replace(/{{value}}/g, value); }; diff --git a/x-pack/plugins/infra/public/apps/legacy_app.tsx b/x-pack/plugins/infra/public/apps/legacy_app.tsx index 50f24c2042c13c..8aeb99c4266519 100644 --- a/x-pack/plugins/infra/public/apps/legacy_app.tsx +++ b/x-pack/plugins/infra/public/apps/legacy_app.tsx @@ -11,7 +11,6 @@ import { AppMountParameters } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; import { Route, RouteProps, Router, Switch } from 'react-router-dom'; -import url from 'url'; // This exists purely to facilitate legacy app/infra URL redirects. // It will be removed in 8.0.0. @@ -79,11 +78,10 @@ const LegacyApp: React.FunctionComponent<{ history: History }> = ({ his nextPath = nextPathParts[0]; nextSearch = nextPathParts[1] ? nextPathParts[1] : undefined; - let nextUrl = url.format({ - pathname: `${nextBasePath}/${nextPath}`, - hash: undefined, - search: nextSearch, - }); + const builtPathname = `${nextBasePath}/${nextPath}`; + const builtSearch = nextSearch ? `?${nextSearch}` : ''; + + let nextUrl = `${builtPathname}${builtSearch}`; nextUrl = nextUrl.replace('//', '/'); diff --git a/x-pack/plugins/infra/public/hooks/use_link_props.tsx b/x-pack/plugins/infra/public/hooks/use_link_props.tsx index 72a538cd56281e..7546f9f0c9f797 100644 --- a/x-pack/plugins/infra/public/hooks/use_link_props.tsx +++ b/x-pack/plugins/infra/public/hooks/use_link_props.tsx @@ -7,7 +7,6 @@ import { useMemo } from 'react'; import { stringify } from 'query-string'; -import url from 'url'; import { url as urlUtils } from '../../../../../src/plugins/kibana_utils/public'; import { usePrefixPathWithBasepath } from './use_prefix_path_with_basepath'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; @@ -58,11 +57,11 @@ export const useLinkProps = ( }, [pathname, encodedSearch]); const href = useMemo(() => { - const link = url.format({ - pathname, - hash: mergedHash, - search: !hash ? encodedSearch : undefined, - }); + const builtPathname = pathname ?? ''; + const builtHash = mergedHash ? `#${mergedHash}` : ''; + const builtSearch = !hash ? (encodedSearch ? `?${encodedSearch}` : '') : ''; + + const link = `${builtPathname}${builtSearch}${builtHash}`; return prefixer(app, link); }, [mergedHash, hash, encodedSearch, pathname, prefixer, app]); From eea6f82e4e436abdf28266127a801a57e3c2dcad Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Fri, 19 Feb 2021 08:00:01 -0700 Subject: [PATCH 31/43] [Maps] Use new index patterns service for Maps telemetry (#86703) Co-authored-by: Matt Kime --- ...rver.indexpatternsservice._constructor_.md | 20 +++ ...-server.indexpatternsservice.clearcache.md | 13 ++ ...data-server.indexpatternsservice.create.md | 27 +++ ...rver.indexpatternsservice.createandsave.md | 26 +++ ....indexpatternsservice.createsavedobject.md | 25 +++ ...data-server.indexpatternsservice.delete.md | 24 +++ ...tternsservice.ensuredefaultindexpattern.md | 11 ++ ...er.indexpatternsservice.fieldarraytomap.md | 13 ++ ...s-data-server.indexpatternsservice.find.md | 13 ++ ...ns-data-server.indexpatternsservice.get.md | 13 ++ ...ta-server.indexpatternsservice.getcache.md | 11 ++ ...-server.indexpatternsservice.getdefault.md | 13 ++ ...atternsservice.getfieldsforindexpattern.md | 13 ++ ...dexpatternsservice.getfieldsforwildcard.md | 13 ++ ...data-server.indexpatternsservice.getids.md | 13 ++ ...er.indexpatternsservice.getidswithtitle.md | 16 ++ ...a-server.indexpatternsservice.gettitles.md | 13 ++ ...lugins-data-server.indexpatternsservice.md | 35 +++- ...rver.indexpatternsservice.refreshfields.md | 13 ++ ....indexpatternsservice.savedobjecttospec.md | 13 ++ ...-server.indexpatternsservice.setdefault.md | 13 ++ ....indexpatternsservice.updatesavedobject.md | 26 +++ ...ata-server.indexpatternsserviceprovider.md | 19 ++ ...ver.indexpatternsserviceprovider.setup.md} | 4 +- ...ver.indexpatternsserviceprovider.start.md} | 4 +- .../kibana-plugin-plugins-data-server.md | 1 + ...plugin-plugins-data-server.plugin.start.md | 4 +- src/plugins/data/server/index.ts | 4 +- .../index_patterns/index_patterns_service.ts | 2 +- src/plugins/data/server/server.api.md | 78 +++++++-- .../maps/server/kibana_server_services.ts | 18 +- .../maps_telemetry/maps_telemetry.test.js | 61 ++++++- .../server/maps_telemetry/maps_telemetry.ts | 164 +++++++----------- .../sample_index_pattern_saved_objects.json | 59 ------- x-pack/plugins/maps/server/plugin.ts | 13 +- 35 files changed, 616 insertions(+), 192 deletions(-) create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.create.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.delete.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.find.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.get.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getids.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md rename docs/development/plugins/data/server/{kibana-plugin-plugins-data-server.indexpatternsservice.setup.md => kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md} (67%) rename docs/development/plugins/data/server/{kibana-plugin-plugins-data-server.indexpatternsservice.start.md => kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md} (75%) delete mode 100644 x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md new file mode 100644 index 00000000000000..86e879eecc5a96 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [(constructor)](./kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md) + +## IndexPatternsService.(constructor) + +Constructs a new instance of the `IndexPatternsService` class + +Signature: + +```typescript +constructor({ uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, }: IndexPatternsServiceDeps); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, } | IndexPatternsServiceDeps | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md new file mode 100644 index 00000000000000..eb0e92f3760c83 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [clearCache](./kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md) + +## IndexPatternsService.clearCache property + +Clear index pattern list cache + +Signature: + +```typescript +clearCache: (id?: string | undefined) => void; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.create.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.create.md new file mode 100644 index 00000000000000..e5cc7c2e433ca3 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.create.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [create](./kibana-plugin-plugins-data-server.indexpatternsservice.create.md) + +## IndexPatternsService.create() method + +Create a new index pattern instance + +Signature: + +```typescript +create(spec: IndexPatternSpec, skipFetchFields?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| spec | IndexPatternSpec | | +| skipFetchFields | boolean | | + +Returns: + +`Promise` + +IndexPattern + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md new file mode 100644 index 00000000000000..9b6e3a82528d59 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [createAndSave](./kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md) + +## IndexPatternsService.createAndSave() method + +Create a new index pattern and save it right away + +Signature: + +```typescript +createAndSave(spec: IndexPatternSpec, override?: boolean, skipFetchFields?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| spec | IndexPatternSpec | | +| override | boolean | | +| skipFetchFields | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md new file mode 100644 index 00000000000000..6ffadf648f5b6f --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [createSavedObject](./kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md) + +## IndexPatternsService.createSavedObject() method + +Save a new index pattern + +Signature: + +```typescript +createSavedObject(indexPattern: IndexPattern, override?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPattern | IndexPattern | | +| override | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.delete.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.delete.md new file mode 100644 index 00000000000000..929a8038494284 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.delete.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [delete](./kibana-plugin-plugins-data-server.indexpatternsservice.delete.md) + +## IndexPatternsService.delete() method + +Deletes an index pattern from .kibana index + +Signature: + +```typescript +delete(indexPatternId: string): Promise<{}>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPatternId | string | | + +Returns: + +`Promise<{}>` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md new file mode 100644 index 00000000000000..c4f6b61e4feb4c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [ensureDefaultIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md) + +## IndexPatternsService.ensureDefaultIndexPattern property + +Signature: + +```typescript +ensureDefaultIndexPattern: EnsureDefaultIndexPattern; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md new file mode 100644 index 00000000000000..e0b27c317ff74c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [fieldArrayToMap](./kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md) + +## IndexPatternsService.fieldArrayToMap property + +Converts field array to map + +Signature: + +```typescript +fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.find.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.find.md new file mode 100644 index 00000000000000..35b94133462aa3 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.find.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [find](./kibana-plugin-plugins-data-server.indexpatternsservice.find.md) + +## IndexPatternsService.find property + +Find and load index patterns by title + +Signature: + +```typescript +find: (search: string, size?: number) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.get.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.get.md new file mode 100644 index 00000000000000..874f1d1a490c77 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [get](./kibana-plugin-plugins-data-server.indexpatternsservice.get.md) + +## IndexPatternsService.get property + +Get an index pattern by id. Cache optimized + +Signature: + +```typescript +get: (id: string) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md new file mode 100644 index 00000000000000..821c06984e55e4 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getCache](./kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md) + +## IndexPatternsService.getCache property + +Signature: + +```typescript +getCache: () => Promise[] | null | undefined>; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md new file mode 100644 index 00000000000000..104e605e01bcbf --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md) + +## IndexPatternsService.getDefault property + +Get default index pattern + +Signature: + +```typescript +getDefault: () => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md new file mode 100644 index 00000000000000..db871c0bec83ca --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getFieldsForIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md) + +## IndexPatternsService.getFieldsForIndexPattern property + +Get field list by providing an index patttern (or spec) + +Signature: + +```typescript +getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md new file mode 100644 index 00000000000000..0b2c6dbfdef8b2 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getFieldsForWildcard](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md) + +## IndexPatternsService.getFieldsForWildcard property + +Get field list by providing { pattern } + +Signature: + +```typescript +getFieldsForWildcard: (options: GetFieldsOptions) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getids.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getids.md new file mode 100644 index 00000000000000..2f0fb56cc44575 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getids.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getIds](./kibana-plugin-plugins-data-server.indexpatternsservice.getids.md) + +## IndexPatternsService.getIds property + +Get list of index pattern ids + +Signature: + +```typescript +getIds: (refresh?: boolean) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md new file mode 100644 index 00000000000000..6433c78483545c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getIdsWithTitle](./kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md) + +## IndexPatternsService.getIdsWithTitle property + +Get list of index pattern ids with titles + +Signature: + +```typescript +getIdsWithTitle: (refresh?: boolean) => Promise>; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md new file mode 100644 index 00000000000000..385e7f70d237a0 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getTitles](./kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md) + +## IndexPatternsService.getTitles property + +Get list of index pattern titles + +Signature: + +```typescript +getTitles: (refresh?: boolean) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md index 83e912d80dbd1b..d55a6e9b325a20 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md @@ -7,13 +7,42 @@ Signature: ```typescript -export declare class IndexPatternsServiceProvider implements Plugin +export declare class IndexPatternsService ``` +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)({ uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, })](./kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md) | | Constructs a new instance of the IndexPatternsService class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [clearCache](./kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md) | | (id?: string | undefined) => void | Clear index pattern list cache | +| [ensureDefaultIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md) | | EnsureDefaultIndexPattern | | +| [fieldArrayToMap](./kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md) | | (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record<string, FieldSpec> | Converts field array to map | +| [find](./kibana-plugin-plugins-data-server.indexpatternsservice.find.md) | | (search: string, size?: number) => Promise<IndexPattern[]> | Find and load index patterns by title | +| [get](./kibana-plugin-plugins-data-server.indexpatternsservice.get.md) | | (id: string) => Promise<IndexPattern> | Get an index pattern by id. Cache optimized | +| [getCache](./kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<IndexPatternSavedObjectAttrs>[] | null | undefined> | | +| [getDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md) | | () => Promise<IndexPattern | null> | Get default index pattern | +| [getFieldsForIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise<any> | Get field list by providing an index patttern (or spec) | +| [getFieldsForWildcard](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md) | | (options: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | +| [getIds](./kibana-plugin-plugins-data-server.indexpatternsservice.getids.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern ids | +| [getIdsWithTitle](./kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md) | | (refresh?: boolean) => Promise<Array<{
    id: string;
    title: string;
    }>> | Get list of index pattern ids with titles | +| [getTitles](./kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern titles | +| [refreshFields](./kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md) | | (indexPattern: IndexPattern) => Promise<void> | Refresh field list for a given index pattern | +| [savedObjectToSpec](./kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md) | | (savedObject: SavedObject<IndexPatternAttributes>) => IndexPatternSpec | Converts index pattern saved object to index pattern spec | +| [setDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md) | | (id: string, force?: boolean) => Promise<void> | Optionally set default index pattern, unless force = true | + ## Methods | Method | Modifiers | Description | | --- | --- | --- | -| [setup(core, { expressions })](./kibana-plugin-plugins-data-server.indexpatternsservice.setup.md) | | | -| [start(core, { fieldFormats, logger })](./kibana-plugin-plugins-data-server.indexpatternsservice.start.md) | | | +| [create(spec, skipFetchFields)](./kibana-plugin-plugins-data-server.indexpatternsservice.create.md) | | Create a new index pattern instance | +| [createAndSave(spec, override, skipFetchFields)](./kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md) | | Create a new index pattern and save it right away | +| [createSavedObject(indexPattern, override)](./kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md) | | Save a new index pattern | +| [delete(indexPatternId)](./kibana-plugin-plugins-data-server.indexpatternsservice.delete.md) | | Deletes an index pattern from .kibana index | +| [updateSavedObject(indexPattern, saveAttempts, ignoreErrors)](./kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md) | | Save existing index pattern. Will attempt to merge differences if there are conflicts | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md new file mode 100644 index 00000000000000..6b81447eca9eda --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [refreshFields](./kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md) + +## IndexPatternsService.refreshFields property + +Refresh field list for a given index pattern + +Signature: + +```typescript +refreshFields: (indexPattern: IndexPattern) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md new file mode 100644 index 00000000000000..92ac4e556ae291 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [savedObjectToSpec](./kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md) + +## IndexPatternsService.savedObjectToSpec property + +Converts index pattern saved object to index pattern spec + +Signature: + +```typescript +savedObjectToSpec: (savedObject: SavedObject) => IndexPatternSpec; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md new file mode 100644 index 00000000000000..708d645a79f1a7 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [setDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md) + +## IndexPatternsService.setDefault property + +Optionally set default index pattern, unless force = true + +Signature: + +```typescript +setDefault: (id: string, force?: boolean) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md new file mode 100644 index 00000000000000..17f261aebdc658 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [updateSavedObject](./kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md) + +## IndexPatternsService.updateSavedObject() method + +Save existing index pattern. Will attempt to merge differences if there are conflicts + +Signature: + +```typescript +updateSavedObject(indexPattern: IndexPattern, saveAttempts?: number, ignoreErrors?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPattern | IndexPattern | | +| saveAttempts | number | | +| ignoreErrors | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md new file mode 100644 index 00000000000000..d408f00e33c9e8 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) + +## IndexPatternsServiceProvider class + +Signature: + +```typescript +export declare class IndexPatternsServiceProvider implements Plugin +``` + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [setup(core, { expressions })](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md) | | | +| [start(core, { fieldFormats, logger })](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md) | | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md similarity index 67% rename from docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md rename to docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md index 6cac0a806d2ecb..b5047d34efac1f 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [setup](./kibana-plugin-plugins-data-server.indexpatternsservice.setup.md) +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) > [setup](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md) -## IndexPatternsService.setup() method +## IndexPatternsServiceProvider.setup() method Signature: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md similarity index 75% rename from docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md rename to docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md index 6528b1c213ccad..98f9310c6d98cc 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [start](./kibana-plugin-plugins-data-server.indexpatternsservice.start.md) +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) > [start](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md) -## IndexPatternsService.start() method +## IndexPatternsServiceProvider.start() method Signature: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 4739de481e0207..491babcdfdecfb 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -12,6 +12,7 @@ | [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) | | | [IndexPatternsFetcher](./kibana-plugin-plugins-data-server.indexpatternsfetcher.md) | | | [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) | | +| [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) | | | [Plugin](./kibana-plugin-plugins-data-server.plugin.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index 9dc38f96df4be6..f479ffd52e9b89 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,7 +12,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -31,7 +31,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }` diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index b57791db2b9fa5..464cc2b1f54d16 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -146,6 +146,8 @@ export { UI_SETTINGS, IndexPattern, IndexPatternLoadExpressionFunctionDefinition, + IndexPatternsService, + IndexPatternsService as IndexPatternsCommonService, } from '../common'; /** @@ -306,4 +308,4 @@ export const config: PluginConfigDescriptor = { schema: configSchema, }; -export type { IndexPatternsServiceProvider as IndexPatternsService } from './index_patterns'; +export type { IndexPatternsServiceProvider } from './index_patterns'; diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index 1888feb93ec0d1..5d703021b94da9 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -19,7 +19,7 @@ import { DataPluginStartDependencies, DataPluginStart } from '../plugin'; import { registerRoutes } from './routes'; import { indexPatternSavedObjectType } from '../saved_objects'; import { capabilitiesProvider } from './capabilities_provider'; -import { IndexPatternsService as IndexPatternsCommonService } from '../../common/index_patterns'; +import { IndexPatternsCommonService } from '../'; import { FieldFormatsStart } from '../field_formats'; import { getIndexPatternLoad } from './expressions'; import { UiSettingsServerToCommon } from './ui_settings_wrapper'; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 23aaab36e79055..c33bd155897802 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -889,11 +889,54 @@ export class IndexPatternsFetcher { validatePatternListActive(patternList: string[]): Promise; } +// Warning: (ae-missing-release-tag) "IndexPatternsService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +class IndexPatternsService { + // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceDeps" needs to be exported by the entry point index.d.ts + constructor({ uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, }: IndexPatternsServiceDeps); + clearCache: (id?: string | undefined) => void; + create(spec: IndexPatternSpec, skipFetchFields?: boolean): Promise; + createAndSave(spec: IndexPatternSpec, override?: boolean, skipFetchFields?: boolean): Promise; + createSavedObject(indexPattern: IndexPattern, override?: boolean): Promise; + delete(indexPatternId: string): Promise<{}>; + // Warning: (ae-forgotten-export) The symbol "EnsureDefaultIndexPattern" needs to be exported by the entry point index.d.ts + // + // (undocumented) + ensureDefaultIndexPattern: EnsureDefaultIndexPattern; + // Warning: (ae-forgotten-export) The symbol "FieldAttrs" needs to be exported by the entry point index.d.ts + fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record; + find: (search: string, size?: number) => Promise; + get: (id: string) => Promise; + // Warning: (ae-forgotten-export) The symbol "IndexPatternSavedObjectAttrs" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getCache: () => Promise[] | null | undefined>; + getDefault: () => Promise; + getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; + // Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts + getFieldsForWildcard: (options: GetFieldsOptions) => Promise; + getIds: (refresh?: boolean) => Promise; + getIdsWithTitle: (refresh?: boolean) => Promise>; + getTitles: (refresh?: boolean) => Promise; + refreshFields: (indexPattern: IndexPattern) => Promise; + savedObjectToSpec: (savedObject: SavedObject_2) => IndexPatternSpec; + setDefault: (id: string, force?: boolean) => Promise; + updateSavedObject(indexPattern: IndexPattern, saveAttempts?: number, ignoreErrors?: boolean): Promise; +} + +export { IndexPatternsService as IndexPatternsCommonService } + +export { IndexPatternsService } + // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStart" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "IndexPatternsServiceProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class IndexPatternsService implements Plugin_3 { +export class IndexPatternsServiceProvider implements Plugin_3 { // Warning: (ae-forgotten-export) The symbol "DataPluginStartDependencies" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceSetupDeps" needs to be exported by the entry point index.d.ts // @@ -903,7 +946,7 @@ export class IndexPatternsService implements Plugin_3 Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract_2, elasticsearchClient: ElasticsearchClient_2) => Promise; }; } @@ -1139,7 +1182,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -1418,21 +1461,20 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:100:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:126:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:126:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:241:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:242:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:251:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:252:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:253:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:265:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index_patterns/index_patterns_service.ts:59:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:241:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:241:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:241:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:241:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:243:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:244:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:253:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:254:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:255:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:259:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:260:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:79:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // src/plugins/data/server/search/types.ts:114:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts diff --git a/x-pack/plugins/maps/server/kibana_server_services.ts b/x-pack/plugins/maps/server/kibana_server_services.ts index 97c17dbe3b33c8..6b59b460ad2c9c 100644 --- a/x-pack/plugins/maps/server/kibana_server_services.ts +++ b/x-pack/plugins/maps/server/kibana_server_services.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { ISavedObjectsRepository } from 'kibana/server'; +import { ElasticsearchClient, ISavedObjectsRepository } from 'kibana/server'; +import { SavedObjectsClient } from '../../../../src/core/server'; +import { IndexPatternsCommonService } from '../../../../src/plugins/data/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IndexPatternsServiceStart } from '../../../../src/plugins/data/server/index_patterns'; let internalRepository: ISavedObjectsRepository; export const setInternalRepository = ( @@ -14,3 +18,15 @@ export const setInternalRepository = ( internalRepository = createInternalRepository(); }; export const getInternalRepository = () => internalRepository; + +let indexPatternsService: IndexPatternsCommonService; +export const setIndexPatternsService = async ( + indexPatternsServiceFactory: IndexPatternsServiceStart['indexPatternsServiceFactory'], + elasticsearchClient: ElasticsearchClient +) => { + indexPatternsService = await indexPatternsServiceFactory( + new SavedObjectsClient(getInternalRepository()), + elasticsearchClient + ); +}; +export const getIndexPatternsService = () => indexPatternsService; diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js index 7243dd84d6a853..8725e672ec3682 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js @@ -6,13 +6,70 @@ */ import mapSavedObjects from './test_resources/sample_map_saved_objects.json'; -import indexPatternSavedObjects from './test_resources/sample_index_pattern_saved_objects'; import { buildMapsIndexPatternsTelemetry, buildMapsSavedObjectsTelemetry, getLayerLists, } from './maps_telemetry'; +jest.mock('../kibana_server_services', () => { + // Mocked for geo shape agg detection + const testAggIndexPatternId = '4a7f6010-0aed-11ea-9dd2-95afd7ad44d4'; + const testAggIndexPattern = { + id: testAggIndexPatternId, + fields: [ + { + name: 'geometry', + esTypes: ['geo_shape'], + }, + ], + }; + const testIndexPatterns = { + 1: { + id: '1', + fields: [ + { + name: 'one', + esTypes: ['geo_point'], + }, + ], + }, + 2: { + id: '2', + fields: [ + { + name: 'two', + esTypes: ['geo_point'], + }, + ], + }, + 3: { + id: '3', + fields: [ + { + name: 'three', + esTypes: ['geo_shape'], + }, + ], + }, + }; + return { + getIndexPatternsService() { + return { + async get(x) { + return x === testAggIndexPatternId ? testAggIndexPattern : testIndexPatterns[x]; + }, + async getIds() { + return Object.values(testIndexPatterns).map((x) => x.id); + }, + async getFieldsForIndexPattern(x) { + return x.fields; + }, + }; + }, + }; +}); + describe('buildMapsSavedObjectsTelemetry', () => { test('returns zeroed telemetry data when there are no saved objects', async () => { const result = buildMapsSavedObjectsTelemetry([]); @@ -88,7 +145,7 @@ describe('buildMapsSavedObjectsTelemetry', () => { test('returns expected telemetry data from index patterns', async () => { const layerLists = getLayerLists(mapSavedObjects); - const result = buildMapsIndexPatternsTelemetry(indexPatternSavedObjects, layerLists); + const result = await buildMapsIndexPatternsTelemetry(layerLists); expect(result).toMatchObject({ indexPatternsWithGeoFieldCount: 3, diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts index 5b098af760e655..0387d96046cb11 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import { SavedObject } from 'kibana/server'; -import { IFieldType, IndexPatternAttributes } from 'src/plugins/data/public'; +import { IFieldType } from 'src/plugins/data/public'; import { ES_GEO_FIELD_TYPE, LAYER_TYPE, @@ -22,7 +22,7 @@ import { LayerDescriptor, } from '../../common/descriptor_types'; import { MapSavedObject, MapSavedObjectAttributes } from '../../common/map_saved_object_type'; -import { getInternalRepository } from '../kibana_server_services'; +import { getIndexPatternsService, getInternalRepository } from '../kibana_server_services'; import { MapsConfigType } from '../../config'; interface Settings { @@ -94,37 +94,6 @@ function getUniqueLayerCounts(layerCountsList: ILayerTypeCount[], mapsCount: num }, {}); } -function getIndexPatternsWithGeoFieldCount( - indexPatterns: Array> -) { - const fieldLists = indexPatterns.map((indexPattern) => - indexPattern.attributes && indexPattern.attributes.fields - ? JSON.parse(indexPattern.attributes.fields) - : [] - ); - - const fieldListsWithGeoFields = fieldLists.filter((fields) => - fields.some( - (field: IFieldType) => - field.type === ES_GEO_FIELD_TYPE.GEO_POINT || field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE - ) - ); - - const fieldListsWithGeoPointFields = fieldLists.filter((fields) => - fields.some((field: IFieldType) => field.type === ES_GEO_FIELD_TYPE.GEO_POINT) - ); - - const fieldListsWithGeoShapeFields = fieldLists.filter((fields) => - fields.some((field: IFieldType) => field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE) - ); - - return { - indexPatternsWithGeoFieldCount: fieldListsWithGeoFields.length, - indexPatternsWithGeoPointFieldCount: fieldListsWithGeoPointFields.length, - indexPatternsWithGeoShapeFieldCount: fieldListsWithGeoShapeFields.length, - }; -} - function getEMSLayerCount(layerLists: LayerDescriptor[][]): ILayerTypeCount[] { return layerLists.map((layerList: LayerDescriptor[]) => { const emsLayers = layerList.filter((layer: LayerDescriptor) => { @@ -143,41 +112,25 @@ function getEMSLayerCount(layerLists: LayerDescriptor[][]): ILayerTypeCount[] { }) as ILayerTypeCount[]; } -function isFieldGeoShape( - indexPatterns: Array>, +async function isFieldGeoShape( indexPatternId: string, geoField: string | undefined -): boolean { - if (!geoField) { +): Promise { + if (!geoField || !indexPatternId) { return false; } - - const matchIndexPattern = indexPatterns.find( - (indexPattern: SavedObject) => { - return indexPattern.id === indexPatternId; - } - ); - - if (!matchIndexPattern) { + const indexPatternsService = await getIndexPatternsService(); + const indexPattern = await indexPatternsService.get(indexPatternId); + if (!indexPattern) { return false; } - - const fieldList: IFieldType[] = - matchIndexPattern.attributes && matchIndexPattern.attributes.fields - ? JSON.parse(matchIndexPattern.attributes.fields) - : []; - - const matchField = fieldList.find((field: IFieldType) => { - return field.name === geoField; - }); - - return !!matchField && matchField.type === ES_GEO_FIELD_TYPE.GEO_SHAPE; + const fieldsForIndexPattern = await indexPatternsService.getFieldsForIndexPattern(indexPattern); + return fieldsForIndexPattern.some( + (fieldDescriptor: IFieldType) => fieldDescriptor.name && fieldDescriptor.name === geoField! + ); } -function isGeoShapeAggLayer( - indexPatterns: Array>, - layer: LayerDescriptor -): boolean { +async function isGeoShapeAggLayer(layer: LayerDescriptor): Promise { if (layer.sourceDescriptor === null) { return false; } @@ -192,8 +145,7 @@ function isGeoShapeAggLayer( const sourceDescriptor = layer.sourceDescriptor; if (sourceDescriptor.type === SOURCE_TYPES.ES_GEO_GRID) { - return isFieldGeoShape( - indexPatterns, + return await isFieldGeoShape( (sourceDescriptor as ESGeoGridSourceDescriptor).indexPatternId, (sourceDescriptor as ESGeoGridSourceDescriptor).geoField ); @@ -201,8 +153,7 @@ function isGeoShapeAggLayer( sourceDescriptor.type === SOURCE_TYPES.ES_SEARCH && (sourceDescriptor as ESSearchSourceDescriptor).scalingType === SCALING_TYPES.CLUSTERS ) { - return isFieldGeoShape( - indexPatterns, + return await isFieldGeoShape( (sourceDescriptor as ESSearchSourceDescriptor).indexPatternId, (sourceDescriptor as ESSearchSourceDescriptor).geoField ); @@ -211,17 +162,15 @@ function isGeoShapeAggLayer( } } -function getGeoShapeAggCount( - layerLists: LayerDescriptor[][], - indexPatterns: Array> -): number { - const countsPerMap: number[] = layerLists.map((layerList: LayerDescriptor[]) => { - const geoShapeAggLayers = layerList.filter((layerDescriptor) => { - return isGeoShapeAggLayer(indexPatterns, layerDescriptor); - }); - return geoShapeAggLayers.length; - }); - +async function getGeoShapeAggCount(layerLists: LayerDescriptor[][]): Promise { + const countsPerMap: number[] = await Promise.all( + layerLists.map(async (layerList: LayerDescriptor[]) => { + const boolIsAggLayerArr = await Promise.all( + layerList.map(async (layerDescriptor) => await isGeoShapeAggLayer(layerDescriptor)) + ); + return boolIsAggLayerArr.filter((x) => x).length; + }) + ); return _.sum(countsPerMap); } @@ -235,30 +184,56 @@ export function getLayerLists(mapSavedObjects: MapSavedObject[]): LayerDescripto }); } -export function buildMapsIndexPatternsTelemetry( - indexPatternSavedObjects: Array>, - layerLists: LayerDescriptor[][] -): GeoIndexPatternsUsage { - const { - indexPatternsWithGeoFieldCount, - indexPatternsWithGeoPointFieldCount, - indexPatternsWithGeoShapeFieldCount, - } = getIndexPatternsWithGeoFieldCount(indexPatternSavedObjects); +async function filterIndexPatternsByField(fields: string[]) { + const indexPatternsService = await getIndexPatternsService(); + const indexPatternIds = await indexPatternsService.getIds(true); + let numIndexPatternsContainingField = 0; + await Promise.all( + indexPatternIds.map(async (indexPatternId: string) => { + const indexPattern = await indexPatternsService.get(indexPatternId); + const fieldsForIndexPattern = await indexPatternsService.getFieldsForIndexPattern( + indexPattern + ); + const containsField = fields.some((field: string) => + fieldsForIndexPattern.some( + (fieldDescriptor: IFieldType) => + fieldDescriptor.esTypes && fieldDescriptor.esTypes.includes(field) + ) + ); + if (containsField) { + numIndexPatternsContainingField++; + } + }) + ); + return numIndexPatternsContainingField; +} +export async function buildMapsIndexPatternsTelemetry( + layerLists: LayerDescriptor[][] +): Promise { + const indexPatternsWithGeoField = await filterIndexPatternsByField([ + ES_GEO_FIELD_TYPE.GEO_POINT, + ES_GEO_FIELD_TYPE.GEO_SHAPE, + ]); + const indexPatternsWithGeoPointField = await filterIndexPatternsByField([ + ES_GEO_FIELD_TYPE.GEO_POINT, + ]); + const indexPatternsWithGeoShapeField = await filterIndexPatternsByField([ + ES_GEO_FIELD_TYPE.GEO_SHAPE, + ]); // Tracks whether user uses Gold+ only functionality - const geoShapeAggLayersCount = getGeoShapeAggCount(layerLists, indexPatternSavedObjects); + const geoShapeAggLayersCount = await getGeoShapeAggCount(layerLists); return { - indexPatternsWithGeoFieldCount, - indexPatternsWithGeoPointFieldCount, - indexPatternsWithGeoShapeFieldCount, + indexPatternsWithGeoFieldCount: indexPatternsWithGeoField, + indexPatternsWithGeoPointFieldCount: indexPatternsWithGeoPointField, + indexPatternsWithGeoShapeFieldCount: indexPatternsWithGeoShapeField, geoShapeAggLayersCount, }; } export function buildMapsSavedObjectsTelemetry(layerLists: LayerDescriptor[][]): LayersStatsUsage { const mapsCount = layerLists.length; - const dataSourcesCount = layerLists.map((layerList: LayerDescriptor[]) => { // todo: not every source-descriptor has an id // @ts-ignore @@ -340,16 +315,7 @@ export async function getMapsTelemetry(config: MapsConfigType): Promise( - 'index-pattern', - (savedObjects) => - _.mergeWith( - indexPatternsTelemetry, - buildMapsIndexPatternsTelemetry(savedObjects, layerLists), - (prevVal, currVal) => prevVal || 0 + currVal || 0 // Additive merge - ) - ); + const indexPatternsTelemetry = await buildMapsIndexPatternsTelemetry(layerLists); return { settings: { diff --git a/x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json b/x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json deleted file mode 100644 index 0b36d5ff840162..00000000000000 --- a/x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json +++ /dev/null @@ -1,59 +0,0 @@ -[ - { - "attributes": { - "fields": "[{\"name\":\"geometry\",\"type\":\"geo_shape\",\"esTypes\":[\"geo_shape\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false}]", - "timeFieldName": "ORIG_DATE", - "title": "indexpattern-with-geoshape" - }, - "id": "4a7f6010-0aed-11ea-9dd2-95afd7ad44d4", - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2019-11-19T16:54:46.405Z", - "version": "Wzg0LDFd" - }, - { - "attributes": { - "fields": "[{\"name\":\"geometry\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", - "title": "indexpattern-with-geopoint" - }, - "id": "55d572f0-0b07-11ea-9dd2-95afd7ad44d4", - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2019-11-19T20:05:37.607Z", - "version": "WzExMSwxXQ==" - }, - { - "attributes": { - "fields": "[{\"name\":\"geometry\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", - "title": "indexpattern-with-geopoint2" - }, - "id": "55d572f0-0b07-11ea-9dd2-95afd7ad44d4", - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2019-11-19T20:05:37.607Z", - "version": "WzExMSwxXQ==" - }, - { - "attributes": { - "fields": "[{\"name\":\"assessment_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"date_exterior_condition\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"recording_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sale_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", - "title": "indexpattern-without-geo" - }, - "id": "55d572f0-0b07-11ea-9dd2-95afd7ad44d4", - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2019-11-19T20:05:37.607Z", - "version": "WzExMSwxXQ==" - } -] diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index cb22a98b70aa80..4118074841aef2 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -20,7 +20,7 @@ import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getExistingMapPath } from '../ import { mapSavedObjects, mapsTelemetrySavedObjects } from './saved_objects'; import { MapsXPackConfig } from '../config'; // @ts-ignore -import { setInternalRepository } from './kibana_server_services'; +import { setIndexPatternsService, setInternalRepository } from './kibana_server_services'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { emsBoundariesSpecProvider } from './tutorials/ems'; // @ts-ignore @@ -30,6 +30,7 @@ import { LicensingPluginSetup } from '../../licensing/server'; import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; import { MapsLegacyPluginSetup } from '../../../../src/plugins/maps_legacy/server'; import { EMSSettings } from '../common/ems_settings'; +import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; interface SetupDeps { features: FeaturesPluginSetupContract; @@ -39,6 +40,10 @@ interface SetupDeps { mapsLegacy: MapsLegacyPluginSetup; } +export interface StartDeps { + data: DataPluginStart; +} + export class MapsPlugin implements Plugin { readonly _initializerContext: PluginInitializerContext; private readonly _logger: Logger; @@ -208,7 +213,11 @@ export class MapsPlugin implements Plugin { } // @ts-ignore - start(core: CoreStart) { + start(core: CoreStart, plugins: StartDeps) { setInternalRepository(core.savedObjects.createInternalRepository); + setIndexPatternsService( + plugins.data.indexPatterns.indexPatternsServiceFactory, + core.elasticsearch.client.asInternalUser + ); } } From a108469ec78a6df4d7628eb94f492b583e7014e6 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Fri, 19 Feb 2021 10:50:35 -0500 Subject: [PATCH 32/43] Allowing deletion of collections (#91926) --- .../server/routes/api/cases/delete_cases.ts | 44 ------------ .../basic/tests/cases/delete_cases.ts | 71 ++++++++++++++++++- 2 files changed, 70 insertions(+), 45 deletions(-) diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts index 263b814df4146e..497e33d7feb308 100644 --- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts @@ -8,43 +8,12 @@ import { schema } from '@kbn/config-schema'; import { SavedObjectsClientContract } from 'src/core/server'; -import { CaseType } from '../../../../common/api'; import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; import { CASES_URL } from '../../../../common/constants'; import { CaseServiceSetup } from '../../../services'; -async function unremovableCases({ - caseService, - client, - ids, - force, -}: { - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; - ids: string[]; - force: boolean | undefined; -}): Promise { - // if the force flag was included then we can skip checking whether the cases are collections and go ahead - // and delete them - if (force) { - return []; - } - - const cases = await caseService.getCases({ caseIds: ids, client }); - const parentCases = cases.saved_objects.filter( - /** - * getCases will return an array of saved_objects and some can be successful cases where as others - * might have failed to find the ID. If it fails to find it, it will set the error field but not - * the attributes so check that we didn't receive an error. - */ - (caseObj) => !caseObj.error && caseObj.attributes.type === CaseType.collection - ); - - return parentCases.map((parentCase) => parentCase.id); -} - async function deleteSubCases({ caseService, client, @@ -84,25 +53,12 @@ export function initDeleteCasesApi({ caseService, router, userActionService }: R validate: { query: schema.object({ ids: schema.arrayOf(schema.string()), - force: schema.maybe(schema.boolean()), }), }, }, async (context, request, response) => { try { const client = context.core.savedObjects.client; - const unremovable = await unremovableCases({ - caseService, - client, - ids: request.query.ids, - force: request.query.force, - }); - - if (unremovable.length > 0) { - return response.badRequest({ - body: `Case IDs: [${unremovable.join(' ,')}] are not removable`, - }); - } await Promise.all( request.query.ids.map((id) => caseService.deleteCase({ diff --git a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts index 1ea4712e260f0e..8edc3b0d081138 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts @@ -10,7 +10,17 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../plugins/case/common/constants'; import { postCaseReq, postCommentUserReq } from '../../../common/lib/mock'; -import { deleteCases, deleteCasesUserActions, deleteComments } from '../../../common/lib/utils'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + deleteCases, + deleteCasesUserActions, + deleteComments, +} from '../../../common/lib/utils'; +import { getSubCaseDetailsUrl } from '../../../../../plugins/case/common/api/helpers'; +import { CollectionWithSubCaseResponse } from '../../../../../plugins/case/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -79,5 +89,64 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(404); }); + + describe('sub cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should delete the sub cases when deleting a collection', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCase?.id).to.not.eql(undefined); + + const { body } = await supertest + .delete(`${CASES_URL}?ids=["${caseInfo.id}"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + expect(body).to.eql({}); + await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id)) + .send() + .expect(404); + }); + + it(`should delete a sub case's comments when that case gets deleted`, async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCase?.id).to.not.eql(undefined); + + // there should be two comments on the sub case now + const { + body: patchedCaseWithSubCase, + }: { body: CollectionWithSubCaseResponse } = await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments`) + .set('kbn-xsrf', 'true') + .query({ subCaseID: caseInfo.subCase!.id }) + .send(postCommentUserReq) + .expect(200); + + const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ + patchedCaseWithSubCase.subCase!.comments![1].id + }`; + // make sure we can get the second comment + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); + + await supertest + .delete(`${CASES_URL}?ids=["${caseInfo.id}"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(404); + }); + }); }); }; From d6984ca160fe40b615e2fd813d8318451b09d9a5 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Fri, 19 Feb 2021 10:56:52 -0500 Subject: [PATCH 33/43] [Maps] upgrade mapbox-gl to v1.13.1 (#91564) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a65c12fce46990..67263f53f28a24 100644 --- a/package.json +++ b/package.json @@ -724,7 +724,7 @@ "loader-utils": "^1.2.3", "log-symbols": "^2.2.0", "lz-string": "^1.4.4", - "mapbox-gl": "^1.12.0", + "mapbox-gl": "1.13.1", "mapbox-gl-draw-rectangle-mode": "^1.0.4", "marge": "^1.0.1", "memoize-one": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index 4738925e44dfbc..8a8147bd25aef2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20250,10 +20250,10 @@ mapbox-gl-draw-rectangle-mode@^1.0.4: resolved "https://registry.yarnpkg.com/mapbox-gl-draw-rectangle-mode/-/mapbox-gl-draw-rectangle-mode-1.0.4.tgz#42987d68872a5fb5cc5d76d3375ee20cd8bab8f7" integrity sha512-BdF6nwEK2p8n9LQoMPzBO8LhddW1fe+d5vK8HQIei+4VcRnUbKNsEj7Z15FsJxCHzsc2BQKXbESx5GaE8x0imQ== -mapbox-gl@^1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-1.12.0.tgz#7d1c73b1153d7ee219d30d80728d7df079bc7c05" - integrity sha512-B3URR4qY9R/Bx+DKqP8qmGCai8IOZYMSZF7ZSvcCZaYTaOYhQQi8ErTEDZtFMOR0ZPj7HFWOkkhl5SqvDfpJpA== +mapbox-gl@1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-1.13.1.tgz#322efe75ab4c764fc4c776da1506aad58d5a5b9d" + integrity sha512-GSyubcoSF5MyaP8z+DasLu5v7KmDK2pp4S5+VQ5WdVQUOaAqQY4jwl4JpcdNho3uWm2bIKs7x1l7q3ynGmW60g== dependencies: "@mapbox/geojson-rewind" "^0.5.0" "@mapbox/geojson-types" "^1.0.2" From 99a60caf81ea043b1e0e67d678a2ea03fd58b060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 19 Feb 2021 11:15:40 -0500 Subject: [PATCH 34/43] [APM] Bug: Service overview - Sparkline loading state icons has changed (#91884) * adjusting icon size * Updating color --- .../components/shared/charts/spark_plot/index.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx index 36c499f9e5ee40..a89d36f7089900 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx @@ -68,7 +68,16 @@ export function SparkPlot({ {!series || series.every((point) => point.y === null) ? ( - +
    + +
    ) : ( Date: Fri, 19 Feb 2021 16:41:00 +0000 Subject: [PATCH 35/43] [Discover] Always show unmapped fields (#91735) * [Discover] Always show unmapped fields * Updating the functional test * Remove unmapped switch toggle * Some more code cleanup Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/angular/discover.js | 11 +-- .../sidebar/discover_field_search.test.tsx | 18 ----- .../sidebar/discover_field_search.tsx | 77 +++---------------- .../components/sidebar/discover_sidebar.tsx | 2 - .../discover_sidebar_responsive.test.tsx | 2 - .../sidebar/discover_sidebar_responsive.tsx | 13 +--- .../public/application/components/types.ts | 13 +--- .../embeddable/search_embeddable.ts | 7 +- .../public/saved_searches/_saved_search.ts | 3 - .../discover/public/saved_searches/types.ts | 1 - .../discover/server/saved_objects/search.ts | 1 - .../saved_objects/search_migrations.test.ts | 37 --------- .../server/saved_objects/search_migrations.ts | 19 ----- .../_indexpattern_with_unmapped_fields.ts | 29 +------ 14 files changed, 17 insertions(+), 216 deletions(-) diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 78ad40e48fd965..88747cf9e84d8c 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -675,19 +675,10 @@ function discoverController($route, $scope, Promise) { history.push('/'); }; - const showUnmappedFieldsDefaultValue = $scope.useNewFieldsApi && !!$scope.opts.savedSearch.pre712; - let showUnmappedFields = showUnmappedFieldsDefaultValue; - - const onChangeUnmappedFields = (value) => { - showUnmappedFields = value; - $scope.unmappedFieldsConfig.showUnmappedFields = value; - $scope.fetch(); - }; + const showUnmappedFields = $scope.useNewFieldsApi; $scope.unmappedFieldsConfig = { - showUnmappedFieldsDefaultValue, showUnmappedFields, - onChangeUnmappedFields, }; $scope.updateDataSource = () => { diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx index 797a6c9697c351..04562cbd26520b 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx @@ -136,22 +136,4 @@ describe('DiscoverFieldSearch', () => { popover = component.find(EuiPopover); expect(popover.prop('isOpen')).toBe(false); }); - - test('unmapped fields', () => { - const onChangeUnmappedFields = jest.fn(); - const componentProps = { - ...defaultProps, - showUnmappedFields: true, - useNewFieldsApi: false, - onChangeUnmappedFields, - }; - const component = mountComponent(componentProps); - const btn = findTestSubject(component, 'toggleFieldFilterButton'); - btn.simulate('click'); - const unmappedFieldsSwitch = findTestSubject(component, 'unmappedFieldsSwitch'); - act(() => { - unmappedFieldsSwitch.simulate('click'); - }); - expect(onChangeUnmappedFields).toHaveBeenCalledWith(false); - }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx index 8fb90bfea3a950..1e99959d77134f 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx @@ -27,8 +27,6 @@ import { EuiOutsideClickDetector, EuiFilterButton, EuiSpacer, - EuiIcon, - EuiToolTip, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -37,7 +35,6 @@ export interface State { aggregatable: string; type: string; missing: boolean; - unmappedFields: boolean; [index: string]: string | boolean; } @@ -61,31 +58,13 @@ export interface Props { * use new fields api */ useNewFieldsApi?: boolean; - - /** - * callback funtion to change the value of unmapped fields switch - * @param value new value to set - */ - onChangeUnmappedFields?: (value: boolean) => void; - - /** - * should unmapped fields switch be rendered - */ - showUnmappedFields?: boolean; } /** * Component is Discover's side bar to search of available fields * Additionally there's a button displayed that allows the user to show/hide more filter fields */ -export function DiscoverFieldSearch({ - onChange, - value, - types, - useNewFieldsApi, - showUnmappedFields, - onChangeUnmappedFields, -}: Props) { +export function DiscoverFieldSearch({ onChange, value, types, useNewFieldsApi }: Props) { const searchPlaceholder = i18n.translate('discover.fieldChooser.searchPlaceHolder', { defaultMessage: 'Search field names', }); @@ -111,7 +90,6 @@ export function DiscoverFieldSearch({ aggregatable: 'any', type: 'any', missing: true, - unmappedFields: !!showUnmappedFields, }); if (typeof value !== 'string') { @@ -181,14 +159,6 @@ export function DiscoverFieldSearch({ handleValueChange('missing', missingValue); }; - const handleUnmappedFieldsChange = (e: EuiSwitchEvent) => { - const unmappedFieldsValue = e.target.checked; - handleValueChange('unmappedFields', unmappedFieldsValue); - if (onChangeUnmappedFields) { - onChangeUnmappedFields(unmappedFieldsValue); - } - }; - const buttonContent = ( { - if (!showUnmappedFields && useNewFieldsApi) { + if (useNewFieldsApi) { return null; } return ( - {showUnmappedFields ? ( - - - - - - - - - - - ) : null} - {useNewFieldsApi ? null : ( - - )} + ); }; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index f0303553dfac0a..c0a192550e6c4a 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -205,8 +205,6 @@ export function DiscoverSidebar({ value={fieldFilter.name} types={fieldTypes} useNewFieldsApi={useNewFieldsApi} - onChangeUnmappedFields={unmappedFieldsConfig?.onChangeUnmappedFields} - showUnmappedFields={unmappedFieldsConfig?.showUnmappedFieldsDefaultValue} />
    diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx index 7b12ab5f9bcd9e..79e8caabd49307 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx @@ -137,9 +137,7 @@ describe('discover responsive sidebar', function () { }); it('renders sidebar with unmapped fields config', function () { const unmappedFieldsConfig = { - onChangeUnmappedFields: jest.fn(), showUnmappedFields: false, - showUnmappedFieldsDefaultValue: false, }; const componentProps = { ...props, unmappedFieldsConfig }; const component = mountWithIntl(); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx index b689db12969222..f0e7c71f9c970d 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx @@ -113,24 +113,13 @@ export interface DiscoverSidebarResponsiveProps { useNewFieldsApi?: boolean; /** - * an object containing properties for proper handling of unmapped fields in the UI + * an object containing properties for proper handling of unmapped fields */ unmappedFieldsConfig?: { - /** - * callback function to change the value of `showUnmappedFields` flag - * @param value new value to set - */ - onChangeUnmappedFields: (value: boolean) => void; /** * determines whether to display unmapped fields - * configurable through the switch in the UI */ showUnmappedFields: boolean; - /** - * determines if we should display an option to toggle showUnmappedFields value in the first place - * this value is not configurable through the UI - */ - showUnmappedFieldsDefaultValue: boolean; }; } diff --git a/src/plugins/discover/public/application/components/types.ts b/src/plugins/discover/public/application/components/types.ts index e276795f9ed7fd..e488f596cece85 100644 --- a/src/plugins/discover/public/application/components/types.ts +++ b/src/plugins/discover/public/application/components/types.ts @@ -159,23 +159,12 @@ export interface DiscoverProps { */ timeRange?: { from: string; to: string }; /** - * An object containing properties for proper handling of unmapped fields in the UI + * An object containing properties for unmapped fields behavior */ unmappedFieldsConfig?: { /** * determines whether to display unmapped fields - * configurable through the switch in the UI */ showUnmappedFields: boolean; - /** - * determines if we should display an option to toggle showUnmappedFields value in the first place - * this value is not configurable through the UI - */ - showUnmappedFieldsDefaultValue: boolean; - /** - * callback function to change the value of `showUnmappedFields` flag - * @param value new value to set - */ - onChangeUnmappedFields: (value: boolean) => void; }; } diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index 658734aa46cb02..2bafa239075027 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -291,7 +291,7 @@ export class SearchEmbeddable const useNewFieldsApi = !this.services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE, false); if (!this.searchScope) return; - const { searchSource, pre712 } = this.savedSearch; + const { searchSource } = this.savedSearch; // Abort any in-progress requests if (this.abortController) this.abortController.abort(); @@ -308,10 +308,7 @@ export class SearchEmbeddable ); if (useNewFieldsApi) { searchSource.removeField('fieldsFromSource'); - const fields: Record = { field: '*' }; - if (pre712) { - fields.include_unmapped = 'true'; - } + const fields: Record = { field: '*', include_unmapped: 'true' }; searchSource.setField('fields', [fields]); } else { searchSource.removeField('fields'); diff --git a/src/plugins/discover/public/saved_searches/_saved_search.ts b/src/plugins/discover/public/saved_searches/_saved_search.ts index a7b6ef49cacd21..320332ca4ace54 100644 --- a/src/plugins/discover/public/saved_searches/_saved_search.ts +++ b/src/plugins/discover/public/saved_searches/_saved_search.ts @@ -20,7 +20,6 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { grid: 'object', sort: 'keyword', version: 'integer', - pre712: 'boolean', }; // Order these fields to the top, the rest are alphabetical public static fieldOrder = ['title', 'description']; @@ -42,7 +41,6 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { grid: 'object', sort: 'keyword', version: 'integer', - pre712: 'boolean', }, searchSource: true, defaults: { @@ -52,7 +50,6 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { hits: 0, sort: [], version: 1, - pre712: false, }, }); this.showInRecentlyAccessed = true; diff --git a/src/plugins/discover/public/saved_searches/types.ts b/src/plugins/discover/public/saved_searches/types.ts index 4646744ee0ef3c..b1c7b48d696b34 100644 --- a/src/plugins/discover/public/saved_searches/types.ts +++ b/src/plugins/discover/public/saved_searches/types.ts @@ -23,7 +23,6 @@ export interface SavedSearch { save: (saveOptions: SavedObjectSaveOpts) => Promise; lastSavedTitle?: string; copyOnSave?: boolean; - pre712?: boolean; hideChart?: boolean; } export interface SavedSearchLoader { diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts index de3a2197fe0acc..b66c06db3e1200 100644 --- a/src/plugins/discover/server/saved_objects/search.ts +++ b/src/plugins/discover/server/saved_objects/search.ts @@ -45,7 +45,6 @@ export const searchSavedObjectType: SavedObjectsType = { title: { type: 'text' }, grid: { type: 'object', enabled: false }, version: { type: 'integer' }, - pre712: { type: 'boolean' }, }, }, migrations: searchMigrations as any, diff --git a/src/plugins/discover/server/saved_objects/search_migrations.test.ts b/src/plugins/discover/server/saved_objects/search_migrations.test.ts index f1dc228a9ac082..fb608c0b6f3e81 100644 --- a/src/plugins/discover/server/saved_objects/search_migrations.test.ts +++ b/src/plugins/discover/server/saved_objects/search_migrations.test.ts @@ -350,41 +350,4 @@ Object { testMigrateMatchAllQuery(migrationFn); }); }); - - describe('7.12.0', () => { - const migrationFn = searchMigrations['7.12.0']; - - describe('migrateExistingSavedSearch', () => { - it('should add a new flag to existing saved searches', () => { - const migratedDoc = migrationFn( - { - type: 'search', - attributes: { - kibanaSavedObjectMeta: {}, - }, - }, - savedObjectMigrationContext - ); - const migratedPre712Flag = migratedDoc.attributes.pre712; - - expect(migratedPre712Flag).toEqual(true); - }); - - it('should not modify a flag if it already exists', () => { - const migratedDoc = migrationFn( - { - type: 'search', - attributes: { - kibanaSavedObjectMeta: {}, - pre712: false, - }, - }, - savedObjectMigrationContext - ); - const migratedPre712Flag = migratedDoc.attributes.pre712; - - expect(migratedPre712Flag).toEqual(false); - }); - }); - }); }); diff --git a/src/plugins/discover/server/saved_objects/search_migrations.ts b/src/plugins/discover/server/saved_objects/search_migrations.ts index 72749bfd2e9cdd..feaf91409797a2 100644 --- a/src/plugins/discover/server/saved_objects/search_migrations.ts +++ b/src/plugins/discover/server/saved_objects/search_migrations.ts @@ -117,28 +117,9 @@ const migrateSearchSortToNestedArray: SavedObjectMigrationFn = (doc) = }; }; -const migrateExistingSavedSearch: SavedObjectMigrationFn = (doc) => { - if (!doc.attributes) { - return doc; - } - const pre712 = doc.attributes.pre712; - // pre712 already has a value - if (pre712 !== undefined) { - return doc; - } - return { - ...doc, - attributes: { - ...doc.attributes, - pre712: true, - }, - }; -}; - export const searchMigrations = { '6.7.2': flow(migrateMatchAllQuery), '7.0.0': flow(setNewReferences), '7.4.0': flow(migrateSearchSortToNestedArray), '7.9.3': flow(migrateMatchAllQuery), - '7.12.0': flow(migrateExistingSavedSearch), }; diff --git a/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts b/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts index 0990b3fa29f708..06933e828db7e1 100644 --- a/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts +++ b/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts @@ -12,14 +12,11 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); - const testSubjects = getService('testSubjects'); const log = getService('log'); const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); describe('index pattern with unmapped fields', () => { - const unmappedFieldsSwitchSelector = 'unmappedFieldsSwitch'; - before(async () => { await esArchiver.loadIfNeeded('unmapped_fields'); await kibanaServer.uiSettings.replace({ @@ -37,7 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.unload('unmapped_fields'); }); - it('unmapped fields do not exist on a new saved search', async () => { + it('unmapped fields exist on a new saved search', async () => { const expectedHitCount = '4'; await retry.try(async function () { expect(await PageObjects.discover.getHitCount()).to.be(expectedHitCount); @@ -46,13 +43,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // message is a mapped field expect(allFields.includes('message')).to.be(true); // sender is not a mapped field - expect(allFields.includes('sender')).to.be(false); - }); - - it('unmapped fields toggle does not exist on a new saved search', async () => { - await PageObjects.discover.openSidebarFieldFilter(); - await testSubjects.existOrFail('filterSelectionPanel'); - await testSubjects.missingOrFail('unmappedFieldsSwitch'); + expect(allFields.includes('sender')).to.be(true); }); it('unmapped fields exist on an existing saved search', async () => { @@ -66,21 +57,5 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(allFields.includes('sender')).to.be(true); expect(allFields.includes('receiver')).to.be(true); }); - - it('unmapped fields toggle exists on an existing saved search', async () => { - await PageObjects.discover.openSidebarFieldFilter(); - await testSubjects.existOrFail('filterSelectionPanel'); - await testSubjects.existOrFail(unmappedFieldsSwitchSelector); - expect(await testSubjects.isEuiSwitchChecked(unmappedFieldsSwitchSelector)).to.be(true); - }); - - it('switching unmapped fields toggle off hides unmapped fields', async () => { - await testSubjects.setEuiSwitch(unmappedFieldsSwitchSelector, 'uncheck'); - await PageObjects.discover.closeSidebarFieldFilter(); - const allFields = await PageObjects.discover.getAllFieldNames(); - expect(allFields.includes('message')).to.be(true); - expect(allFields.includes('sender')).to.be(false); - expect(allFields.includes('receiver')).to.be(false); - }); }); } From 10b1fddf356f2d871aee5bd195d76245370e1af0 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Fri, 19 Feb 2021 08:49:48 -0800 Subject: [PATCH 36/43] [Fleet] Handle long text in agent details page (#91776) * Fix #85521 * Set a minimum height for agent logs component #89831 * Truncate long integration names nicely #85404 --- .../agent_details/agent_details_integrations.tsx | 14 +++++++++----- .../agent_details/agent_details_overview.tsx | 4 +++- .../components/agent_logs/agent_logs.tsx | 9 +++++++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx index d8beabab67ef14..d71fb8be5f9cf4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx @@ -27,8 +27,7 @@ import { displayInputType, getLogsQueryByInputType } from './input_type_utils'; const StyledEuiAccordion = styled(EuiAccordion)` .ingest-integration-title-button { - padding: ${(props) => props.theme.eui.paddingSizes.m} - ${(props) => props.theme.eui.paddingSizes.m}; + padding: ${(props) => props.theme.eui.paddingSizes.m}; } &.euiAccordion-isOpen .ingest-integration-title-button { @@ -38,6 +37,10 @@ const StyledEuiAccordion = styled(EuiAccordion)` .euiTableRow:last-child .euiTableRowCell { border-bottom: none; } + + .euiIEFlexWrapFix { + min-width: 0; + } `; const CollapsablePanel: React.FC<{ id: string; title: React.ReactNode }> = ({ @@ -46,11 +49,11 @@ const CollapsablePanel: React.FC<{ id: string; title: React.ReactNode }> = ({ children, }) => { return ( - + {children} @@ -128,8 +131,9 @@ export const AgentDetailsIntegration: React.FunctionComponent<{ )}
    - + {title} - {description} + + {description} + ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx index 423467702e05af..fafe389d07b82c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx @@ -185,8 +185,6 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen [http.basePath, state.start, state.end, logStreamQuery] ); - const [logsPanelRef, { height: logPanelHeight }] = useMeasure(); - const agentVersion = agent.local_metadata?.elastic?.agent?.version; const isLogFeatureAvailable = useMemo(() => { if (!agentVersion) { @@ -199,6 +197,13 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen return semverGte(agentVersionWithPrerelease, '7.11.0'); }, [agentVersion]); + // Set absolute height on logs component (needed to render correctly in Safari) + // based on available height, or 600px, whichever is greater + const [logsPanelRef, { height: measuredlogPanelHeight }] = useMeasure(); + const logPanelHeight = useMemo(() => Math.max(measuredlogPanelHeight, 600), [ + measuredlogPanelHeight, + ]); + if (!isLogFeatureAvailable) { return ( Date: Fri, 19 Feb 2021 10:28:50 -0700 Subject: [PATCH 37/43] Unskip Search Sessions Management UI test (#90110) * Unskip Search Sessions Management UI test * more logging * logging * ci test * skip reload test * add tm task to archives used by dependent tests * --wip-- [skip ci] * revert jest affecting changes * fix search sessions archive * add pagination test * test organize * log cleanup * fix async in tests * remove obsolete test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/search/session/index.ts | 2 + .../sessions_mgmt/components/table/table.tsx | 9 +- .../dashboard/async_search/data.json | 31 ++ .../dashboard/async_search/mappings.json | 90 ++++ .../data/search_sessions/data.json.gz | Bin 1976 -> 2650 bytes .../data/search_sessions/mappings.json | 422 +++++++++--------- .../search_sessions_management_page.ts | 7 +- .../config.ts | 2 +- .../services/search_sessions.ts | 4 +- .../apps/dashboard/async_search/index.ts | 2 +- .../tests/apps/discover/index.ts | 2 +- .../search_sessions/sessions_management.ts | 149 +++++-- .../sessions_management_permissions.ts | 10 +- 13 files changed, 456 insertions(+), 274 deletions(-) diff --git a/x-pack/plugins/data_enhanced/common/search/session/index.ts b/x-pack/plugins/data_enhanced/common/search/session/index.ts index e83137308be98c..45b5c16bca9579 100644 --- a/x-pack/plugins/data_enhanced/common/search/session/index.ts +++ b/x-pack/plugins/data_enhanced/common/search/session/index.ts @@ -7,3 +7,5 @@ export * from './status'; export * from './types'; + +export const SEARCH_SESSIONS_TABLE_ID = 'searchSessionsMgmtUiTable'; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx index 6139f3ef8a847d..40ed0205d8dc93 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx @@ -9,11 +9,12 @@ import { EuiButton, EuiInMemoryTable, EuiSearchBarProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { CoreStart } from 'kibana/public'; import moment from 'moment'; -import React, { useCallback, useMemo, useRef, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import useInterval from 'react-use/lib/useInterval'; import { TableText } from '../'; import { IManagementSectionsPluginsSetup, SessionsConfigSchema } from '../..'; +import { SEARCH_SESSIONS_TABLE_ID } from '../../../../../common/search'; import { SearchSessionsMgmtAPI } from '../../lib/api'; import { getColumns } from '../../lib/get_columns'; import { UISession } from '../../types'; @@ -21,8 +22,6 @@ import { OnActionComplete } from '../actions'; import { getAppFilter } from './app_filter'; import { getStatusFilter } from './status_filter'; -const TABLE_ID = 'searchSessionsMgmtTable'; - interface Props { core: CoreStart; api: SearchSessionsMgmtAPI; @@ -107,8 +106,8 @@ export function SearchSessionsMgmtTable({ core, api, timezone, config, plugins, return ( {...props} - id={TABLE_ID} - data-test-subj={TABLE_ID} + id={SEARCH_SESSIONS_TABLE_ID} + data-test-subj={SEARCH_SESSIONS_TABLE_ID} rowProps={() => ({ 'data-test-subj': 'searchSessionsRow', })} diff --git a/x-pack/test/functional/es_archives/dashboard/async_search/data.json b/x-pack/test/functional/es_archives/dashboard/async_search/data.json index 486c73f711a6bf..90c890d0f041d3 100644 --- a/x-pack/test/functional/es_archives/dashboard/async_search/data.json +++ b/x-pack/test/functional/es_archives/dashboard/async_search/data.json @@ -243,3 +243,34 @@ } } +{ + "type": "doc", + "value": { + "id": "task:data_enhanced_search_sessions_monitor", + "index": ".kibana_task_manager_1", + "source": { + "references": [], + "task": { + "attempts": 0, + "ownerId": null, + "params": "{}", + "retryAt": "2020-11-30T15:43:39.626Z", + "runAt": "2020-11-30T15:43:08.277Z", + "scheduledAt": "2020-11-30T15:43:08.277Z", + "retryAt": null, + "schedule": { + "interval": "3s" + }, + "scope": [ + "testing" + ], + "startedAt": null, + "state": "{}", + "status": "idle", + "taskType": "search_sessions_monitor" + }, + "type": "task", + "updated_at": "2020-11-30T15:43:08.277Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/dashboard/async_search/mappings.json b/x-pack/test/functional/es_archives/dashboard/async_search/mappings.json index 210fade40c648f..ee860fe973f602 100644 --- a/x-pack/test/functional/es_archives/dashboard/async_search/mappings.json +++ b/x-pack/test/functional/es_archives/dashboard/async_search/mappings.json @@ -242,3 +242,93 @@ } } } + +{ + "type": "index", + "value": { + "aliases": { + ".kibana_task_manager": { + } + }, + "index": ".kibana_task_manager_1", + "mappings": { + "dynamic": "strict", + "properties": { + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "task": { + "properties": { + "attempts": { + "type": "integer" + }, + "ownerId": { + "type": "keyword" + }, + "params": { + "type": "text" + }, + "retryAt": { + "type": "date" + }, + "runAt": { + "type": "date" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledAt": { + "type": "date" + }, + "scope": { + "type": "keyword" + }, + "startedAt": { + "type": "date" + }, + "state": { + "type": "text" + }, + "status": { + "type": "keyword" + }, + "taskType": { + "type": "keyword" + }, + "user": { + "type": "keyword" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/data/search_sessions/data.json.gz b/x-pack/test/functional/es_archives/data/search_sessions/data.json.gz index 51e8c09f19247f979a9f1823ab5786b99cf849e4..fff020036a8e3de67513dbdb85f1bf96c850605d 100644 GIT binary patch literal 2650 zcmV-g3Z?ZQiwFoylPzEZ17u-zVJ>QOZ*BnXoNZGY*%HU!-%nxrdP$w<2j6LgnCyZP z7*w{ltn)O23nf#KSQXBknbY0V=l|;-B!BMg=C|XpySKCJ zM{f6*o!x7bOpEt_?(Ft`F+K?V3dgp!%9 zy1Ucb0d5~1G|s5Aby`QAvro}+yBpc}h}Z$|w`qemjy`?%cOzWCs*`!-a+6Qzj=0fq zoP<0qMl%S*YZm0e?<&Jj)S8bFPWc=JIz6UWNQcSqhYt#xW$<{?G2kK zxvPID6aDCBUGs&L7Afb2_KqQ8kd=ZaSdF|JD)Sk4Q3WpLK9lbEMwx)=!3IqCdcEn$Hy6?O>@Elvrm1$6U%lU|Yu|7>OM= z&RQG<4_p=7f@p1H&kd$1#D*;Aend`C&$QCyOVn;AHcifdYd5N4*gI_DY6kn(K&c=lI76^qwM2}%K)fN+QyT=_pXE$R&VAavJ|B#m z(OtLKJ?%utA3inARp;)vsD!OogW03D8psO7W4P%w1PO*xjj_|5%9t8$6>`LKcnQZ$ z48wtO9vp_;aE@WKWb4(Zj>n3g&_+E*o5eBiB4?IiD>Hm&G>`3op)UMyQ$R`t3b={$p*~wWM0jb_T zmMK~p%$SHB?7CRSl3R*F-V|n_bj-!td#;4$9yo)YBphJ`GhI+3hRqGFzJ@E^{UF&47GM4c&#MdcPn`83po+kW2j&#%W>$yvMIty!9g2zNi94`*4Y)}-gdZuhj5`TKWkns0>@Mj)t6VjQ@& zkS)e(fCZ?WCm{$oN56oCp~xy43n zZ<6|9r`1YptaH?uozR0{>!+7DCh3h$r;L2Nb<3>aU=vb-FhMcXP&+xP7=~LxC?XQT zqZP=wi@lDC23-d}Qq5oohXO-td7lmjH|Nx!*~3c~(cYmu9OT{VxY;=Boz`om6rDV_ zPh(4rC6Ka27~@dKpzzR8Ut!4@qlm`ZVz>*8!nWYf8uXo_r?p!N`diaD!AZ z4ys}VbR>a;76k&O7N55uaX-mJ_N~7dLn{!n>(DaS!PmYWi%l{KaQMqKWVeg;5|ds| z&E>o;8q9|u9N%5UH17{X6XxBU7-ys5!#)GEHfDQ7?-BA2itc%zk&l*U@HU})3_20= zPBYeBTy{DMN8s%PyRQ4;ratm>V~T$-=A~vhANf7J9}R~A+NwWxn&CR6)39^+HC)A_ zWAL&+Su{&|HZ86Q%~LXL++V;$*)~n`i`3-(>v_7*54Z2T!iU3?e3XR^zb(*K!u5I| zQ@9MYvWpYP2EzKc|6f~NG(Tg}cD5E>=fab+J?;zVz~rCU{Bw!RM^o4Pp`pfdkK;W| z-$|wB)=#tKLr4S2;V4@yd$`{^Iyl~M9$xH!{(KQNf4lflZB(24?c?U|!jKYy;0 zVMhz!I#rCjI6@(5HvMT8AOG)10JPDL)dntUr#1_8>Zw6TpBZ$M;HFuzomOy8zYWff zL+T4tx0HK3l;c~LdLNoQtX)-cbS=SZw27+8~J1| zWZ5X&;_VILO>FV@;&>}3cbks4l3x6%H9vtH-mK5$@Kb&!$BQ`eXNFr6(p8_xVZQyV zlP%s}DcQOZ*BnXn_E-cI26a<`zgHMx1Gi#`Ib0O%eKI> zvjs{*J8kJ?6k7%_j-A>vTZZX(UrElzNd*Z_vDeIjv|w46qw|-3(s1V7X*#=|QoXO~ zLF(%-HT{Ofg?PTx^e7PJ`IPvyAJC8#F%vbSB%oi#8{;|}lZ1@0D9zI%^Jfhw(Pc(h zlqT!eCG&rDy6BX+RhaMudrC6Pc)k%?`GQrq##YzG)k{+x^HJi|-dN?lwEvPm?gZ z?Dq`By|R^m%NFJD<06WK<6^>}JcPQ~^GwI?+Q{>;bd8+L(Y#B?x3TG3sAEyr>R?RA z9YSrlL$T!&8U~0rEhnzla51vpHM|EzoX>eye}mun$HIo=T95LZ^iyX_7^7Lz-)d%C zd3UAXvM7vb9PBZci5cp5-$WBxCZ9ij7SAl2&;wC+JX=Sk8mCDu+%M2Gqzl{bKng5($@y1_rywD z$d6YKw?&Ny7qi_ry9Rc9J7R-nB*}>{N=V#yU5_FX{Df7$VWc&-On4ucbV3uRCX8d4 z2$|OMO9dGG5MfHv+Q{%s`$q_~YW-%W%aNR?5C_*SPC#!)EKTEFtqn39$Ho=-cU9uJ z?bfE;l%1UBV$b%!d_j0SSFbi)_iU8=Dd&J_#N8=ut{RN++QG=H(XZ2}yeBWKncr8N z3b^EKaaq@1@`=3R14qg1s@mhXQ3v~`*O^^26u*X*!vV7mRL(6h; z=(wTlQ|g5Q4!Z;iTI&b#Ux)7w-{KE(K+eyncs#lK!#~6y-d$b~YQ7&H z3{D1kNDmns;N$)G*VF0wFh$3&&;Kp;j*^H)BpxtvOWSa?l*C>x3@O5Cc}N{S>U@k6 z-sVOwc%2k16=m|+C{36j8S)5~#g+H^4E5qd-@%q0Q2qPkWkS!mp)zHVWQQx-u$TfPFEf)u$ z=-TzG``z`c$myRX7sq@^;+)E|`E}wb>6~WiC0~ZQHk*JZ7d|gxX+}ZC|Fequ4;jlP zE%NeCIGWDHm#EF+Lz+;2qfN82%hyMPS10fG&qjNvr=#<;zek7r$NOh{!;`a(XkUZn z<|~kG8Q8OTB)M4w$rXrJk=)u$a$zfw92oFZNd79FOk>Iy7jV3F8t%w(ewwc>5IG5#c3#f@ebTE&q146TuZ|RuS9*g2Vo=^BPY0 zK8#~vcndY$^%_2x{+UqRse$4OJgX?~0>yy73XEX{!6t|5;Ap|w65bxChaRr@K z5cdG$0DuOAnt<@OCAG4r^%%zEYMbCs&Ig`W%)o%b#ZvqkSMA;6;y}E7Kx&8 zc);#|(wM3R3~z~w5K~1%$UnL5QhV2wp>?s0z(0fYG4Y68RC zq9Ft|s}whDNEB7!Sw(S36a@k_7t{oTw?#h)YA)zqL#Cz*%PM+9rY7wErh}ReFuWxy zLQr#!=A1DXwL0>|5;Ap|w65U(LqQ$=SL#354?0MKMm6A<1O1tF+e zA$Sd$nkqD_2o9N=u>YG4Y68RCq9Ft|S14XXrltzdDvCp;xpe7K!E&4snpyq#x KdKBy*oB#mBztLO( diff --git a/x-pack/test/functional/es_archives/data/search_sessions/mappings.json b/x-pack/test/functional/es_archives/data/search_sessions/mappings.json index a3a56871269dff..61305d640fe3e8 100644 --- a/x-pack/test/functional/es_archives/data/search_sessions/mappings.json +++ b/x-pack/test/functional/es_archives/data/search_sessions/mappings.json @@ -1,96 +1,8 @@ { "type": "index", "value": { - "aliases": { - ".kibana": { - } - }, - "index": ".kibana_1", + "index": ".kibana", "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "action": "6e96ac5e648f57523879661ea72525b7", - "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", - "alert": "49eb3350984bd2a162914d3776e70cfb", - "api_key_pending_invalidation": "16f515278a295f6245149ad7c5ddedb7", - "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", - "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", - "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", - "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", - "background-session": "dfd06597e582fdbbbc09f1a3615e6ce0", - "canvas-element": "7390014e1091044523666d97247392fc", - "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", - "cases": "477f214ff61acc3af26a7b7818e380c1", - "cases-comments": "8a50736330e953bca91747723a319593", - "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69", - "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", - "config": "c63748b75f39d0c54de12d12c1ccbc20", - "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", - "dashboard": "40554caf09725935e2c02e02563a2d07", - "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", - "endpoint:user-artifact-manifest": "a0d7b04ad405eed54d76e279c3727862", - "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "epm-packages": "0cbbb16506734d341a96aaed65ec6413", - "epm-packages-assets": "44621b2f6052ef966da47b7c3a00f33b", - "exception-list": "67f055ab8c10abd7b2ebfd969b836788", - "exception-list-agnostic": "67f055ab8c10abd7b2ebfd969b836788", - "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9", - "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", - "fleet-agents": "cb661e8ede2b640c42c8e5ef99db0683", - "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", - "graph-workspace": "27a94b2edcb0610c6aea54a7c56d7752", - "index-pattern": "45915a1ad866812242df474eb0479052", - "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", - "ingest-agent-policies": "8b0733cce189659593659dad8db426f0", - "ingest-outputs": "8854f34453a47e26f86a29f8f3b80b4e", - "ingest-package-policies": "c91ca97b1ff700f0fc64dc6b13d65a85", - "ingest_manager_settings": "02a03095f0e05b7a538fa801b88a217f", - "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "lens": "52346cfec69ff7b47d5f0c12361a2797", - "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", - "map": "4a05b35c3a3a58fbc72dd0202dc3487f", - "maps-telemetry": "5ef305b18111b77789afefbd36b66171", - "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "ml-job": "3bb64c31915acf93fc724af137a0891b", - "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", - "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", - "namespace": "2f4316de49999235636386fe51dc06c1", - "namespaces": "2f4316de49999235636386fe51dc06c1", - "originId": "2f4316de49999235636386fe51dc06c1", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "43012c7ebc4cb57054e0a490e4b43023", - "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", - "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", - "siem-ui-timeline": "d12c5474364d737d17252acf1dc4585c", - "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", - "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", - "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", - "spaces-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", - "tag": "83d55da58f6530f7055415717ec06474", - "telemetry": "36a616f7026dfa617d6655df850fe16d", - "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", - "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", - "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", - "url": "c7f66a0df8b1b52f17c28c4adb111105", - "visualization": "f819cf6636b75c9e76ba733a0c6ef355", - "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" - } - }, "dynamic": "strict", "properties": { "action": { @@ -302,49 +214,6 @@ "dynamic": "false", "type": "object" }, - "search-session": { - "properties": { - "appId": { - "type": "keyword" - }, - "created": { - "type": "date" - }, - "touched": { - "type": "date" - }, - "expires": { - "type": "date" - }, - "idMapping": { - "enabled": false, - "type": "object" - }, - "initialState": { - "enabled": false, - "type": "object" - }, - "name": { - "type": "keyword" - }, - "persisted": { - "type": "boolean" - }, - "restoreState": { - "enabled": false, - "type": "object" - }, - "sessionId": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "urlGeneratorId": { - "type": "keyword" - } - } - }, "canvas-element": { "dynamic": "false", "properties": { @@ -519,6 +388,13 @@ } } }, + "settings": { + "properties": { + "syncAlerts": { + "type": "boolean" + } + } + }, "status": { "type": "keyword" }, @@ -528,6 +404,9 @@ "title": { "type": "keyword" }, + "type": { + "type": "keyword" + }, "updated_at": { "type": "date" }, @@ -551,6 +430,9 @@ "alertId": { "type": "keyword" }, + "associationType": { + "type": "keyword" + }, "comment": { "type": "text" }, @@ -672,6 +554,78 @@ } } }, + "cases-connector-mappings": { + "properties": { + "mappings": { + "properties": { + "action_type": { + "type": "keyword" + }, + "source": { + "type": "keyword" + }, + "target": { + "type": "keyword" + } + } + } + } + }, + "cases-sub-case": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, "cases-user-actions": { "properties": { "action": { @@ -828,6 +782,19 @@ }, "endpoint:user-artifact-manifest": { "properties": { + "artifacts": { + "properties": { + "artifactId": { + "index": false, + "type": "keyword" + }, + "policyId": { + "index": false, + "type": "keyword" + } + }, + "type": "nested" + }, "created": { "index": false, "type": "date" @@ -838,19 +805,6 @@ "semanticVersion": { "index": false, "type": "keyword" - }, - "artifacts": { - "type": "nested", - "properties": { - "policyId": { - "type": "keyword", - "index": false - }, - "artifactId": { - "type": "keyword", - "index": false - } - } } } }, @@ -1053,12 +1007,22 @@ "type": "keyword" }, "name": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" }, "os_types": { "type": "keyword" }, "tags": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" }, "tie_breaker_id": { @@ -1179,12 +1143,22 @@ "type": "keyword" }, "name": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" }, "os_types": { "type": "keyword" }, "tags": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" }, "tie_breaker_id": { @@ -1201,10 +1175,14 @@ } } }, - "file-upload-telemetry": { + "file-upload-usage-collection-telemetry": { "properties": { - "filesUploadedTotalCount": { - "type": "long" + "file_upload": { + "properties": { + "index_creation_count": { + "type": "long" + } + } } } }, @@ -1312,9 +1290,6 @@ "policy_revision": { "type": "integer" }, - "shared_id": { - "type": "keyword" - }, "type": { "type": "keyword" }, @@ -1428,6 +1403,12 @@ "is_default": { "type": "boolean" }, + "is_default_fleet_server": { + "type": "boolean" + }, + "is_managed": { + "type": "boolean" + }, "monitoring_enabled": { "index": false, "type": "keyword" @@ -1622,6 +1603,10 @@ } } }, + "legacy-url-alias": { + "dynamic": "false", + "type": "object" + }, "lens": { "properties": { "description": { @@ -1661,6 +1646,10 @@ }, "map": { "properties": { + "bounds": { + "dynamic": "false", + "type": "object" + }, "description": { "type": "text" }, @@ -1689,47 +1678,6 @@ "dynamic": "false", "type": "object" }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "config": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "search": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "space": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, "ml-job": { "properties": { "datafeed_id": { @@ -1753,17 +1701,6 @@ } } }, - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, "monitoring-telemetry": { "properties": { "reportedClusterUuids": { @@ -1843,6 +1780,15 @@ "description": { "type": "text" }, + "grid": { + "enabled": false, + "type": "object" + }, + "hideChart": { + "doc_values": false, + "index": false, + "type": "boolean" + }, "hits": { "doc_values": false, "index": false, @@ -1856,6 +1802,9 @@ } } }, + "pre712": { + "type": "boolean" + }, "sort": { "doc_values": false, "index": false, @@ -1869,6 +1818,58 @@ } } }, + "search-session": { + "properties": { + "appId": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "expires": { + "type": "date" + }, + "idMapping": { + "enabled": false, + "type": "object" + }, + "initialState": { + "enabled": false, + "type": "object" + }, + "name": { + "type": "keyword" + }, + "persisted": { + "type": "boolean" + }, + "realmName": { + "type": "keyword" + }, + "realmType": { + "type": "keyword" + }, + "restoreState": { + "enabled": false, + "type": "object" + }, + "sessionId": { + "type": "keyword" + }, + "status": { + "type": "keyword" + }, + "touched": { + "type": "date" + }, + "urlGeneratorId": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, "search-telemetry": { "dynamic": "false", "type": "object" @@ -2192,10 +2193,14 @@ "type": "keyword" }, "sort": { + "dynamic": "false", "properties": { "columnId": { "type": "keyword" }, + "columnType": { + "type": "keyword" + }, "sortDirection": { "type": "keyword" } @@ -2389,13 +2394,6 @@ } } }, - "tsvb-validation-telemetry": { - "properties": { - "failedRequests": { - "type": "long" - } - } - }, "type": { "type": "keyword" }, @@ -2604,7 +2602,9 @@ "index": { "auto_expand_replicas": "0-1", "number_of_replicas": "0", - "number_of_shards": "1" + "number_of_shards": "1", + "priority": "10", + "refresh_interval": "1s" } } } diff --git a/x-pack/test/functional/page_objects/search_sessions_management_page.ts b/x-pack/test/functional/page_objects/search_sessions_management_page.ts index df4e99dd595d90..402569971691d9 100644 --- a/x-pack/test/functional/page_objects/search_sessions_management_page.ts +++ b/x-pack/test/functional/page_objects/search_sessions_management_page.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SEARCH_SESSIONS_TABLE_ID } from '../../../plugins/data_enhanced/common/search'; import { FtrProviderContext } from '../ftr_provider_context'; export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrProviderContext) { @@ -23,7 +24,7 @@ export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrPr }, async getList() { - const table = await testSubjects.find('searchSessionsMgmtTable'); + const table = await testSubjects.find(SEARCH_SESSIONS_TABLE_ID); const allRows = await table.findAllByTestSubject('searchSessionsRow'); return Promise.all( @@ -45,9 +46,7 @@ export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrPr reload: async () => { log.debug('management ui: reload the status'); await actionsCell.click(); - await find.clickByCssSelector( - '[data-test-subj="sessionManagementPopoverAction-reload"]' - ); + await testSubjects.click('sessionManagementPopoverAction-reload'); }, delete: async () => { log.debug('management ui: delete the session'); diff --git a/x-pack/test/send_search_to_background_integration/config.ts b/x-pack/test/send_search_to_background_integration/config.ts index cc09fe8b568e05..2763ebb63c3efb 100644 --- a/x-pack/test/send_search_to_background_integration/config.ts +++ b/x-pack/test/send_search_to_background_integration/config.ts @@ -24,8 +24,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { testFiles: [ resolve(__dirname, './tests/apps/dashboard/async_search'), resolve(__dirname, './tests/apps/discover'), - resolve(__dirname, './tests/apps/management/search_sessions'), resolve(__dirname, './tests/apps/lens'), + resolve(__dirname, './tests/apps/management/search_sessions'), ], kbnTestServer: { diff --git a/x-pack/test/send_search_to_background_integration/services/search_sessions.ts b/x-pack/test/send_search_to_background_integration/services/search_sessions.ts index bf79d35178a60d..0d03a28dfb9017 100644 --- a/x-pack/test/send_search_to_background_integration/services/search_sessions.ts +++ b/x-pack/test/send_search_to_background_integration/services/search_sessions.ts @@ -137,7 +137,9 @@ export function SearchSessionsProvider({ getService }: FtrProviderContext) { .expect(200); const { saved_objects: savedObjects } = body as SavedObjectsFindResponse; - log.debug(`Found created search sessions: ${savedObjects.map(({ id }) => id)}`); + if (savedObjects.length) { + log.debug(`Found created search sessions: ${savedObjects.map(({ id }) => id)}`); + } await Promise.all( savedObjects.map(async (so) => { log.debug(`Deleting search session: ${so.id}`); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts index 5a912117fe445d..82642a640ce479 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts @@ -13,7 +13,7 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid const PageObjects = getPageObjects(['common']); const searchSessions = getService('searchSessions'); - describe('async search', function () { + describe('Dashboard', function () { this.tags('ciGroup3'); before(async () => { diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts b/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts index 42f7560b82f4f8..f2bbdf9c9287bb 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts @@ -13,7 +13,7 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid const PageObjects = getPageObjects(['common']); const searchSessions = getService('searchSessions'); - describe('async search', function () { + describe('Discover', function () { this.tags('ciGroup3'); before(async () => { diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts index f925cfb78a8c6e..d81a7ee12f616d 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts @@ -22,13 +22,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const log = getService('log'); - // FLAKY: https://github.com/elastic/kibana/issues/89069 - describe.skip('Search sessions Management UI', () => { + describe('Search Sessions Management UI', () => { describe('New search sessions', () => { before(async () => { await PageObjects.common.navigateToApp('dashboard'); log.debug('wait for dashboard landing page'); - retry.tryForTime(10000, async () => { + await retry.tryForTime(10000, async () => { testSubjects.existOrFail('dashboardLandingPage'); }); await searchSessions.markTourDone(); @@ -51,6 +50,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.waitFor(`wait for first item to complete`, async function () { const s = await PageObjects.searchSessionsManagement.getList(); + if (!s[0]) { + log.warning(`Expected item is not in the table!`); + } else { + log.debug(`First item status: ${s[0].status}`); + } return s[0] && s[0].status === 'complete'; }); @@ -72,22 +76,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await searchSessions.expectState('restored'); }); - // NOTE: this test depends on the previous one passing - it('Reloads as new session from management', async () => { - await PageObjects.searchSessionsManagement.goTo(); - - const searchSessionList = await PageObjects.searchSessionsManagement.getList(); - - expect(searchSessionList.length).to.be(1); - await searchSessionList[0].reload(); - - // embeddable has loaded - await PageObjects.dashboard.waitForRenderComplete(); - - // new search session was completed - await searchSessions.expectState('completed'); - }); - it('Deletes a session from management', async () => { await PageObjects.searchSessionsManagement.goTo(); @@ -122,34 +110,105 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.load('data/search_sessions'); const searchSessionList = await PageObjects.searchSessionsManagement.getList(); - expect(searchSessionList.length).to.be(10); + expectSnapshot(searchSessionList.map((ss) => [ss.app, ss.name, ss.created, ss.expires])) + .toMatchInline(` + Array [ + Array [ + "graph", + "[eCommerce] Orders Test 6 ", + "16 Feb, 2021, 00:00:00", + "--", + ], + Array [ + "lens", + "[eCommerce] Orders Test 7", + "15 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "apm", + "[eCommerce] Orders Test 8", + "14 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "appSearch", + "[eCommerce] Orders Test 9", + "13 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "auditbeat", + "[eCommerce] Orders Test 10", + "12 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "code", + "[eCommerce] Orders Test 11", + "11 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "console", + "[eCommerce] Orders Test 12", + "10 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "security", + "[eCommerce] Orders Test 5 ", + "9 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "visualize", + "[eCommerce] Orders Test 4 ", + "8 Feb, 2021, 00:00:00", + "--", + ], + Array [ + "canvas", + "[eCommerce] Orders Test 3", + "7 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + ] + `); + + await esArchiver.unload('data/search_sessions'); + }); + + it('has working pagination controls', async () => { + await esArchiver.load('data/search_sessions'); - expect(searchSessionList.map((ss) => ss.created)).to.eql([ - '25 Dec, 2020, 00:00:00', - '24 Dec, 2020, 00:00:00', - '23 Dec, 2020, 00:00:00', - '22 Dec, 2020, 00:00:00', - '21 Dec, 2020, 00:00:00', - '20 Dec, 2020, 00:00:00', - '19 Dec, 2020, 00:00:00', - '18 Dec, 2020, 00:00:00', - '17 Dec, 2020, 00:00:00', - '16 Dec, 2020, 00:00:00', - ]); - - expect(searchSessionList.map((ss) => ss.expires)).to.eql([ - '--', - '--', - '--', - '23 Dec, 2020, 00:00:00', - '22 Dec, 2020, 00:00:00', - '--', - '--', - '--', - '18 Dec, 2020, 00:00:00', - '17 Dec, 2020, 00:00:00', - ]); + log.debug(`loading first page of sessions`); + const sessionListFirst = await PageObjects.searchSessionsManagement.getList(); + expect(sessionListFirst.length).to.be(10); + + await testSubjects.click('pagination-button-next'); + + const sessionListSecond = await PageObjects.searchSessionsManagement.getList(); + expect(sessionListSecond.length).to.be(2); + + expectSnapshot(sessionListSecond.map((ss) => [ss.app, ss.name, ss.created, ss.expires])) + .toMatchInline(` + Array [ + Array [ + "discover", + "[eCommerce] Orders Test 2", + "6 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "dashboard", + "[eCommerce] Revenue Dashboard", + "5 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + ] + `); await esArchiver.unload('data/search_sessions'); }); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts index 48f4156afbe82b..ad22fd2cbaf714 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts @@ -22,8 +22,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const managementMenu = getService('managementMenu'); - describe('Search sessions Management UI permissions', () => { - describe('Sessions management is not available if non of apps enable search sessions', () => { + describe('Search Sessions Management UI permissions', () => { + describe('Sessions management is not available', () => { before(async () => { await security.role.create('data_analyst', { elasticsearch: {}, @@ -56,13 +56,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.security.forceLogout(); }); - it('Sessions management is not available if non of apps enable search sessions', async () => { + it('if no apps enable search sessions', async () => { const links = await appsMenu.readLinks(); expect(links.map((link) => link.text)).to.not.contain('Stack Management'); }); }); - describe('Sessions management is available if one of apps enables search sessions', () => { + describe('Sessions management is available', () => { before(async () => { await security.role.create('data_analyst', { elasticsearch: {}, @@ -95,7 +95,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.security.forceLogout(); }); - it('Sessions management is available if one of apps enables search sessions', async () => { + it('if one app enables search sessions', async () => { const links = await appsMenu.readLinks(); expect(links.map((link) => link.text)).to.contain('Stack Management'); await PageObjects.common.navigateToApp('management'); From 85bc8b0b42d4a6d343d12b33aae71f5db42ae852 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Fri, 19 Feb 2021 12:34:01 -0500 Subject: [PATCH 38/43] [Time to Visualize] Stay in Edit Mode After Dashboard Quicksave (#91729) * Make quicksave function stay in edit mode --- .../public/application/dashboard_app.tsx | 1 + .../application/dashboard_state_manager.ts | 11 ++- .../public/application/lib/save_dashboard.ts | 6 +- .../application/listing/confirm_overlays.tsx | 47 +++++----- .../listing/dashboard_unsaved_listing.tsx | 18 ++-- .../application/top_nav/dashboard_top_nav.tsx | 27 +++--- .../application/top_nav/get_top_nav_config.ts | 54 ++++++----- .../dashboard/public/dashboard_strings.ts | 90 ++++++++++++------- .../public/top_nav_menu/top_nav_menu_data.tsx | 1 + .../public/top_nav_menu/top_nav_menu_item.tsx | 1 + .../apps/dashboard/dashboard_save.ts | 6 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 13 files changed, 156 insertions(+), 110 deletions(-) diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index f659fa002e922b..8466cf009db9df 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -294,6 +294,7 @@ export function DashboardApp({ }} viewMode={viewMode} lastDashboardId={savedDashboardId} + clearUnsavedChanges={() => setUnsavedChanges(false)} timefilter={data.query.timefilter.timefilter} onQuerySubmit={(_payload, isUpdate) => { if (isUpdate === false) { diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts index e4b2afa8a46ea3..7f3f347e6e3aec 100644 --- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts @@ -345,7 +345,7 @@ export class DashboardStateManager { /** * Resets the state back to the last saved version of the dashboard. */ - public resetState() { + public resetState(resetViewMode: boolean) { // In order to show the correct warning, we have to store the unsaved // title on the dashboard object. We should fix this at some point, but this is how all the other object // save panels work at the moment. @@ -366,9 +366,14 @@ export class DashboardStateManager { this.stateDefaults.query = this.lastSavedDashboardFilters.query; // Need to make a copy to ensure they are not overwritten. this.stateDefaults.filters = [...this.getLastSavedFilterBars()]; - this.isDirty = false; - this.stateContainer.set(this.stateDefaults); + + if (resetViewMode) { + this.stateContainer.set(this.stateDefaults); + } else { + const currentViewMode = this.stateContainer.get().viewMode; + this.stateContainer.set({ ...this.stateDefaults, viewMode: currentViewMode }); + } } /** diff --git a/src/plugins/dashboard/public/application/lib/save_dashboard.ts b/src/plugins/dashboard/public/application/lib/save_dashboard.ts index 80392f61946cd6..6913fcda4c8e2c 100644 --- a/src/plugins/dashboard/public/application/lib/save_dashboard.ts +++ b/src/plugins/dashboard/public/application/lib/save_dashboard.ts @@ -11,6 +11,8 @@ import { SavedObjectSaveOpts } from '../../services/saved_objects'; import { updateSavedDashboard } from './update_saved_dashboard'; import { DashboardStateManager } from '../dashboard_state_manager'; +export type SavedDashboardSaveOpts = SavedObjectSaveOpts & { stayInEditMode?: boolean }; + /** * Saves the dashboard. * @param toJson A custom toJson function. Used because the previous code used @@ -23,7 +25,7 @@ export function saveDashboard( toJson: (obj: any) => string, timeFilter: TimefilterContract, dashboardStateManager: DashboardStateManager, - saveOptions: SavedObjectSaveOpts + saveOptions: SavedDashboardSaveOpts ): Promise { const savedDashboard = dashboardStateManager.savedDashboard; const appState = dashboardStateManager.appState; @@ -36,7 +38,7 @@ export function saveDashboard( // reset state only when save() was successful // e.g. save() could be interrupted if title is duplicated and not confirmed dashboardStateManager.lastSavedDashboardFilters = dashboardStateManager.getFilterState(); - dashboardStateManager.resetState(); + dashboardStateManager.resetState(!saveOptions.stayInEditMode); } return id; diff --git a/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx b/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx index d302bb4216bc49..b1e9af32ccd196 100644 --- a/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx +++ b/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx @@ -18,21 +18,23 @@ import { } from '@elastic/eui'; import React from 'react'; import { OverlayStart } from '../../../../../core/public'; -import { createConfirmStrings, leaveConfirmStrings } from '../../dashboard_strings'; +import { + createConfirmStrings, + discardConfirmStrings, + leaveEditModeConfirmStrings, +} from '../../dashboard_strings'; import { toMountPoint } from '../../services/kibana_react'; -export const confirmDiscardUnsavedChanges = ( - overlays: OverlayStart, - discardCallback: () => void, - cancelButtonText = leaveConfirmStrings.getCancelButtonText() -) => +export type DiscardOrKeepSelection = 'cancel' | 'discard' | 'keep'; + +export const confirmDiscardUnsavedChanges = (overlays: OverlayStart, discardCallback: () => void) => overlays - .openConfirm(leaveConfirmStrings.getDiscardSubtitle(), { - confirmButtonText: leaveConfirmStrings.getConfirmButtonText(), - cancelButtonText, + .openConfirm(discardConfirmStrings.getDiscardSubtitle(), { + confirmButtonText: discardConfirmStrings.getDiscardConfirmButtonText(), + cancelButtonText: discardConfirmStrings.getDiscardCancelButtonText(), buttonColor: 'danger', defaultFocusedButton: EUI_MODAL_CANCEL_BUTTON, - title: leaveConfirmStrings.getDiscardTitle(), + title: discardConfirmStrings.getDiscardTitle(), }) .then((isConfirmed) => { if (isConfirmed) { @@ -40,8 +42,6 @@ export const confirmDiscardUnsavedChanges = ( } }); -export type DiscardOrKeepSelection = 'cancel' | 'discard' | 'keep'; - export const confirmDiscardOrKeepUnsavedChanges = ( overlays: OverlayStart ): Promise => { @@ -50,11 +50,13 @@ export const confirmDiscardOrKeepUnsavedChanges = ( toMountPoint( <> - {leaveConfirmStrings.getLeaveEditModeTitle()} + + {leaveEditModeConfirmStrings.getLeaveEditModeTitle()} + - {leaveConfirmStrings.getLeaveEditModeSubtitle()} + {leaveEditModeConfirmStrings.getLeaveEditModeSubtitle()} @@ -62,33 +64,34 @@ export const confirmDiscardOrKeepUnsavedChanges = ( data-test-subj="dashboardDiscardConfirmCancel" onClick={() => session.close()} > - {leaveConfirmStrings.getCancelButtonText()} + {leaveEditModeConfirmStrings.getLeaveEditModeCancelButtonText()} { session.close(); - resolve('keep'); + resolve('discard'); }} > - {leaveConfirmStrings.getKeepChangesText()} + {leaveEditModeConfirmStrings.getLeaveEditModeDiscardButtonText()} { session.close(); - resolve('discard'); + resolve('keep'); }} > - {leaveConfirmStrings.getConfirmButtonText()} + {leaveEditModeConfirmStrings.getLeaveEditModeKeepChangesText()} ), { 'data-test-subj': 'dashboardDiscardConfirmModal', + maxWidth: 550, } ); }); diff --git a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx index db50cfb638d64c..66e8b2348490a1 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx @@ -17,11 +17,7 @@ import { } from '@elastic/eui'; import React, { useCallback, useEffect, useState } from 'react'; import { DashboardSavedObject } from '../..'; -import { - createConfirmStrings, - dashboardUnsavedListingStrings, - getNewDashboardTitle, -} from '../../dashboard_strings'; +import { dashboardUnsavedListingStrings, getNewDashboardTitle } from '../../dashboard_strings'; import { useKibana } from '../../services/kibana_react'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_panel_storage'; import { DashboardAppServices, DashboardRedirect } from '../types'; @@ -136,14 +132,10 @@ export const DashboardUnsavedListing = ({ const onDiscard = useCallback( (id?: string) => { - confirmDiscardUnsavedChanges( - overlays, - () => { - dashboardPanelStorage.clearPanels(id); - refreshUnsavedDashboards(); - }, - createConfirmStrings.getCancelButtonText() - ); + confirmDiscardUnsavedChanges(overlays, () => { + dashboardPanelStorage.clearPanels(id); + refreshUnsavedDashboards(); + }); }, [overlays, refreshUnsavedDashboards, dashboardPanelStorage] ); diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 11fb7f0cb56ff4..d279a6c219c9df 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -19,12 +19,7 @@ import { openAddPanelFlyout, ViewMode, } from '../../services/embeddable'; -import { - getSavedObjectFinder, - SavedObjectSaveOpts, - SaveResult, - showSaveModal, -} from '../../services/saved_objects'; +import { getSavedObjectFinder, SaveResult, showSaveModal } from '../../services/saved_objects'; import { NavAction } from '../../types'; import { DashboardSavedObject } from '../..'; @@ -48,6 +43,7 @@ import { OverlayRef } from '../../../../../core/public'; import { getNewDashboardTitle, unsavedChangesBadge } from '../../dashboard_strings'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_panel_storage'; import { DashboardContainer } from '..'; +import { SavedDashboardSaveOpts } from '../lib/save_dashboard'; export interface DashboardTopNavState { chromeIsVisible: boolean; @@ -64,13 +60,15 @@ export interface DashboardTopNavProps { timefilter: TimefilterContract; indexPatterns: IndexPattern[]; redirectTo: DashboardRedirect; - unsavedChanges?: boolean; + unsavedChanges: boolean; + clearUnsavedChanges: () => void; lastDashboardId?: string; viewMode: ViewMode; } export function DashboardTopNav({ dashboardStateManager, + clearUnsavedChanges, dashboardContainer, lastDashboardId, unsavedChanges, @@ -98,6 +96,7 @@ export function DashboardTopNav({ } = useKibana().services; const [state, setState] = useState({ chromeIsVisible: false }); + const [isSaveInProgress, setIsSaveInProgress] = useState(false); useEffect(() => { const visibleSubscription = chrome.getIsVisible$().subscribe((chromeIsVisible) => { @@ -177,7 +176,7 @@ export function DashboardTopNav({ } function discardChanges() { - dashboardStateManager.resetState(); + dashboardStateManager.resetState(true); dashboardStateManager.clearUnsavedPanels(); // We need to do a hard reset of the timepicker. appState will not reload like @@ -222,7 +221,7 @@ export function DashboardTopNav({ * @resolved {String} - The id of the doc */ const save = useCallback( - async (saveOptions: SavedObjectSaveOpts) => { + async (saveOptions: SavedDashboardSaveOpts) => { return saveDashboard(angular.toJson, timefilter, dashboardStateManager, saveOptions) .then(function (id) { if (id) { @@ -239,7 +238,6 @@ export function DashboardTopNav({ redirectTo({ destination: 'dashboard', id, useReplace: !lastDashboardId }); } else { chrome.docTitle.change(dashboardStateManager.savedDashboard.lastSavedTitle); - dashboardStateManager.switchViewMode(ViewMode.VIEW); } } return { id }; @@ -355,7 +353,8 @@ export function DashboardTopNav({ } } - save({}).then((response: SaveResult) => { + setIsSaveInProgress(true); + save({ stayInEditMode: true }).then((response: SaveResult) => { // If the save wasn't successful, put the original values back. if (!(response as { id: string }).id) { dashboardStateManager.setTitle(currentTitle); @@ -364,10 +363,13 @@ export function DashboardTopNav({ if (savedObjectsTagging) { dashboardStateManager.setTags(currentTags); } + } else { + clearUnsavedChanges(); } + setIsSaveInProgress(false); return response; }); - }, [save, savedObjectsTagging, dashboardStateManager]); + }, [save, savedObjectsTagging, dashboardStateManager, clearUnsavedChanges]); const runClone = useCallback(() => { const currentTitle = dashboardStateManager.getTitle(); @@ -467,6 +469,7 @@ export function DashboardTopNav({ hideWriteControls: dashboardCapabilities.hideWriteControls, isNewDashboard: !savedDashboard.id, isDirty: dashboardStateManager.isDirty, + isSaveInProgress, }); const badges = unsavedChanges diff --git a/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts b/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts index 26eea1b5f718de..801ab54eb9839c 100644 --- a/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts +++ b/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { ViewMode } from '../../services/embeddable'; import { TopNavIds } from './top_nav_ids'; import { NavAction } from '../../types'; +import { TopNavMenuData } from '../../../../navigation/public'; /** * @param actions - A mapping of TopNavIds to an action function that should run when the @@ -20,7 +21,12 @@ import { NavAction } from '../../types'; export function getTopNavConfig( dashboardMode: ViewMode, actions: { [key: string]: NavAction }, - options: { hideWriteControls: boolean; isNewDashboard: boolean; isDirty: boolean } + options: { + hideWriteControls: boolean; + isNewDashboard: boolean; + isDirty: boolean; + isSaveInProgress?: boolean; + } ) { switch (dashboardMode) { case ViewMode.VIEW: @@ -36,20 +42,17 @@ export function getTopNavConfig( getEditConfig(actions[TopNavIds.ENTER_EDIT_MODE]), ]; case ViewMode.EDIT: - return options.isNewDashboard - ? [ - getOptionsConfig(actions[TopNavIds.OPTIONS]), - getShareConfig(actions[TopNavIds.SHARE]), - getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]), - getSaveConfig(actions[TopNavIds.SAVE], options.isNewDashboard), - ] - : [ - getOptionsConfig(actions[TopNavIds.OPTIONS]), - getShareConfig(actions[TopNavIds.SHARE]), - getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]), - getSaveConfig(actions[TopNavIds.SAVE]), - getQuickSave(actions[TopNavIds.QUICK_SAVE]), - ]; + const disableButton = options.isSaveInProgress; + const navItems: TopNavMenuData[] = [ + getOptionsConfig(actions[TopNavIds.OPTIONS], disableButton), + getShareConfig(actions[TopNavIds.SHARE], disableButton), + getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE], disableButton), + getSaveConfig(actions[TopNavIds.SAVE], options.isNewDashboard, disableButton), + ]; + if (!options.isNewDashboard) { + navItems.push(getQuickSave(actions[TopNavIds.QUICK_SAVE], disableButton, options.isDirty)); + } + return navItems; default: return []; } @@ -106,9 +109,12 @@ function getEditConfig(action: NavAction) { /** * @returns {kbnTopNavConfig} */ -function getQuickSave(action: NavAction) { +function getQuickSave(action: NavAction, isLoading?: boolean, isDirty?: boolean) { return { + isLoading, + disableButton: !isDirty, id: 'quick-save', + iconType: 'save', emphasize: true, label: getSaveButtonLabel(), description: i18n.translate('dashboard.topNave.saveConfigDescription', { @@ -122,10 +128,12 @@ function getQuickSave(action: NavAction) { /** * @returns {kbnTopNavConfig} */ -function getSaveConfig(action: NavAction, isNewDashboard = false) { +function getSaveConfig(action: NavAction, isNewDashboard = false, disableButton?: boolean) { return { + disableButton, id: 'save', label: isNewDashboard ? getSaveButtonLabel() : getSaveAsButtonLabel(), + iconType: isNewDashboard ? 'save' : undefined, description: i18n.translate('dashboard.topNave.saveAsConfigDescription', { defaultMessage: 'Save as a new dashboard', }), @@ -138,11 +146,12 @@ function getSaveConfig(action: NavAction, isNewDashboard = false) { /** * @returns {kbnTopNavConfig} */ -function getViewConfig(action: NavAction) { +function getViewConfig(action: NavAction, disableButton?: boolean) { return { + disableButton, id: 'cancel', label: i18n.translate('dashboard.topNave.cancelButtonAriaLabel', { - defaultMessage: 'cancel', + defaultMessage: 'Return', }), description: i18n.translate('dashboard.topNave.viewConfigDescription', { defaultMessage: 'Switch to view-only mode', @@ -172,7 +181,7 @@ function getCloneConfig(action: NavAction) { /** * @returns {kbnTopNavConfig} */ -function getShareConfig(action: NavAction | undefined) { +function getShareConfig(action: NavAction | undefined, disableButton?: boolean) { return { id: 'share', label: i18n.translate('dashboard.topNave.shareButtonAriaLabel', { @@ -184,15 +193,16 @@ function getShareConfig(action: NavAction | undefined) { testId: 'shareTopNavButton', run: action ?? (() => {}), // disable the Share button if no action specified - disableButton: !action, + disableButton: !action || disableButton, }; } /** * @returns {kbnTopNavConfig} */ -function getOptionsConfig(action: NavAction) { +function getOptionsConfig(action: NavAction, disableButton?: boolean) { return { + disableButton, id: 'options', label: i18n.translate('dashboard.topNave.optionsButtonAriaLabel', { defaultMessage: 'options', diff --git a/src/plugins/dashboard/public/dashboard_strings.ts b/src/plugins/dashboard/public/dashboard_strings.ts index dad347b176c7ef..79a59d0cfa6051 100644 --- a/src/plugins/dashboard/public/dashboard_strings.ts +++ b/src/plugins/dashboard/public/dashboard_strings.ts @@ -199,6 +199,25 @@ export const getNewDashboardTitle = () => defaultMessage: 'New Dashboard', }); +export const getDashboard60Warning = () => + i18n.translate('dashboard.urlWasRemovedInSixZeroWarningMessage', { + defaultMessage: 'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.', + }); + +export const dashboardReadonlyBadge = { + getText: () => + i18n.translate('dashboard.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + getTooltip: () => + i18n.translate('dashboard.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save dashboards', + }), +}; + +/* + Modals +*/ export const shareModalStrings = { getTopMenuCheckbox: () => i18n.translate('dashboard.embedUrlParamExtension.topMenu', { @@ -222,22 +241,6 @@ export const shareModalStrings = { }), }; -export const getDashboard60Warning = () => - i18n.translate('dashboard.urlWasRemovedInSixZeroWarningMessage', { - defaultMessage: 'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.', - }); - -export const dashboardReadonlyBadge = { - getText: () => - i18n.translate('dashboard.badge.readOnly.text', { - defaultMessage: 'Read only', - }), - getTooltip: () => - i18n.translate('dashboard.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save dashboards', - }), -}; - export const leaveConfirmStrings = { getLeaveTitle: () => i18n.translate('dashboard.appLeaveConfirmModal.unsavedChangesTitle', { @@ -247,33 +250,51 @@ export const leaveConfirmStrings = { i18n.translate('dashboard.appLeaveConfirmModal.unsavedChangesSubtitle', { defaultMessage: 'Leave Dashboard with unsaved work?', }), - getKeepChangesText: () => - i18n.translate('dashboard.appLeaveConfirmModal.keepUnsavedChangesButtonLabel', { - defaultMessage: 'Keep unsaved changes', + getLeaveCancelButtonText: () => + i18n.translate('dashboard.appLeaveConfirmModal.cancelButtonLabel', { + defaultMessage: 'Cancel', }), +}; + +export const leaveEditModeConfirmStrings = { getLeaveEditModeTitle: () => - i18n.translate('dashboard.changeViewModeConfirmModal.leaveEditMode', { - defaultMessage: 'Leave edit mode with unsaved work?', + i18n.translate('dashboard.changeViewModeConfirmModal.leaveEditModeTitle', { + defaultMessage: 'You have unsaved changes', }), getLeaveEditModeSubtitle: () => - i18n.translate('dashboard.changeViewModeConfirmModal.discardChangesOptionalDescription', { - defaultMessage: `If you discard your changes, there's no getting them back.`, + i18n.translate('dashboard.changeViewModeConfirmModal.description', { + defaultMessage: `You can keep or discard your changes on return to view mode. You can't recover discarded changes.`, + }), + getLeaveEditModeKeepChangesText: () => + i18n.translate('dashboard.changeViewModeConfirmModal.keepUnsavedChangesButtonLabel', { + defaultMessage: 'Keep changes', + }), + getLeaveEditModeDiscardButtonText: () => + i18n.translate('dashboard.changeViewModeConfirmModal.confirmButtonLabel', { + defaultMessage: 'Discard changes', + }), + getLeaveEditModeCancelButtonText: () => + i18n.translate('dashboard.changeViewModeConfirmModal.cancelButtonLabel', { + defaultMessage: 'Continue editing', }), +}; + +export const discardConfirmStrings = { getDiscardTitle: () => - i18n.translate('dashboard.changeViewModeConfirmModal.discardChangesTitle', { + i18n.translate('dashboard.discardChangesConfirmModal.discardChangesTitle', { defaultMessage: 'Discard changes to dashboard?', }), getDiscardSubtitle: () => - i18n.translate('dashboard.changeViewModeConfirmModal.discardChangesDescription', { + i18n.translate('dashboard.discardChangesConfirmModal.discardChangesDescription', { defaultMessage: `Once you discard your changes, there's no getting them back.`, }), - getConfirmButtonText: () => - i18n.translate('dashboard.changeViewModeConfirmModal.confirmButtonLabel', { + getDiscardConfirmButtonText: () => + i18n.translate('dashboard.discardChangesConfirmModal.confirmButtonLabel', { defaultMessage: 'Discard changes', }), - getCancelButtonText: () => - i18n.translate('dashboard.changeViewModeConfirmModal.cancelButtonLabel', { - defaultMessage: 'Continue editing', + getDiscardCancelButtonText: () => + i18n.translate('dashboard.discardChangesConfirmModal.cancelButtonLabel', { + defaultMessage: 'Cancel', }), }; @@ -290,13 +311,20 @@ export const createConfirmStrings = { i18n.translate('dashboard.createConfirmModal.confirmButtonLabel', { defaultMessage: 'Start over', }), - getContinueButtonText: () => leaveConfirmStrings.getCancelButtonText(), + getContinueButtonText: () => + i18n.translate('dashboard.createConfirmModal.continueButtonLabel', { + defaultMessage: 'Continue editing', + }), getCancelButtonText: () => i18n.translate('dashboard.createConfirmModal.cancelButtonLabel', { defaultMessage: 'Cancel', }), }; +/* + Error Messages +*/ + export const panelStorageErrorStrings = { getPanelsGetError: (message: string) => i18n.translate('dashboard.panelStorageError.getError', { diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx index 3a54c7ed011855..b6b056134361a7 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx @@ -20,6 +20,7 @@ export interface TopNavMenuData { disableButton?: boolean | (() => boolean); tooltip?: string | (() => string | undefined); emphasize?: boolean; + isLoading?: boolean; iconType?: string; iconSide?: EuiButtonProps['iconSide']; } diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index ec91452badf365..523bf07f828c95 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -30,6 +30,7 @@ export function TopNavMenuItem(props: TopNavMenuData) { const commonButtonProps = { isDisabled: isDisabled(), onClick: handleClick, + isLoading: props.isLoading, iconType: props.iconType, iconSide: props.iconSide, 'data-test-subj': props.testId, diff --git a/test/functional/apps/dashboard/dashboard_save.ts b/test/functional/apps/dashboard/dashboard_save.ts index d1320b064b6d1f..0a0a2fc1dd2865 100644 --- a/test/functional/apps/dashboard/dashboard_save.ts +++ b/test/functional/apps/dashboard/dashboard_save.ts @@ -130,7 +130,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickQuickSave(); await testSubjects.existOrFail('saveDashboardSuccess'); - await testSubjects.existOrFail('dashboardEditMode'); + }); + + it('Stays in edit mode after performing a quick save', async function () { + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('dashboardQuickSaveMenuItem'); }); }); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1162a9bf00c70e..16712da1d7b2ea 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -589,8 +589,6 @@ "dashboard.badge.readOnly.tooltip": "ダッシュボードを保存できません", "dashboard.changeViewModeConfirmModal.cancelButtonLabel": "編集を続行", "dashboard.changeViewModeConfirmModal.confirmButtonLabel": "変更を破棄", - "dashboard.changeViewModeConfirmModal.discardChangesDescription": "変更を破棄すると、元に戻すことはできません。", - "dashboard.changeViewModeConfirmModal.discardChangesTitle": "ダッシュボードへの変更を破棄しますか?", "dashboard.cloneModal.cloneDashboardTitleAriaLabel": "クローンダッシュボードタイトル", "dashboard.dashboardAppBreadcrumbsTitle": "ダッシュボード", "dashboard.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "ダッシュボードが読み込めません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fc658ae8ce719e..e89fc62a21db68 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -589,8 +589,6 @@ "dashboard.badge.readOnly.tooltip": "无法保存仪表板", "dashboard.changeViewModeConfirmModal.cancelButtonLabel": "继续编辑", "dashboard.changeViewModeConfirmModal.confirmButtonLabel": "放弃更改", - "dashboard.changeViewModeConfirmModal.discardChangesDescription": "放弃更改后,它们将无法恢复。", - "dashboard.changeViewModeConfirmModal.discardChangesTitle": "放弃对仪表板的更改?", "dashboard.cloneModal.cloneDashboardTitleAriaLabel": "克隆仪表板标题", "dashboard.dashboardAppBreadcrumbsTitle": "仪表板", "dashboard.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "无法加载仪表板。", From 857300b1477ac3ab6cea1988dadc9997e2780d39 Mon Sep 17 00:00:00 2001 From: Brandon Morelli Date: Fri, 19 Feb 2021 09:51:10 -0800 Subject: [PATCH 39/43] docs: update dependencies table bug (#91964) --- docs/apm/service-overview.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/apm/service-overview.asciidoc b/docs/apm/service-overview.asciidoc index 5fd214e6ce613a..36d021d64456e5 100644 --- a/docs/apm/service-overview.asciidoc +++ b/docs/apm/service-overview.asciidoc @@ -62,8 +62,8 @@ each dependency. By default, dependencies are sorted by _Impact_ to show the mos If there is a particular dependency you are interested in, click *View service map* to view the related <>. -IMPORTANT: A known issue prevents Real User Monitoring (RUM) dependencies from being shown in the -*Dependencies* table. We are working on a fix for this issue. +NOTE: Displaying dependencies for services instrumented with the Real User Monitoring (RUM) agent +requires an agent version ≥ v5.6.3. [role="screenshot"] image::apm/images/spans-dependencies.png[Span type duration and dependencies] From 405255cd9704079ede5a5c7c2274e90731de6b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 19 Feb 2021 12:55:29 -0500 Subject: [PATCH 40/43] [APM] Break down error table api removing the sparklines (#89138) * breaking error table api * shows loading state while fetching metrics * adding api tests * removing pagination from server * adding API test * refactoring * fixing license * renaming apis * fixing some stuff * addressing PR comments * adding request id * addressing PR comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Dario Gieselaar --- .../service_overview.test.tsx | 3 +- .../get_column.tsx | 94 +++++++ .../service_overview_errors_table/index.tsx | 197 ++++++--------- ...rvice_error_group_comparison_statistics.ts | 105 ++++++++ ..._service_error_group_primary_statistics.ts | 96 ++++++++ .../apm/server/routes/create_apm_api.ts | 6 +- x-pack/plugins/apm/server/routes/services.ts | 70 ++++-- .../plugins/apm/server/routes/transactions.ts | 2 +- .../test/apm_api_integration/tests/index.ts | 3 +- .../tests/service_overview/error_groups.ts | 229 ------------------ .../error_groups_comparison_statistics.snap | 133 ++++++++++ .../error_groups_comparison_statistics.ts | 110 +++++++++ .../error_groups_primary_statistics.ts | 115 +++++++++ 13 files changed, 774 insertions(+), 389 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_primary_statistics.ts delete mode 100644 x-pack/test/apm_api_integration/tests/service_overview/error_groups.ts create mode 100644 x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap create mode 100644 x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts create mode 100644 x-pack/test/apm_api_integration/tests/services/error_groups_primary_statistics.ts diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index 999718e754c619..f6ffec46f9f513 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -82,9 +82,8 @@ describe('ServiceOverview', () => { /* eslint-disable @typescript-eslint/naming-convention */ const calls = { - 'GET /api/apm/services/{serviceName}/error_groups': { + 'GET /api/apm/services/{serviceName}/error_groups/primary_statistics': { error_groups: [], - total_error_groups: 0, }, 'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics': { transactionGroups: [], diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx new file mode 100644 index 00000000000000..94913c1678d219 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiBasicTableColumn } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { asInteger } from '../../../../../common/utils/formatters'; +import { px, unit } from '../../../../style/variables'; +import { SparkPlot } from '../../../shared/charts/spark_plot'; +import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; +import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; + +type ErrorGroupPrimaryStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/primary_statistics'>; +type ErrorGroupComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics'>; + +export function getColumns({ + serviceName, + errorGroupComparisonStatistics, +}: { + serviceName: string; + errorGroupComparisonStatistics: ErrorGroupComparisonStatistics; +}): Array> { + return [ + { + field: 'name', + name: i18n.translate('xpack.apm.serviceOverview.errorsTableColumnName', { + defaultMessage: 'Name', + }), + render: (_, { name, group_id: errorGroupId }) => { + return ( + + {name} + + } + /> + ); + }, + }, + { + field: 'last_seen', + name: i18n.translate( + 'xpack.apm.serviceOverview.errorsTableColumnLastSeen', + { + defaultMessage: 'Last seen', + } + ), + render: (_, { last_seen: lastSeen }) => { + return ; + }, + width: px(unit * 9), + }, + { + field: 'occurrences', + name: i18n.translate( + 'xpack.apm.serviceOverview.errorsTableColumnOccurrences', + { + defaultMessage: 'Occurrences', + } + ), + width: px(unit * 12), + render: (_, { occurrences, group_id: errorGroupId }) => { + const timeseries = + errorGroupComparisonStatistics?.[errorGroupId]?.timeseries; + return ( + + ); + }, + }, + ]; +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index f7f5db32e986cc..109bf0483f2b0e 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -7,40 +7,26 @@ import { EuiBasicTable, - EuiBasicTableColumn, EuiFlexGroup, EuiFlexItem, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { orderBy } from 'lodash'; import React, { useState } from 'react'; -import { asInteger } from '../../../../../common/utils/formatters'; +import uuid from 'uuid'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; -import { px, unit } from '../../../../style/variables'; -import { SparkPlot } from '../../../shared/charts/spark_plot'; -import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; -import { TimestampTooltip } from '../../../shared/TimestampTooltip'; -import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; import { ServiceOverviewTableContainer } from '../service_overview_table_container'; +import { getColumns } from './get_column'; interface Props { serviceName: string; } -interface ErrorGroupItem { - name: string; - last_seen: number; - group_id: string; - occurrences: { - value: number; - timeseries: Array<{ x: number; y: number }> | null; - }; -} - type SortDirection = 'asc' | 'desc'; type SortField = 'name' | 'last_seen' | 'occurrences'; @@ -50,6 +36,11 @@ const DEFAULT_SORT = { field: 'occurrences' as const, }; +const INITIAL_STATE = { + items: [], + requestId: undefined, +}; + export function ServiceOverviewErrorsTable({ serviceName }: Props) { const { urlParams: { environment, start, end }, @@ -67,88 +58,16 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { sort: DEFAULT_SORT, }); - const columns: Array> = [ - { - field: 'name', - name: i18n.translate('xpack.apm.serviceOverview.errorsTableColumnName', { - defaultMessage: 'Name', - }), - render: (_, { name, group_id: errorGroupId }) => { - return ( - - {name} - - } - /> - ); - }, - }, - { - field: 'last_seen', - name: i18n.translate( - 'xpack.apm.serviceOverview.errorsTableColumnLastSeen', - { - defaultMessage: 'Last seen', - } - ), - render: (_, { last_seen: lastSeen }) => { - return ; - }, - width: px(unit * 9), - }, - { - field: 'occurrences', - name: i18n.translate( - 'xpack.apm.serviceOverview.errorsTableColumnOccurrences', - { - defaultMessage: 'Occurrences', - } - ), - width: px(unit * 12), - render: (_, { occurrences }) => { - return ( - - ); - }, - }, - ]; + const { pageIndex, sort } = tableOptions; - const { - data = { - totalItemCount: 0, - items: [], - tableOptions: { - pageIndex: 0, - sort: DEFAULT_SORT, - }, - }, - status, - } = useFetcher( + const { data = INITIAL_STATE, status } = useFetcher( (callApmApi) => { if (!start || !end || !transactionType) { return; } - return callApmApi({ - endpoint: 'GET /api/apm/services/{serviceName}/error_groups', + endpoint: + 'GET /api/apm/services/{serviceName}/error_groups/primary_statistics', params: { path: { serviceName }, query: { @@ -156,46 +75,68 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { start, end, uiFilters: JSON.stringify(uiFilters), - size: PAGE_SIZE, - numBuckets: 20, - pageIndex: tableOptions.pageIndex, - sortField: tableOptions.sort.field, - sortDirection: tableOptions.sort.direction, transactionType, }, }, }).then((response) => { return { + requestId: uuid(), items: response.error_groups, - totalItemCount: response.total_error_groups, - tableOptions: { - pageIndex: tableOptions.pageIndex, - sort: { - field: tableOptions.sort.field, - direction: tableOptions.sort.direction, - }, - }, }; }); }, - [ - environment, - start, - end, - serviceName, - uiFilters, - tableOptions.pageIndex, - tableOptions.sort.field, - tableOptions.sort.direction, - transactionType, - ] + [environment, start, end, serviceName, uiFilters, transactionType] ); - const { + const { requestId, items } = data; + const currentPageErrorGroups = orderBy( items, - totalItemCount, - tableOptions: { pageIndex, sort }, - } = data; + sort.field, + sort.direction + ).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE); + + const groupIds = JSON.stringify( + currentPageErrorGroups.map(({ group_id: groupId }) => groupId).sort() + ); + const { + data: errorGroupComparisonStatistics, + status: errorGroupComparisonStatisticsStatus, + } = useFetcher( + (callApmApi) => { + if ( + requestId && + currentPageErrorGroups.length && + start && + end && + transactionType + ) { + return callApmApi({ + endpoint: + 'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics', + params: { + path: { serviceName }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + numBuckets: 20, + transactionType, + groupIds, + }, + }, + }); + } + }, + // only fetches agg results when requestId or group ids change + // eslint-disable-next-line react-hooks/exhaustive-deps + [requestId, groupIds], + { preservePreviousData: false } + ); + + const columns = getColumns({ + serviceName, + errorGroupComparisonStatistics: errorGroupComparisonStatistics ?? {}, + }); return ( @@ -228,15 +169,18 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { > diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts new file mode 100644 index 00000000000000..3655fa513dfb42 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { keyBy } from 'lodash'; +import { + ERROR_GROUP_ID, + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; +import { withApmSpan } from '../../../utils/with_apm_span'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; + +export async function getServiceErrorGroupComparisonStatistics({ + serviceName, + setup, + numBuckets, + transactionType, + groupIds, + environment, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + numBuckets: number; + transactionType: string; + groupIds: string[]; + environment?: string; +}) { + return withApmSpan( + 'get_service_error_group_comparison_statistics', + async () => { + const { apmEventClient, start, end, esFilter } = setup; + + const { intervalString } = getBucketSize({ start, end, numBuckets }); + + const timeseriesResponse = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { terms: { [ERROR_GROUP_ID]: groupIds } }, + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...esFilter, + ], + }, + }, + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID, + size: 500, + }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, + }, + }, + }, + }, + }, + }, + }, + }); + + if (!timeseriesResponse.aggregations) { + return {}; + } + + const groups = timeseriesResponse.aggregations.error_groups.buckets.map( + (bucket) => { + const groupId = bucket.key as string; + return { + groupId, + timeseries: bucket.timeseries.buckets.map((timeseriesBucket) => { + return { + x: timeseriesBucket.key, + y: timeseriesBucket.doc_count, + }; + }), + }; + } + ); + + return keyBy(groups, 'groupId'); + } + ); +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_primary_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_primary_statistics.ts new file mode 100644 index 00000000000000..e6c1c5db8f2ca8 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_primary_statistics.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ERROR_EXC_MESSAGE, + ERROR_GROUP_ID, + ERROR_LOG_MESSAGE, + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; +import { withApmSpan } from '../../../utils/with_apm_span'; +import { getErrorName } from '../../helpers/get_error_name'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; + +export function getServiceErrorGroupPrimaryStatistics({ + serviceName, + setup, + transactionType, + environment, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + transactionType: string; + environment?: string; +}) { + return withApmSpan('get_service_error_group_primary_statistics', async () => { + const { apmEventClient, start, end, esFilter } = setup; + + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...esFilter, + ], + }, + }, + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID, + size: 500, + order: { + _count: 'desc', + }, + }, + aggs: { + sample: { + top_hits: { + size: 1, + _source: [ERROR_LOG_MESSAGE, ERROR_EXC_MESSAGE, '@timestamp'], + sort: { + '@timestamp': 'desc', + }, + }, + }, + }, + }, + }, + }, + }); + + const errorGroups = + response.aggregations?.error_groups.buckets.map((bucket) => ({ + group_id: bucket.key as string, + name: + getErrorName(bucket.sample.hits.hits[0]._source) ?? + NOT_AVAILABLE_LABEL, + last_seen: new Date( + bucket.sample.hits.hits[0]?._source['@timestamp'] + ).getTime(), + occurrences: bucket.doc_count, + })) ?? []; + + return { + is_aggregation_accurate: + (response.aggregations?.error_groups.sum_other_doc_count ?? 0) === 0, + error_groups: errorGroups, + }; + }); +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 822a45fca269fd..c96e02f6c18215 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -24,7 +24,8 @@ import { serviceNodeMetadataRoute, serviceAnnotationsRoute, serviceAnnotationsCreateRoute, - serviceErrorGroupsRoute, + serviceErrorGroupsPrimaryStatisticsRoute, + serviceErrorGroupsComparisonStatisticsRoute, serviceThroughputRoute, serviceDependenciesRoute, serviceMetadataDetailsRoute, @@ -126,12 +127,13 @@ const createApmApi = () => { .add(serviceNodeMetadataRoute) .add(serviceAnnotationsRoute) .add(serviceAnnotationsCreateRoute) - .add(serviceErrorGroupsRoute) + .add(serviceErrorGroupsPrimaryStatisticsRoute) .add(serviceThroughputRoute) .add(serviceDependenciesRoute) .add(serviceMetadataDetailsRoute) .add(serviceMetadataIconsRoute) .add(serviceInstancesRoute) + .add(serviceErrorGroupsComparisonStatisticsRoute) // Agent configuration .add(getSingleAgentConfigurationRoute) diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 24c7c6e3e23d7e..2ce41f3d1e1a09 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -16,15 +16,17 @@ import { getServiceAnnotations } from '../lib/services/annotations'; import { getServices } from '../lib/services/get_services'; import { getServiceAgentName } from '../lib/services/get_service_agent_name'; import { getServiceDependencies } from '../lib/services/get_service_dependencies'; -import { getServiceErrorGroups } from '../lib/services/get_service_error_groups'; +import { getServiceErrorGroupPrimaryStatistics } from '../lib/services/get_service_error_groups/get_service_error_group_primary_statistics'; +import { getServiceErrorGroupComparisonStatistics } from '../lib/services/get_service_error_groups/get_service_error_group_comparison_statistics'; import { getServiceInstances } from '../lib/services/get_service_instances'; import { getServiceMetadataDetails } from '../lib/services/get_service_metadata_details'; import { getServiceMetadataIcons } from '../lib/services/get_service_metadata_icons'; import { getServiceNodeMetadata } from '../lib/services/get_service_node_metadata'; import { getServiceTransactionTypes } from '../lib/services/get_service_transaction_types'; import { getThroughput } from '../lib/services/get_throughput'; -import { offsetPreviousPeriodCoordinates } from '../utils/offset_previous_period_coordinate'; import { createRoute } from './create_route'; +import { offsetPreviousPeriodCoordinates } from '../utils/offset_previous_period_coordinate'; +import { jsonRt } from '../../common/runtime_types/json_rt'; import { comparisonRangeRt, environmentRt, @@ -276,8 +278,42 @@ export const serviceAnnotationsCreateRoute = createRoute({ }, }); -export const serviceErrorGroupsRoute = createRoute({ - endpoint: 'GET /api/apm/services/{serviceName}/error_groups', +export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({ + endpoint: + 'GET /api/apm/services/{serviceName}/error_groups/primary_statistics', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + query: t.intersection([ + environmentRt, + rangeRt, + uiFiltersRt, + t.type({ + transactionType: t.string, + }), + ]), + }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const { + path: { serviceName }, + query: { transactionType, environment }, + } = context.params; + return getServiceErrorGroupPrimaryStatistics({ + serviceName, + setup, + transactionType, + environment, + }); + }, +}); + +export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({ + endpoint: + 'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics', params: t.type({ path: t.type({ serviceName: t.string, @@ -287,16 +323,9 @@ export const serviceErrorGroupsRoute = createRoute({ rangeRt, uiFiltersRt, t.type({ - size: toNumberRt, numBuckets: toNumberRt, - pageIndex: toNumberRt, - sortDirection: t.union([t.literal('asc'), t.literal('desc')]), - sortField: t.union([ - t.literal('last_seen'), - t.literal('occurrences'), - t.literal('name'), - ]), transactionType: t.string, + groupIds: jsonRt.pipe(t.array(t.string)), }), ]), }), @@ -306,27 +335,16 @@ export const serviceErrorGroupsRoute = createRoute({ const { path: { serviceName }, - query: { - environment, - numBuckets, - pageIndex, - size, - sortDirection, - sortField, - transactionType, - }, + query: { environment, numBuckets, transactionType, groupIds }, } = context.params; - return getServiceErrorGroups({ + return getServiceErrorGroupComparisonStatistics({ environment, serviceName, setup, - size, numBuckets, - pageIndex, - sortDirection, - sortField, transactionType, + groupIds, }); }, }); diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index 5a4be216a817c2..960cc7f5264242 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -117,7 +117,7 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({ rangeRt, uiFiltersRt, t.type({ - transactionNames: jsonRt, + transactionNames: jsonRt.pipe(t.array(t.string)), numBuckets: toNumberRt, transactionType: t.string, latencyAggregationType: latencyAggregationTypeRt, diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 72ca22ae749ca7..3884b3ae750a5c 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -34,7 +34,6 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./service_maps/service_maps')); loadTestFile(require.resolve('./service_overview/dependencies')); - loadTestFile(require.resolve('./service_overview/error_groups')); loadTestFile(require.resolve('./service_overview/instances')); loadTestFile(require.resolve('./services/agent_name')); @@ -44,6 +43,8 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./services/throughput')); loadTestFile(require.resolve('./services/top_services')); loadTestFile(require.resolve('./services/transaction_types')); + loadTestFile(require.resolve('./services/error_groups_primary_statistics')); + loadTestFile(require.resolve('./services/error_groups_comparison_statistics')); loadTestFile(require.resolve('./settings/anomaly_detection/basic')); loadTestFile(require.resolve('./settings/anomaly_detection/no_access_user')); diff --git a/x-pack/test/apm_api_integration/tests/service_overview/error_groups.ts b/x-pack/test/apm_api_integration/tests/service_overview/error_groups.ts deleted file mode 100644 index fb7376a77382f1..00000000000000 --- a/x-pack/test/apm_api_integration/tests/service_overview/error_groups.ts +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import qs from 'querystring'; -import { pick, uniqBy } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import archives from '../../common/fixtures/es_archiver/archives_metadata'; -import { registry } from '../../common/registry'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - - const archiveName = 'apm_8.0.0'; - const { start, end } = archives[archiveName]; - - registry.when( - 'Service overview error groups when data is not loaded', - { config: 'basic', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - expect(response.status).to.be(200); - expect(response.body).to.eql({ - total_error_groups: 0, - error_groups: [], - is_aggregation_accurate: true, - }); - }); - } - ); - - registry.when( - 'Service overview error groups when data is loaded', - { config: 'basic', archives: [archiveName] }, - () => { - it('returns the correct data', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - expect(response.status).to.be(200); - - expectSnapshot(response.body.total_error_groups).toMatchInline(`5`); - - expectSnapshot(response.body.error_groups.map((group: any) => group.name)).toMatchInline(` - Array [ - "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy132[\\"revenue\\"])", - "java.io.IOException: Connection reset by peer", - "java.io.IOException: Connection reset by peer", - "Could not write JSON: Unable to find co.elastic.apm.opbeans.model.Customer with id 7173; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Unable to find co.elastic.apm.opbeans.model.Customer with id 7173 (through reference chain: co.elastic.apm.opbeans.model.Customer_$$_jvst101_3[\\"email\\"])", - "Request method 'POST' not supported", - ] - `); - - expectSnapshot(response.body.error_groups.map((group: any) => group.occurrences.value)) - .toMatchInline(` - Array [ - 5, - 3, - 2, - 1, - 1, - ] - `); - - const firstItem = response.body.error_groups[0]; - - expectSnapshot(pick(firstItem, 'group_id', 'last_seen', 'name', 'occurrences.value')) - .toMatchInline(` - Object { - "group_id": "051f95eabf120ebe2f8b0399fe3e54c5", - "last_seen": 1607437366098, - "name": "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy132[\\"revenue\\"])", - "occurrences": Object { - "value": 5, - }, - } - `); - - const visibleDataPoints = firstItem.occurrences.timeseries.filter(({ y }: any) => y > 0); - expectSnapshot(visibleDataPoints.length).toMatchInline(`4`); - }); - - it('sorts items in the correct order', async () => { - const descendingResponse = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - expect(descendingResponse.status).to.be(200); - - const descendingOccurrences = descendingResponse.body.error_groups.map( - (item: any) => item.occurrences.value - ); - - expect(descendingOccurrences).to.eql(descendingOccurrences.concat().sort().reverse()); - - const ascendingResponse = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - const ascendingOccurrences = ascendingResponse.body.error_groups.map( - (item: any) => item.occurrences.value - ); - - expect(ascendingOccurrences).to.eql(ascendingOccurrences.concat().sort().reverse()); - }); - - it('sorts items by the correct field', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'last_seen', - transactionType: 'request', - })}` - ); - - expect(response.status).to.be(200); - - const dates = response.body.error_groups.map((group: any) => group.last_seen); - - expect(dates).to.eql(dates.concat().sort().reverse()); - }); - - it('paginates through the items', async () => { - const size = 1; - - const firstPage = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - expect(firstPage.status).to.eql(200); - - const totalItems = firstPage.body.total_error_groups; - - const pages = Math.floor(totalItems / size); - - const items = await new Array(pages) - .fill(undefined) - .reduce(async (prevItemsPromise, _, pageIndex) => { - const prevItems = await prevItemsPromise; - - const thisPage = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size, - numBuckets: 20, - pageIndex, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - return prevItems.concat(thisPage.body.error_groups); - }, Promise.resolve([])); - - expect(items.length).to.eql(totalItems); - - expect(uniqBy(items, 'group_id').length).to.eql(totalItems); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap b/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap new file mode 100644 index 00000000000000..a536a6de67ff33 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap @@ -0,0 +1,133 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`APM API tests basic apm_8.0.0 Error groups comparison statistics when data is loaded returns the correct data 1`] = ` +Object { + "groupId": "051f95eabf120ebe2f8b0399fe3e54c5", + "timeseries": Array [ + Object { + "x": 1607435820000, + "y": 0, + }, + Object { + "x": 1607435880000, + "y": 0, + }, + Object { + "x": 1607435940000, + "y": 0, + }, + Object { + "x": 1607436000000, + "y": 0, + }, + Object { + "x": 1607436060000, + "y": 0, + }, + Object { + "x": 1607436120000, + "y": 0, + }, + Object { + "x": 1607436180000, + "y": 0, + }, + Object { + "x": 1607436240000, + "y": 0, + }, + Object { + "x": 1607436300000, + "y": 1, + }, + Object { + "x": 1607436360000, + "y": 0, + }, + Object { + "x": 1607436420000, + "y": 0, + }, + Object { + "x": 1607436480000, + "y": 0, + }, + Object { + "x": 1607436540000, + "y": 0, + }, + Object { + "x": 1607436600000, + "y": 1, + }, + Object { + "x": 1607436660000, + "y": 0, + }, + Object { + "x": 1607436720000, + "y": 0, + }, + Object { + "x": 1607436780000, + "y": 0, + }, + Object { + "x": 1607436840000, + "y": 0, + }, + Object { + "x": 1607436900000, + "y": 0, + }, + Object { + "x": 1607436960000, + "y": 0, + }, + Object { + "x": 1607437020000, + "y": 0, + }, + Object { + "x": 1607437080000, + "y": 0, + }, + Object { + "x": 1607437140000, + "y": 0, + }, + Object { + "x": 1607437200000, + "y": 2, + }, + Object { + "x": 1607437260000, + "y": 0, + }, + Object { + "x": 1607437320000, + "y": 1, + }, + Object { + "x": 1607437380000, + "y": 0, + }, + Object { + "x": 1607437440000, + "y": 0, + }, + Object { + "x": 1607437500000, + "y": 0, + }, + Object { + "x": 1607437560000, + "y": 0, + }, + Object { + "x": 1607437620000, + "y": 0, + }, + ], +} +`; diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts new file mode 100644 index 00000000000000..a13a76e2ddb463 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import url from 'url'; +import expect from '@kbn/expect'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; + +type ErrorGroupsComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics'>; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + const { start, end } = metadata; + const groupIds = [ + '051f95eabf120ebe2f8b0399fe3e54c5', + '3bb34b98031a19c277bf59c3db82d3f3', + 'b1c3ff13ec52de11187facf9c6a82538', + '9581687a53eac06aba50ba17cbd959c5', + '97c2eef51fec10d177ade955670a2f15', + ]; + + registry.when( + 'Error groups comparison statistics when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/comparison_statistics`, + query: { + start, + end, + uiFilters: '{}', + numBuckets: 20, + transactionType: 'request', + groupIds: JSON.stringify(groupIds), + }, + }) + ); + expect(response.status).to.be(200); + expect(response.body).to.empty(); + }); + } + ); + + registry.when( + 'Error groups comparison statistics when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + it('returns the correct data', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/comparison_statistics`, + query: { + start, + end, + uiFilters: '{}', + numBuckets: 20, + transactionType: 'request', + groupIds: JSON.stringify(groupIds), + }, + }) + ); + + expect(response.status).to.be(200); + + const errorGroupsComparisonStatistics = response.body as ErrorGroupsComparisonStatistics; + expect(Object.keys(errorGroupsComparisonStatistics).sort()).to.eql(groupIds.sort()); + + groupIds.forEach((groupId) => { + expect(errorGroupsComparisonStatistics[groupId]).not.to.be.empty(); + }); + + const errorgroupsComparisonStatistics = errorGroupsComparisonStatistics[groupIds[0]]; + expect( + errorgroupsComparisonStatistics.timeseries.map(({ y }) => isFinite(y)).length + ).to.be.greaterThan(0); + expectSnapshot(errorgroupsComparisonStatistics).toMatch(); + }); + + it('returns an empty list when requested groupIds are not available in the given time range', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/comparison_statistics`, + query: { + start, + end, + uiFilters: '{}', + numBuckets: 20, + transactionType: 'request', + groupIds: JSON.stringify(['foo']), + }, + }) + ); + + expect(response.status).to.be(200); + expect(response.body).to.empty(); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups_primary_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups_primary_statistics.ts new file mode 100644 index 00000000000000..8a334ca567f0ed --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/error_groups_primary_statistics.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import url from 'url'; +import expect from '@kbn/expect'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; + +type ErrorGroupsPrimaryStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/primary_statistics'>; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + const { start, end } = metadata; + + registry.when( + 'Error groups primary statistics when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/primary_statistics`, + query: { + start, + end, + uiFilters: '{}', + transactionType: 'request', + }, + }) + ); + + expect(response.status).to.be(200); + + expect(response.status).to.be(200); + expect(response.body.error_groups).to.empty(); + expect(response.body.is_aggregation_accurate).to.eql(true); + }); + } + ); + + registry.when( + 'Error groups primary statistics when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + it('returns the correct data', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/primary_statistics`, + query: { + start, + end, + uiFilters: '{}', + transactionType: 'request', + environment: 'production', + }, + }) + ); + + expect(response.status).to.be(200); + + const errorGroupPrimaryStatistics = response.body as ErrorGroupsPrimaryStatistics; + + expect(errorGroupPrimaryStatistics.is_aggregation_accurate).to.eql(true); + expect(errorGroupPrimaryStatistics.error_groups.length).to.be.greaterThan(0); + + expectSnapshot(errorGroupPrimaryStatistics.error_groups.map(({ name }) => name)) + .toMatchInline(` + Array [ + "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy132[\\"revenue\\"])", + "java.io.IOException: Connection reset by peer", + "java.io.IOException: Connection reset by peer", + "Could not write JSON: Unable to find co.elastic.apm.opbeans.model.Customer with id 7173; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Unable to find co.elastic.apm.opbeans.model.Customer with id 7173 (through reference chain: co.elastic.apm.opbeans.model.Customer_$$_jvst101_3[\\"email\\"])", + "Request method 'POST' not supported", + ] + `); + + const occurences = errorGroupPrimaryStatistics.error_groups.map( + ({ occurrences }) => occurrences + ); + + occurences.forEach((occurence) => expect(occurence).to.be.greaterThan(0)); + + expectSnapshot(occurences).toMatchInline(` + Array [ + 5, + 3, + 2, + 1, + 1, + ] + `); + + const firstItem = errorGroupPrimaryStatistics.error_groups[0]; + + expectSnapshot(firstItem).toMatchInline(` + Object { + "group_id": "051f95eabf120ebe2f8b0399fe3e54c5", + "last_seen": 1607437366098, + "name": "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy132[\\"revenue\\"])", + "occurrences": 5, + } + `); + }); + } + ); +} From 92301fe98d2681ba5c28e8857aaf73513df79dfc Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Fri, 19 Feb 2021 18:58:41 +0100 Subject: [PATCH 41/43] [ML] Fix event rate chart annotation position (#91899) --- .../event_rate_chart/event_rate_chart.tsx | 1 - .../charts/event_rate_chart/overlay_range.tsx | 31 ++++++------------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx index 93a3694ec8c21d..48f586bba8f41e 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx @@ -78,7 +78,6 @@ export const EventRateChart: FC = ({ = ({ - overlayKey, - eventRateChartData, - start, - end, - color, - showMarker = true, -}) => { - const maxHeight = Math.max(...eventRateChartData.map((e) => e.value)); - +export const OverlayRange: FC = ({ overlayKey, start, end, color, showMarker = true }) => { return ( <> = ({ coordinates: { x0: start, x1: end, - y0: 0, - y1: maxHeight, }, }, ]} @@ -62,16 +49,16 @@ export const OverlayRange: FC = ({ opacity: 0, }, }} + markerPosition={Position.Bottom} + hideTooltips={true} marker={ showMarker ? ( - <> -
    -
    - -
    -
    {timeFormatter(start)}
    +
    +
    +
    - +
    {timeFormatter(start)}
    +
    ) : undefined } /> From 4a1134c732048c08b38b138bcff1d2a3cf1f0334 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Fri, 19 Feb 2021 18:59:26 +0100 Subject: [PATCH 42/43] [Security Solution] Adds cypress-pipe (#91550) * introducing cypress-pipe * moves cypress pipe and promise packages to dev dependencies Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 5 +++-- x-pack/plugins/security_solution/cypress/support/index.js | 1 + x-pack/plugins/security_solution/cypress/tasks/timeline.ts | 5 +++-- x-pack/plugins/security_solution/cypress/tasks/timelines.ts | 6 ++---- x-pack/plugins/security_solution/cypress/tsconfig.json | 1 + yarn.lock | 5 +++++ 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 67263f53f28a24..8c8a866e9f214d 100644 --- a/package.json +++ b/package.json @@ -183,7 +183,6 @@ "content-disposition": "0.5.3", "core-js": "^3.6.5", "custom-event-polyfill": "^0.3.0", - "cypress-promise": "^1.1.0", "cytoscape": "^3.10.0", "cytoscape-dagre": "^2.2.2", "d3-array": "1.2.4", @@ -613,6 +612,8 @@ "cypress": "^6.2.1", "cypress-cucumber-preprocessor": "^2.5.2", "cypress-multi-reporters": "^1.4.0", + "cypress-pipe": "^2.0.0", + "cypress-promise": "^1.1.0", "d3": "3.5.17", "d3-cloud": "1.2.5", "d3-scale": "1.0.7", @@ -833,8 +834,8 @@ "val-loader": "^1.1.1", "vega": "^5.19.1", "vega-lite": "^4.17.0", - "vega-spec-injector": "^0.0.2", "vega-schema-url-parser": "^2.1.0", + "vega-spec-injector": "^0.0.2", "vega-tooltip": "^0.25.0", "venn.js": "0.2.20", "vinyl-fs": "^3.0.3", diff --git a/x-pack/plugins/security_solution/cypress/support/index.js b/x-pack/plugins/security_solution/cypress/support/index.js index 0b6cea1a9487b2..73a9f1503a47de 100644 --- a/x-pack/plugins/security_solution/cypress/support/index.js +++ b/x-pack/plugins/security_solution/cypress/support/index.js @@ -22,6 +22,7 @@ // Import commands.js using ES2015 syntax: import './commands'; +import 'cypress-pipe'; Cypress.Cookies.defaults({ preserve: 'sid', diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index c001f1fc2bc47d..ada09d9c05c087 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -177,8 +177,9 @@ export const openTimelineInspectButton = () => { }; export const openTimelineFromSettings = () => { - cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').click({ force: true }); - cy.get(OPEN_TIMELINE_ICON).click({ force: true }); + const click = ($el: Cypress.ObjectLike) => cy.wrap($el).click(); + cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').pipe(click); + cy.get(OPEN_TIMELINE_ICON).pipe(click); }; export const openTimelineTemplateFromSettings = (id: string) => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts index 5f9448a58288b0..15575f304009b8 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts @@ -20,10 +20,8 @@ export const exportTimeline = (timelineId: string) => { }; export const openTimeline = (id: string) => { - // This temporary wait here is to reduce flakeyness until we integrate cypress-pipe. Then please let us use cypress pipe. - // Ref: https://www.cypress.io/blog/2019/01/22/when-can-the-test-click/ - // Ref: https://github.com/NicholasBoll/cypress-pipe#readme - cy.get(TIMELINE(id)).should('be.visible').wait(1500).click(); + const click = ($el: Cypress.ObjectLike) => cy.wrap($el).click(); + cy.get(TIMELINE(id)).should('be.visible').pipe(click); }; export const waitForTimelinesPanelToBeLoaded = () => { diff --git a/x-pack/plugins/security_solution/cypress/tsconfig.json b/x-pack/plugins/security_solution/cypress/tsconfig.json index d0669ccb782818..270d877a362a6d 100644 --- a/x-pack/plugins/security_solution/cypress/tsconfig.json +++ b/x-pack/plugins/security_solution/cypress/tsconfig.json @@ -8,6 +8,7 @@ "tsBuildInfoFile": "../../../../build/tsbuildinfo/security_solution/cypress", "types": [ "cypress", + "cypress-pipe", "node" ], "resolveJsonModule": true, diff --git a/yarn.lock b/yarn.lock index 8a8147bd25aef2..e2c6ba8d320e60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11680,6 +11680,11 @@ cypress-multi-reporters@^1.4.0: debug "^4.1.1" lodash "^4.17.15" +cypress-pipe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cypress-pipe/-/cypress-pipe-2.0.0.tgz#577df7a70a8603d89a96dfe4092a605962181af8" + integrity sha512-KW9s+bz4tFLucH3rBGfjW+Q12n7S4QpUSSyxiGrgPOfoHlbYWzAGB3H26MO0VTojqf9NVvfd5Kt0MH5XMgbfyg== + cypress-promise@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/cypress-promise/-/cypress-promise-1.1.0.tgz#f2d66965945fe198431aaf692d5157cea9d47b25" From 4e2601d9c98a4cbf8b215d79ddadbf377e565cca Mon Sep 17 00:00:00 2001 From: Pete Hampton Date: Fri, 19 Feb 2021 18:10:15 +0000 Subject: [PATCH 43/43] [7.12][Telemetry] Add missing fields for security telemetry (#91920) Co-authored-by: Thiago Souza --- .../server/lib/telemetry/sender.test.ts | 16 ++++ .../server/lib/telemetry/sender.ts | 80 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index 56e2f9c7c73043..d5edd4678a9a22 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -34,6 +34,11 @@ describe('TelemetryEventsSender', () => { agent: { name: 'test', }, + rule: { + id: 'X', + name: 'Y', + ruleset: 'Z', + }, file: { size: 3, path: 'X', @@ -47,6 +52,9 @@ describe('TelemetryEventsSender', () => { malware_classification: { key1: 'X', }, + malware_signature: { + key1: 'X', + }, quarantine_result: true, quarantine_message: 'this file is bad', something_else: 'nope', @@ -70,6 +78,11 @@ describe('TelemetryEventsSender', () => { agent: { name: 'test', }, + rule: { + id: 'X', + name: 'Y', + ruleset: 'Z', + }, file: { size: 3, path: 'X', @@ -81,6 +94,9 @@ describe('TelemetryEventsSender', () => { malware_classification: { key1: 'X', }, + malware_signature: { + key1: 'X', + }, quarantine_result: true, quarantine_message: 'this file is bad', }, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index a18604fb92a40a..3ee18a84e11333 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -296,16 +296,20 @@ interface AllowlistFields { // Allow list for the data we include in the events. True means that it is deep-cloned // blindly. Object contents means that we only copy the fields that appear explicitly in // the sub-object. +/* eslint-disable @typescript-eslint/naming-convention */ const allowlistEventFields: AllowlistFields = { '@timestamp': true, agent: true, Endpoint: true, + Memory_protection: true, Ransomware: true, data_stream: true, ecs: true, elastic: true, event: true, rule: { + id: true, + name: true, ruleset: true, }, file: { @@ -320,6 +324,7 @@ const allowlistEventFields: AllowlistFields = { Ext: { code_signature: true, malware_classification: true, + malware_signature: true, quarantine_result: true, quarantine_message: true, }, @@ -335,7 +340,12 @@ const allowlistEventFields: AllowlistFields = { pid: true, uptime: true, Ext: { + architecture: true, code_signature: true, + dll: true, + token: { + integrity_level_name: true, + }, }, parent: { name: true, @@ -343,12 +353,82 @@ const allowlistEventFields: AllowlistFields = { command_line: true, hash: true, Ext: { + architecture: true, code_signature: true, + dll: true, + token: { + integrity_level_name: true, + }, }, uptime: true, pid: true, ppid: true, }, + Target: { + process: { + Ext: { + architecture: true, + code_signature: true, + dll: true, + token: { + integrity_level_name: true, + }, + }, + parent: { + process: { + Ext: { + architecture: true, + code_signature: true, + dll: true, + token: { + integrity_level_name: true, + }, + }, + }, + }, + thread: { + Ext: { + call_stack: true, + start_address: true, + start_address_details: { + address_offset: true, + allocation_base: true, + allocation_protection: true, + allocation_size: true, + allocation_type: true, + base_address: true, + bytes_start_address: true, + compressed_bytes: true, + dest_bytes: true, + dest_bytes_disasm: true, + dest_bytes_disasm_hash: true, + pe: { + Ext: { + legal_copyright: true, + product_version: true, + code_signature: { + status: true, + subject_name: true, + trusted: true, + }, + company: true, + description: true, + file_version: true, + imphash: true, + original_file_name: true, + product: true, + }, + }, + pe_detected: true, + region_protection: true, + region_size: true, + region_state: true, + strings: true, + }, + }, + }, + }, + }, token: { integrity_level_name: true, },