diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index e40cc584dc376..fa1e141be93ea 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -13,11 +13,7 @@ stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a ]) { parallel([ 'kibana-intake-agent': { - withEnv([ - 'NODE_ENV=test' // Needed for jest tests only - ]) { - kibanaPipeline.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh')() - } + kibanaPipeline.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh')() }, 'x-pack-intake-agent': { withEnv([ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 56db8d3793f57..b924c7a1a2c29 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -54,6 +54,7 @@ /src/plugins/ui_actions/ @elastic/kibana-app-arch /src/plugins/visualizations/ @elastic/kibana-app-arch /x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch +/x-pack/plugins/drilldowns/ @elastic/kibana-app-arch # APM /x-pack/legacy/plugins/apm/ @elastic/apm-ui @@ -75,6 +76,7 @@ /x-pack/plugins/ingest_manager/ @elastic/ingest /x-pack/legacy/plugins/ingest_manager/ @elastic/ingest /x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest +/x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui # Machine Learning /x-pack/legacy/plugins/ml/ @elastic/ml-ui @@ -177,6 +179,8 @@ /x-pack/legacy/plugins/rollup/ @elastic/es-ui /x-pack/plugins/searchprofiler/ @elastic/es-ui /x-pack/legacy/plugins/snapshot_restore/ @elastic/es-ui +/x-pack/legacy/plugins/upgrade_assistant/ @elastic/es-ui +/x-pack/plugins/upgrade_assistant/ @elastic/es-ui /x-pack/plugins/watcher/ @elastic/es-ui # Endpoint @@ -184,3 +188,10 @@ /x-pack/test/api_integration/apis/endpoint/ @elastic/endpoint-app-team /x-pack/test/functional/apps/endpoint/ @elastic/endpoint-app-team /x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team + +# SIEM +/x-pack/legacy/plugins/siem/ @elastic/siem +/x-pack/plugins/siem/ @elastic/siem +/x-pack/test/detection_engine_api_integration @elastic/siem +/x-pack/test/api_integration/apis/siem @elastic/siem +/x-pack/plugins/case @elastic/siem diff --git a/docs/apm/api.asciidoc b/docs/apm/api.asciidoc new file mode 100644 index 0000000000000..b520cc46bef8d --- /dev/null +++ b/docs/apm/api.asciidoc @@ -0,0 +1,260 @@ +[role="xpack"] +[[apm-api]] +== API + +Some APM app features are provided via a REST API: + +* <> + +TIP: Kibana provides additional <>, +and general information on <>. + +//// +******************************************************* +//// + +[[agent-config-api]] +=== Agent Configuration API + +The Agent configuration API allows you to fine-tune your APM agent configuration, +without needing to redeploy your application. + +The following Agent configuration APIs are available: + +* <> to create or update an Agent configuration +* <> to delete an Agent configuration. +* <> to list all Agent configurations. +* <> to search for an Agent configuration. + +//// +******************************************************* +//// + +[[apm-update-config]] +==== Create or update configuration + +[[apm-update-config-req]] +===== Request + +`PUT /api/apm/settings/agent-configuration` + +[[apm-update-config-req-body]] +===== Request body + +`service`:: +(required, object) Service identifying the configuration to create or update. + +`name` ::: + (required, string) Name of service + +`environment` ::: + (optional, string) Environment of service + +`settings`:: +(required) Key/value object with settings and their corresponding value. + +`agent_name`:: +(optional) The agent name is used by the UI to determine which settings to display. + + +[[apm-update-config-example]] +===== Example + +[source,console] +-------------------------------------------------- +PUT /api/apm/settings/agent-configuration +{ + "service" : { + "name" : "frontend", + "environment" : "production" + }, + "settings" : { + "transaction_sample_rate" : 0.4, + "capture_body" : "off", + "transaction_max_spans" : 500 + }, + "agent_name": "nodejs" +} +-------------------------------------------------- + +//// +******************************************************* +//// + + +[[apm-delete-config]] +==== Delete configuration + +[[apm-delete-config-req]] +===== Request + +`DELETE /api/apm/settings/agent-configuration` + +[[apm-delete-config-req-body]] +===== Request body +`service`:: +(required, object) Service identifying the configuration to delete + +`name` ::: + (required, string) Name of service + +`environment` ::: + (optional, string) Environment of service + + +[[apm-delete-config-example]] +===== Example + +[source,console] +-------------------------------------------------- +DELETE /api/apm/settings/agent-configuration +{ + "service" : { + "name" : "frontend", + "environment": "production" + } +} +-------------------------------------------------- + +//// +******************************************************* +//// + + +[[apm-list-config]] +==== List configuration + + +[[apm-list-config-req]] +===== Request + +`GET /api/apm/settings/agent-configuration` + +[[apm-list-config-body]] +===== Response body + +[source,js] +-------------------------------------------------- +[ + { + "agent_name": "go", + "service": { + "name": "opbeans-go", + "environment": "production" + }, + "settings": { + "transaction_sample_rate": 1, + "capture_body": "off", + "transaction_max_spans": 200 + }, + "@timestamp": 1581934104843, + "applied_by_agent": false, + "etag": "1e58c178efeebae15c25c539da740d21dee422fc" + }, + { + "agent_name": "go", + "service": { + "name": "opbeans-go" + }, + "settings": { + "transaction_sample_rate": 1, + "capture_body": "off", + "transaction_max_spans": 300 + }, + "@timestamp": 1581934111727, + "applied_by_agent": false, + "etag": "3eed916d3db434d9fb7f039daa681c7a04539a64" + }, + { + "agent_name": "nodejs", + "service": { + "name": "frontend" + }, + "settings": { + "transaction_sample_rate": 1, + }, + "@timestamp": 1582031336265, + "applied_by_agent": false, + "etag": "5080ed25785b7b19f32713681e79f46996801a5b" + } +] +-------------------------------------------------- + +[[apm-list-config-example]] +===== Example + +[source,console] +-------------------------------------------------- +GET /api/apm/settings/agent-configuration +-------------------------------------------------- + +//// +******************************************************* +//// + + +[[apm-search-config]] +==== Search configuration + +[[apm-search-config-req]] +===== Request + +`POST /api/apm/settings/agent-configuration/search` + +[[apm-search-config-req-body]] +===== Request body + +`service`:: +(required, object) Service identifying the configuration. + +`name` ::: + (required, string) Name of service + +`environment` ::: + (optional, string) Environment of service + +`etag`:: +(required) etag is sent by the agent to indicate the etag of the last successfully applied configuration. If the etag matches an existing configuration its `applied_by_agent` property will be set to `true`. Every time a configuration is edited `applied_by_agent` is reset to `false`. + +[[apm-search-config-body]] +===== Response body + +[source,js] +-------------------------------------------------- +{ + "_index": ".apm-agent-configuration", + "_id": "CIaqXXABmQCdPphWj8EJ", + "_score": 2, + "_source": { + "agent_name": "nodejs", + "service": { + "name": "frontend" + }, + "settings": { + "transaction_sample_rate": 1, + }, + "@timestamp": 1582031336265, + "applied_by_agent": false, + "etag": "5080ed25785b7b19f32713681e79f46996801a5b" + } +} +-------------------------------------------------- + +[[apm-search-config-example]] +===== Example + +[source,console] +-------------------------------------------------- +POST /api/apm/settings/agent-configuration/search +{ + "etag" : "1e58c178efeebae15c25c539da740d21dee422fc", + "service" : { + "name" : "frontend", + "environment": "production" + } +} +-------------------------------------------------- + +//// +******************************************************* +//// diff --git a/docs/apm/index.asciidoc b/docs/apm/index.asciidoc index 7eb7278cf0358..d3f0dc5b7f11f 100644 --- a/docs/apm/index.asciidoc +++ b/docs/apm/index.asciidoc @@ -24,3 +24,5 @@ include::getting-started.asciidoc[] include::bottlenecks.asciidoc[] include::using-the-apm-ui.asciidoc[] + +include::api.asciidoc[] diff --git a/docs/development/core/public/kibana-plugin-public.ibasepath.md b/docs/development/core/public/kibana-plugin-public.ibasepath.md index ca4c4b7ad3be7..7f2070eb1fd6d 100644 --- a/docs/development/core/public/kibana-plugin-public.ibasepath.md +++ b/docs/development/core/public/kibana-plugin-public.ibasepath.md @@ -19,4 +19,5 @@ export interface IBasePath | [get](./kibana-plugin-public.ibasepath.get.md) | () => string | Gets the basePath string. | | [prepend](./kibana-plugin-public.ibasepath.prepend.md) | (url: string) => string | Prepends path with the basePath. | | [remove](./kibana-plugin-public.ibasepath.remove.md) | (url: string) => string | Removes the prepended basePath from the path. | +| [serverBasePath](./kibana-plugin-public.ibasepath.serverbasepath.md) | string | Returns the server's root basePath as configured, without any namespace prefix.See for getting the basePath value for a specific request | diff --git a/docs/development/core/public/kibana-plugin-public.ibasepath.serverbasepath.md b/docs/development/core/public/kibana-plugin-public.ibasepath.serverbasepath.md new file mode 100644 index 0000000000000..0c2b5451767c7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.ibasepath.serverbasepath.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IBasePath](./kibana-plugin-public.ibasepath.md) > [serverBasePath](./kibana-plugin-public.ibasepath.serverbasepath.md) + +## IBasePath.serverBasePath property + +Returns the server's root basePath as configured, without any namespace prefix. + +See for getting the basePath value for a specific request + +Signature: + +```typescript +readonly serverBasePath: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.ihttpfetcherror.name.md b/docs/development/core/public/kibana-plugin-public.ihttpfetcherror.name.md new file mode 100644 index 0000000000000..ba986b75503ed --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.ihttpfetcherror.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpFetchError](./kibana-plugin-public.ihttpfetcherror.md) > [name](./kibana-plugin-public.ihttpfetcherror.name.md) + +## IHttpFetchError.name property + +Signature: + +```typescript +readonly name: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.md b/docs/development/core/public/kibana-plugin-public.savedobject.md index b1bb3e267bf0e..542d7d3a063ec 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobject.md +++ b/docs/development/core/public/kibana-plugin-public.savedobject.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObject +export interface SavedObject ``` ## Properties diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.md index 5a08b3f97f429..97772112ff006 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObjectsBatchResponse +export interface SavedObjectsBatchResponse ``` ## Properties diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.md index c479e9f9f3e3f..ef83acb8fd73d 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.md @@ -7,7 +7,7 @@ Signature: ```typescript -export interface SavedObjectsBulkCreateObject extends SavedObjectsCreateOptions +export interface SavedObjectsBulkCreateObject extends SavedObjectsCreateOptions ``` ## Properties diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbulkupdateobject.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkupdateobject.md index ca0eabb265901..a57702c305f50 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsbulkupdateobject.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkupdateobject.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObjectsBulkUpdateObject +export interface SavedObjectsBulkUpdateObject ``` ## Properties diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkcreate.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkcreate.md index 391b2f57205d5..c5072bea5b50e 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkcreate.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkcreate.md @@ -9,5 +9,5 @@ Creates multiple documents at once Signature: ```typescript -bulkCreate: (objects?: SavedObjectsBulkCreateObject[], options?: SavedObjectsBulkCreateOptions) => Promise>; +bulkCreate: (objects?: SavedObjectsBulkCreateObject[], options?: SavedObjectsBulkCreateOptions) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkget.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkget.md index a54dfe72167a7..37b9f6d6c951d 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkget.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkget.md @@ -12,7 +12,7 @@ Returns an array of objects by id bulkGet: (objects?: { id: string; type: string; - }[]) => Promise>; + }[]) => Promise>; ``` ## Example diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkupdate.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkupdate.md index 94ae9bb70ccda..2f8565def3d40 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkupdate.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkupdate.md @@ -9,7 +9,7 @@ Update multiple documents at once Signature: ```typescript -bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; +bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; ``` ## Parameters @@ -20,7 +20,7 @@ bulkUpdate(objects?: SavedObjectsBulkUpdateObje Returns: -`Promise>` +`Promise>` The result of the update operation containing both failed and updated saved objects. diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.create.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.create.md index 5a7666084ea0f..ea3fbe31ca8a5 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.create.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.create.md @@ -9,5 +9,5 @@ Persists an object Signature: ```typescript -create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; +create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md index d3494045952ad..3d8005c9390b7 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md @@ -9,5 +9,5 @@ Search for objects Signature: ```typescript -find: (options: Pick) => Promise>; +find: (options: Pick) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.get.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.get.md index bddbadd3e1361..37a91f7211da5 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.get.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.get.md @@ -9,5 +9,5 @@ Fetches a single object Signature: ```typescript -get: (type: string, id: string) => Promise>; +get: (type: string, id: string) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md index 7aa17eae2da87..5c22a2a0bdd91 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md @@ -20,12 +20,12 @@ The constructor for this class is marked as internal. Third-party code should no | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [bulkCreate](./kibana-plugin-public.savedobjectsclient.bulkcreate.md) | | (objects?: SavedObjectsBulkCreateObject<SavedObjectAttributes>[], options?: SavedObjectsBulkCreateOptions) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Creates multiple documents at once | -| [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Returns an array of objects by id | -| [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | +| [bulkCreate](./kibana-plugin-public.savedobjectsclient.bulkcreate.md) | | (objects?: SavedObjectsBulkCreateObject<unknown>[], options?: SavedObjectsBulkCreateOptions) => Promise<SavedObjectsBatchResponse<unknown>> | Creates multiple documents at once | +| [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<unknown>> | Returns an array of objects by id | +| [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T = unknown>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | | [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}> | Deletes an object | -| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | -| [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | +| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T = unknown>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | +| [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T = unknown>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | ## Methods diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.update.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.update.md index 9f7e46943bbd5..d1049a75edf1f 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.update.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.update.md @@ -9,7 +9,7 @@ Updates an object Signature: ```typescript -update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; +update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.md index f60e7305fba34..31ab73464dfd9 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.md @@ -11,7 +11,7 @@ Return type of the Saved Objects `find()` method. Signature: ```typescript -export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse +export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse ``` ## Properties diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.md index 1f6de163ec17d..4906d5967f7df 100644 --- a/docs/development/core/public/kibana-plugin-public.simplesavedobject.md +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.md @@ -11,7 +11,7 @@ It provides basic functionality for creating/saving/deleting saved objects, but Signature: ```typescript -export declare class SimpleSavedObject +export declare class SimpleSavedObject ``` ## Constructors diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig._constructor_.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig._constructor_.md new file mode 100644 index 0000000000000..55e5cf74fc512 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig._constructor_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [(constructor)](./kibana-plugin-server.elasticsearchconfig._constructor_.md) + +## ElasticsearchConfig.(constructor) + +Constructs a new instance of the `ElasticsearchConfig` class + +Signature: + +```typescript +constructor(rawConfig: ElasticsearchConfigType); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| rawConfig | ElasticsearchConfigType | | + diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.apiversion.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.apiversion.md new file mode 100644 index 0000000000000..097654f1fb090 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.apiversion.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [apiVersion](./kibana-plugin-server.elasticsearchconfig.apiversion.md) + +## ElasticsearchConfig.apiVersion property + +Version of the Elasticsearch (6.7, 7.1 or `master`) client will be connecting to. + +Signature: + +```typescript +readonly apiVersion: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.customheaders.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.customheaders.md new file mode 100644 index 0000000000000..0b3998e59c5df --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.customheaders.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [customHeaders](./kibana-plugin-server.elasticsearchconfig.customheaders.md) + +## ElasticsearchConfig.customHeaders property + +Header names and values to send to Elasticsearch with every request. These headers cannot be overwritten by client-side headers and aren't affected by `requestHeadersWhitelist` configuration. + +Signature: + +```typescript +readonly customHeaders: ElasticsearchConfigType['customHeaders']; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.healthcheckdelay.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.healthcheckdelay.md new file mode 100644 index 0000000000000..b5589727d80aa --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.healthcheckdelay.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [healthCheckDelay](./kibana-plugin-server.elasticsearchconfig.healthcheckdelay.md) + +## ElasticsearchConfig.healthCheckDelay property + +The interval between health check requests Kibana sends to the Elasticsearch. + +Signature: + +```typescript +readonly healthCheckDelay: Duration; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.hosts.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.hosts.md new file mode 100644 index 0000000000000..29770ba5e0795 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.hosts.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [hosts](./kibana-plugin-server.elasticsearchconfig.hosts.md) + +## ElasticsearchConfig.hosts property + +Hosts that the client will connect to. If sniffing is enabled, this list will be used as seeds to discover the rest of your cluster. + +Signature: + +```typescript +readonly hosts: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.ignoreversionmismatch.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.ignoreversionmismatch.md new file mode 100644 index 0000000000000..42e32f920c1db --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.ignoreversionmismatch.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [ignoreVersionMismatch](./kibana-plugin-server.elasticsearchconfig.ignoreversionmismatch.md) + +## ElasticsearchConfig.ignoreVersionMismatch property + +Whether to allow kibana to connect to a non-compatible elasticsearch node. + +Signature: + +```typescript +readonly ignoreVersionMismatch: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.logqueries.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.logqueries.md new file mode 100644 index 0000000000000..64de7f6504450 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.logqueries.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [logQueries](./kibana-plugin-server.elasticsearchconfig.logqueries.md) + +## ElasticsearchConfig.logQueries property + +Specifies whether all queries to the client should be logged (status code, method, query etc.). + +Signature: + +```typescript +readonly logQueries: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.md new file mode 100644 index 0000000000000..e478dc7b966a2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.md @@ -0,0 +1,41 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) + +## ElasticsearchConfig class + +Wrapper of config schema. + +Signature: + +```typescript +export declare class ElasticsearchConfig +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(rawConfig)](./kibana-plugin-server.elasticsearchconfig._constructor_.md) | | Constructs a new instance of the ElasticsearchConfig class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [apiVersion](./kibana-plugin-server.elasticsearchconfig.apiversion.md) | | string | Version of the Elasticsearch (6.7, 7.1 or master) client will be connecting to. | +| [customHeaders](./kibana-plugin-server.elasticsearchconfig.customheaders.md) | | ElasticsearchConfigType['customHeaders'] | Header names and values to send to Elasticsearch with every request. These headers cannot be overwritten by client-side headers and aren't affected by requestHeadersWhitelist configuration. | +| [healthCheckDelay](./kibana-plugin-server.elasticsearchconfig.healthcheckdelay.md) | | Duration | The interval between health check requests Kibana sends to the Elasticsearch. | +| [hosts](./kibana-plugin-server.elasticsearchconfig.hosts.md) | | string[] | Hosts that the client will connect to. If sniffing is enabled, this list will be used as seeds to discover the rest of your cluster. | +| [ignoreVersionMismatch](./kibana-plugin-server.elasticsearchconfig.ignoreversionmismatch.md) | | boolean | Whether to allow kibana to connect to a non-compatible elasticsearch node. | +| [logQueries](./kibana-plugin-server.elasticsearchconfig.logqueries.md) | | boolean | Specifies whether all queries to the client should be logged (status code, method, query etc.). | +| [password](./kibana-plugin-server.elasticsearchconfig.password.md) | | string | If Elasticsearch is protected with basic authentication, this setting provides the password that the Kibana server uses to perform its administrative functions. | +| [pingTimeout](./kibana-plugin-server.elasticsearchconfig.pingtimeout.md) | | Duration | Timeout after which PING HTTP request will be aborted and retried. | +| [requestHeadersWhitelist](./kibana-plugin-server.elasticsearchconfig.requestheaderswhitelist.md) | | string[] | List of Kibana client-side headers to send to Elasticsearch when request scoped cluster client is used. If this is an empty array then \*no\* client-side will be sent. | +| [requestTimeout](./kibana-plugin-server.elasticsearchconfig.requesttimeout.md) | | Duration | Timeout after which HTTP request will be aborted and retried. | +| [shardTimeout](./kibana-plugin-server.elasticsearchconfig.shardtimeout.md) | | Duration | Timeout for Elasticsearch to wait for responses from shards. Set to 0 to disable. | +| [sniffInterval](./kibana-plugin-server.elasticsearchconfig.sniffinterval.md) | | false | Duration | Interval to perform a sniff operation and make sure the list of nodes is complete. If false then sniffing is disabled. | +| [sniffOnConnectionFault](./kibana-plugin-server.elasticsearchconfig.sniffonconnectionfault.md) | | boolean | Specifies whether the client should immediately sniff for a more current list of nodes when a connection dies. | +| [sniffOnStart](./kibana-plugin-server.elasticsearchconfig.sniffonstart.md) | | boolean | Specifies whether the client should attempt to detect the rest of the cluster when it is first instantiated. | +| [ssl](./kibana-plugin-server.elasticsearchconfig.ssl.md) | | Pick<SslConfigSchema, Exclude<keyof SslConfigSchema, 'certificateAuthorities' | 'keystore' | 'truststore'>> & {
certificateAuthorities?: string[];
} | Set of settings configure SSL connection between Kibana and Elasticsearch that are required when xpack.ssl.verification_mode in Elasticsearch is set to either certificate or full. | +| [username](./kibana-plugin-server.elasticsearchconfig.username.md) | | string | If Elasticsearch is protected with basic authentication, this setting provides the username that the Kibana server uses to perform its administrative functions. | + diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.password.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.password.md new file mode 100644 index 0000000000000..ffe6f75e9874d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.password.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [password](./kibana-plugin-server.elasticsearchconfig.password.md) + +## ElasticsearchConfig.password property + +If Elasticsearch is protected with basic authentication, this setting provides the password that the Kibana server uses to perform its administrative functions. + +Signature: + +```typescript +readonly password?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.pingtimeout.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.pingtimeout.md new file mode 100644 index 0000000000000..09123f0969b60 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.pingtimeout.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [pingTimeout](./kibana-plugin-server.elasticsearchconfig.pingtimeout.md) + +## ElasticsearchConfig.pingTimeout property + +Timeout after which PING HTTP request will be aborted and retried. + +Signature: + +```typescript +readonly pingTimeout: Duration; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.requestheaderswhitelist.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.requestheaderswhitelist.md new file mode 100644 index 0000000000000..eeced56e3103f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.requestheaderswhitelist.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [requestHeadersWhitelist](./kibana-plugin-server.elasticsearchconfig.requestheaderswhitelist.md) + +## ElasticsearchConfig.requestHeadersWhitelist property + +List of Kibana client-side headers to send to Elasticsearch when request scoped cluster client is used. If this is an empty array then \*no\* client-side will be sent. + +Signature: + +```typescript +readonly requestHeadersWhitelist: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.requesttimeout.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.requesttimeout.md new file mode 100644 index 0000000000000..dbd5ecb939673 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.requesttimeout.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [requestTimeout](./kibana-plugin-server.elasticsearchconfig.requesttimeout.md) + +## ElasticsearchConfig.requestTimeout property + +Timeout after which HTTP request will be aborted and retried. + +Signature: + +```typescript +readonly requestTimeout: Duration; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.shardtimeout.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.shardtimeout.md new file mode 100644 index 0000000000000..aa923042bf64f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.shardtimeout.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [shardTimeout](./kibana-plugin-server.elasticsearchconfig.shardtimeout.md) + +## ElasticsearchConfig.shardTimeout property + +Timeout for Elasticsearch to wait for responses from shards. Set to 0 to disable. + +Signature: + +```typescript +readonly shardTimeout: Duration; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.sniffinterval.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.sniffinterval.md new file mode 100644 index 0000000000000..37fd2a7439535 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.sniffinterval.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [sniffInterval](./kibana-plugin-server.elasticsearchconfig.sniffinterval.md) + +## ElasticsearchConfig.sniffInterval property + +Interval to perform a sniff operation and make sure the list of nodes is complete. If `false` then sniffing is disabled. + +Signature: + +```typescript +readonly sniffInterval: false | Duration; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.sniffonconnectionfault.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.sniffonconnectionfault.md new file mode 100644 index 0000000000000..c703be548d34b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.sniffonconnectionfault.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [sniffOnConnectionFault](./kibana-plugin-server.elasticsearchconfig.sniffonconnectionfault.md) + +## ElasticsearchConfig.sniffOnConnectionFault property + +Specifies whether the client should immediately sniff for a more current list of nodes when a connection dies. + +Signature: + +```typescript +readonly sniffOnConnectionFault: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.sniffonstart.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.sniffonstart.md new file mode 100644 index 0000000000000..26a7d9cc11a80 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.sniffonstart.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [sniffOnStart](./kibana-plugin-server.elasticsearchconfig.sniffonstart.md) + +## ElasticsearchConfig.sniffOnStart property + +Specifies whether the client should attempt to detect the rest of the cluster when it is first instantiated. + +Signature: + +```typescript +readonly sniffOnStart: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.ssl.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.ssl.md new file mode 100644 index 0000000000000..4d23c410f59fa --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.ssl.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [ssl](./kibana-plugin-server.elasticsearchconfig.ssl.md) + +## ElasticsearchConfig.ssl property + +Set of settings configure SSL connection between Kibana and Elasticsearch that are required when `xpack.ssl.verification_mode` in Elasticsearch is set to either `certificate` or `full`. + +Signature: + +```typescript +readonly ssl: Pick> & { + certificateAuthorities?: string[]; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.username.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.username.md new file mode 100644 index 0000000000000..d0098d656befb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.username.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [username](./kibana-plugin-server.elasticsearchconfig.username.md) + +## ElasticsearchConfig.username property + +If Elasticsearch is protected with basic authentication, this setting provides the username that the Kibana server uses to perform its administrative functions. + +Signature: + +```typescript +readonly username?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 482f014b226b9..9ec443d6482e8 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -19,6 +19,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [BasePath](./kibana-plugin-server.basepath.md) | Access or manipulate the Kibana base path | | [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [ClusterClient](./kibana-plugin-server.clusterclient.md). | | [CspConfig](./kibana-plugin-server.cspconfig.md) | CSP configuration for use in Kibana. | +| [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) | Wrapper of config schema. | | [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] | | [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | | [RouteValidationError](./kibana-plugin-server.routevalidationerror.md) | Error to return when the validation is not successful. | diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.md b/docs/development/core/server/kibana-plugin-server.savedobject.md index b3184fd38ad93..5ab08f4eadf2e 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobject.md +++ b/docs/development/core/server/kibana-plugin-server.savedobject.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObject +export interface SavedObject ``` ## Properties diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md index 87386b986009d..7b765055de6c1 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObjectsBulkCreateObject +export interface SavedObjectsBulkCreateObject ``` ## Properties diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkresponse.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkresponse.md index 20a1194c87eda..2ced4f4c8e1a0 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkresponse.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkresponse.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObjectsBulkResponse +export interface SavedObjectsBulkResponse ``` ## Properties diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateobject.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateobject.md index 8e4e3d761148e..67013290a629d 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateobject.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateobject.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObjectsBulkUpdateObject extends Pick +export interface SavedObjectsBulkUpdateObject extends Pick ``` ## Properties diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateresponse.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateresponse.md index 065b9df0823cd..3468991611608 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateresponse.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateresponse.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObjectsBulkUpdateResponse +export interface SavedObjectsBulkUpdateResponse ``` ## Properties diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkcreate.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkcreate.md index 40f947188de54..47da795631a3a 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkcreate.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkcreate.md @@ -9,7 +9,7 @@ Persists multiple documents batched together as a single request Signature: ```typescript -bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; +bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkget.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkget.md index c86c30d14db3b..71006e2afa3ee 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkget.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkget.md @@ -9,7 +9,7 @@ Returns an array of objects by id Signature: ```typescript -bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; +bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkupdate.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkupdate.md index 33958837ebca3..cceeb9c1ca320 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkupdate.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkupdate.md @@ -9,7 +9,7 @@ Bulk Updates multiple SavedObject at once Signature: ```typescript -bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; +bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.create.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.create.md index ddb78a57e71bc..7f6fc117937cb 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.create.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.create.md @@ -9,7 +9,7 @@ Persists a SavedObject Signature: ```typescript -create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; +create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.find.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.find.md index f72691d3ce0c8..c8804e2a97851 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.find.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.find.md @@ -9,7 +9,7 @@ Find all SavedObjects matching the search query Signature: ```typescript -find(options: SavedObjectsFindOptions): Promise>; +find(options: SavedObjectsFindOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.get.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.get.md index 3906462184d4f..b48cef25ca3d1 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.get.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.get.md @@ -9,7 +9,7 @@ Retrieves a single object Signature: ```typescript -get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; +get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.update.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.update.md index 2c71e518b7b05..ed6243c409fa4 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.update.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.update.md @@ -9,7 +9,7 @@ Updates an SavedObject Signature: ```typescript -update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; +update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md index efdc07cea88fd..a79a23db967cc 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md @@ -11,7 +11,7 @@ Return type of the Saved Objects `find()` method. Signature: ```typescript -export interface SavedObjectsFindResponse +export interface SavedObjectsFindResponse ``` ## Properties diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkcreate.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkcreate.md index dfe9e51e62483..f0d8d9edfbe79 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkcreate.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkcreate.md @@ -9,7 +9,7 @@ Creates multiple documents at once Signature: ```typescript -bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; +bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkget.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkget.md index 34b113bce5410..e27c5fc3bec9a 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkget.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkget.md @@ -9,7 +9,7 @@ Returns an array of objects by id Signature: ```typescript -bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; +bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkupdate.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkupdate.md index 23c7a92624957..5ad09d59f4061 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkupdate.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkupdate.md @@ -9,7 +9,7 @@ Updates multiple objects in bulk Signature: ```typescript -bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; +bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.create.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.create.md index 29e3c3ab24654..fd6495bd2d3c4 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.create.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.create.md @@ -9,7 +9,7 @@ Persists an object Signature: ```typescript -create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; +create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.find.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.find.md index dbf6d59e78d85..ccb9feca1669e 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.find.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.find.md @@ -7,7 +7,7 @@ Signature: ```typescript -find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise>; +find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.get.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.get.md index 930a4647ca175..b3ccbc7277b4e 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.get.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.get.md @@ -9,7 +9,7 @@ Gets a single object Signature: ```typescript -get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; +get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.update.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.update.md index 5e9f69ecc567b..bb215cdb97af5 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.update.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.update.md @@ -9,7 +9,7 @@ Updates an object Signature: ```typescript -update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; +update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsupdateresponse.md b/docs/development/core/server/kibana-plugin-server.savedobjectsupdateresponse.md index 64c9037735358..130b0b4faaa07 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsupdateresponse.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsupdateresponse.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObjectsUpdateResponse extends Omit, 'attributes' | 'references'> +export interface SavedObjectsUpdateResponse extends Omit, 'attributes' | 'references'> ``` ## Properties diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index ec626677d0902..9d4052bbd0156 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -62,17 +62,12 @@ mentioned use "\_default_". `histogram:maxBars`:: Date histograms are not generated with more bars than the value of this property, scaling values when necessary. `history:limit`:: In fields that have history, such as query inputs, show this many recent values. -`indexPattern:fieldMapping:lookBack`:: For index patterns containing timestamps in their names, -look for this many recent matching patterns from which to query the field mapping. `indexPattern:placeholder`:: The default placeholder value to use in Management > Index Patterns > Create Index Pattern. `metaFields`:: Fields that exist outside of `_source`. Kibana merges these fields into the document when displaying it. `metrics:max_buckets`:: The maximum numbers of buckets that a single data source can return. This might arise when the user selects a short interval (for example, 1s) for a long time period (1 year). -`pageNavigation`:: The style of navigation menu for Kibana. -Choices are Individual, the legacy style where every plugin is represented in the nav, -and Grouped, a new format that bundles related plugins together in nested navigation. `query:allowLeadingWildcards`:: Allows a wildcard (*) as the first character in a query clause. Only applies when experimental query features are enabled in the query bar. To disallow leading wildcards in Lucene queries, diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index c1f06aff722b5..2d4d00b730109 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -231,7 +231,7 @@ This setting does not impact <> and <> visualizations. Supported on {ece}. Each layer object points to an external vector file that contains a geojson FeatureCollection. The file must use the -https://en.wikipedia.org/wiki/World_Geodetic_System[WGS84 coordinate reference system] +https://en.wikipedia.org/wiki/World_Geodetic_System[WGS84 coordinate reference system (ESPG:4326)] and only include polygons. If the file is hosted on a separate domain from Kibana, the server needs to be CORS-enabled so Kibana can download the file. The following example shows a valid regionmap configuration. diff --git a/examples/demo_search/common/index.ts b/examples/demo_search/common/index.ts index 6587ee96ef61b..8ea8d6186ee82 100644 --- a/examples/demo_search/common/index.ts +++ b/examples/demo_search/common/index.ts @@ -20,6 +20,7 @@ import { IKibanaSearchRequest, IKibanaSearchResponse } from '../../../src/plugins/data/public'; export const DEMO_SEARCH_STRATEGY = 'DEMO_SEARCH_STRATEGY'; +export const ASYNC_DEMO_SEARCH_STRATEGY = 'ASYNC_DEMO_SEARCH_STRATEGY'; export interface IDemoRequest extends IKibanaSearchRequest { mood: string | 'sad' | 'happy'; @@ -29,3 +30,11 @@ export interface IDemoRequest extends IKibanaSearchRequest { export interface IDemoResponse extends IKibanaSearchResponse { greeting: string; } + +export interface IAsyncDemoRequest extends IKibanaSearchRequest { + fibonacciNumbers: number; +} + +export interface IAsyncDemoResponse extends IKibanaSearchResponse { + fibonacciSequence: number[]; +} diff --git a/examples/demo_search/kibana.json b/examples/demo_search/kibana.json index 0603706b07d1f..cb73274ed23f7 100644 --- a/examples/demo_search/kibana.json +++ b/examples/demo_search/kibana.json @@ -2,7 +2,7 @@ "id": "demoSearch", "version": "0.0.1", "kibanaVersion": "kibana", - "configPath": ["demo_search"], + "configPath": ["demoSearch"], "server": true, "ui": true, "requiredPlugins": ["data"], diff --git a/examples/demo_search/public/async_demo_search_strategy.ts b/examples/demo_search/public/async_demo_search_strategy.ts new file mode 100644 index 0000000000000..7a3f33ce05a75 --- /dev/null +++ b/examples/demo_search/public/async_demo_search_strategy.ts @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; +import { + ISearchContext, + TSearchStrategyProvider, + ISearchStrategy, +} from '../../../src/plugins/data/public'; + +import { ASYNC_DEMO_SEARCH_STRATEGY, IAsyncDemoResponse } from '../common'; +import { ASYNC_SEARCH_STRATEGY } from '../../../x-pack/plugins/data_enhanced/public'; + +/** + * This demo search strategy provider simply provides a shortcut for calling the DEMO_ASYNC_SEARCH_STRATEGY + * on the server side, without users having to pass it in explicitly, and it takes advantage of the + * already registered ASYNC_SEARCH_STRATEGY that exists on the client. + * + * so instead of callers having to do: + * + * ``` + * search( + * { ...request, serverStrategy: DEMO_ASYNC_SEARCH_STRATEGY }, + * options, + * ASYNC_SEARCH_STRATEGY + * ) as Observable, + *``` + + * They can instead just do + * + * ``` + * search(request, options, DEMO_ASYNC_SEARCH_STRATEGY); + * ``` + * + * and are ensured type safety in regard to the request and response objects. + * + * @param context - context supplied by other plugins. + * @param search - a search function to access other strategies that have already been registered. + */ +export const asyncDemoClientSearchStrategyProvider: TSearchStrategyProvider = ( + context: ISearchContext +): ISearchStrategy => { + const asyncStrategyProvider = context.getSearchStrategy(ASYNC_SEARCH_STRATEGY); + const { search } = asyncStrategyProvider(context); + return { + search: (request, options) => { + return search( + { ...request, serverStrategy: ASYNC_DEMO_SEARCH_STRATEGY }, + options + ) as Observable; + }, + }; +}; diff --git a/examples/demo_search/public/demo_search_strategy.ts b/examples/demo_search/public/demo_search_strategy.ts index cb2480c8a5f19..8dc2779a8544c 100644 --- a/examples/demo_search/public/demo_search_strategy.ts +++ b/examples/demo_search/public/demo_search_strategy.ts @@ -43,7 +43,7 @@ import { DEMO_SEARCH_STRATEGY, IDemoResponse } from '../common'; * ``` * context.search(request, options, DEMO_SEARCH_STRATEGY); * ``` - * + * * and are ensured type safety in regard to the request and response objects. * * @param context - context supplied by other plugins. diff --git a/examples/demo_search/public/plugin.ts b/examples/demo_search/public/plugin.ts index 62c912716e627..a2539cc7a21c5 100644 --- a/examples/demo_search/public/plugin.ts +++ b/examples/demo_search/public/plugin.ts @@ -19,9 +19,16 @@ import { DataPublicPluginSetup } from '../../../src/plugins/data/public'; import { Plugin, CoreSetup } from '../../../src/core/public'; -import { DEMO_SEARCH_STRATEGY } from '../common'; +import { + DEMO_SEARCH_STRATEGY, + IDemoRequest, + IDemoResponse, + ASYNC_DEMO_SEARCH_STRATEGY, + IAsyncDemoRequest, + IAsyncDemoResponse, +} from '../common'; import { demoClientSearchStrategyProvider } from './demo_search_strategy'; -import { IDemoRequest, IDemoResponse } from '../common'; +import { asyncDemoClientSearchStrategyProvider } from './async_demo_search_strategy'; interface DemoDataSearchSetupDependencies { data: DataPublicPluginSetup; @@ -39,10 +46,12 @@ interface DemoDataSearchSetupDependencies { declare module '../../../src/plugins/data/public' { export interface IRequestTypesMap { [DEMO_SEARCH_STRATEGY]: IDemoRequest; + [ASYNC_DEMO_SEARCH_STRATEGY]: IAsyncDemoRequest; } export interface IResponseTypesMap { [DEMO_SEARCH_STRATEGY]: IDemoResponse; + [ASYNC_DEMO_SEARCH_STRATEGY]: IAsyncDemoResponse; } } @@ -52,6 +61,10 @@ export class DemoDataPlugin implements Plugin { DEMO_SEARCH_STRATEGY, demoClientSearchStrategyProvider ); + deps.data.search.registerSearchStrategyProvider( + ASYNC_DEMO_SEARCH_STRATEGY, + asyncDemoClientSearchStrategyProvider + ); } public start() {} diff --git a/examples/demo_search/server/async_demo_search_strategy.ts b/examples/demo_search/server/async_demo_search_strategy.ts new file mode 100644 index 0000000000000..d2d40891a5d93 --- /dev/null +++ b/examples/demo_search/server/async_demo_search_strategy.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TSearchStrategyProvider } from '../../../src/plugins/data/server'; +import { ASYNC_DEMO_SEARCH_STRATEGY } from '../common'; + +function getFibonacciSequence(n = 0) { + const beginning = [0, 1].slice(0, n); + return Array(Math.max(0, n)) + .fill(null) + .reduce((sequence, value, i) => { + if (i < 2) return sequence; + return [...sequence, sequence[i - 1] + sequence[i - 2]]; + }, beginning); +} + +const generateId = (() => { + let id = 0; + return () => `${id++}`; +})(); + +const loadedMap = new Map(); +const totalMap = new Map(); + +export const asyncDemoSearchStrategyProvider: TSearchStrategyProvider = () => { + return { + search: async request => { + const id = request.id ?? generateId(); + + const loaded = (loadedMap.get(id) ?? 0) + 1; + loadedMap.set(id, loaded); + + const total = request.fibonacciNumbers ?? totalMap.get(id); + totalMap.set(id, total); + + const fibonacciSequence = getFibonacciSequence(loaded); + return { id, total, loaded, fibonacciSequence }; + }, + cancel: async id => { + loadedMap.delete(id); + totalMap.delete(id); + }, + }; +}; diff --git a/examples/demo_search/server/plugin.ts b/examples/demo_search/server/plugin.ts index 653aa217717fa..49fbae43e3aa2 100644 --- a/examples/demo_search/server/plugin.ts +++ b/examples/demo_search/server/plugin.ts @@ -20,7 +20,15 @@ import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/server'; import { PluginSetup as DataPluginSetup } from 'src/plugins/data/server'; import { demoSearchStrategyProvider } from './demo_search_strategy'; -import { DEMO_SEARCH_STRATEGY, IDemoRequest, IDemoResponse } from '../common'; +import { + DEMO_SEARCH_STRATEGY, + IDemoRequest, + IDemoResponse, + ASYNC_DEMO_SEARCH_STRATEGY, + IAsyncDemoRequest, + IAsyncDemoResponse, +} from '../common'; +import { asyncDemoSearchStrategyProvider } from './async_demo_search_strategy'; interface IDemoSearchExplorerDeps { data: DataPluginSetup; @@ -38,10 +46,12 @@ interface IDemoSearchExplorerDeps { declare module '../../../src/plugins/data/server' { export interface IRequestTypesMap { [DEMO_SEARCH_STRATEGY]: IDemoRequest; + [ASYNC_DEMO_SEARCH_STRATEGY]: IAsyncDemoRequest; } export interface IResponseTypesMap { [DEMO_SEARCH_STRATEGY]: IDemoResponse; + [ASYNC_DEMO_SEARCH_STRATEGY]: IAsyncDemoResponse; } } @@ -54,6 +64,11 @@ export class DemoDataPlugin implements Plugin id: 'demoSearch', component: , }, + { + title: 'Async demo search strategy', + id: 'asyncDemoSearch', + component: , + }, ]; const routes = pages.map((page, i) => ( diff --git a/examples/search_explorer/public/async_demo_strategy.tsx b/examples/search_explorer/public/async_demo_strategy.tsx new file mode 100644 index 0000000000000..40ddcc1f48fe7 --- /dev/null +++ b/examples/search_explorer/public/async_demo_strategy.tsx @@ -0,0 +1,123 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { + EuiPageContentBody, + EuiFormRow, + EuiFlexItem, + EuiFlexGroup, + EuiFieldNumber, +} from '@elastic/eui'; +import { ISearchGeneric } from '../../../src/plugins/data/public'; +import { DoSearch } from './do_search'; +import { GuideSection } from './guide_section'; + +import { ASYNC_DEMO_SEARCH_STRATEGY, IAsyncDemoRequest } from '../../demo_search/common'; + +// @ts-ignore +import demoStrategyServerProvider from '!!raw-loader!./../../demo_search/server/async_demo_search_strategy'; +// @ts-ignore +import demoStrategyPublicProvider from '!!raw-loader!./../../demo_search/public/async_demo_search_strategy'; +// @ts-ignore +import demoStrategyServerPlugin from '!!raw-loader!./../../demo_search/server/plugin'; +// @ts-ignore +import demoStrategyPublicPlugin from '!!raw-loader!./../../demo_search/public/plugin'; + +interface Props { + search: ISearchGeneric; +} + +interface State { + searching: boolean; + fibonacciNumbers: number; + changes: boolean; + error?: any; +} + +export class AsyncDemoStrategy extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + searching: false, + changes: false, + fibonacciNumbers: 5, + }; + } + + renderDemo = () => { + const request: IAsyncDemoRequest = { + fibonacciNumbers: this.state.fibonacciNumbers, + }; + return ( + + + + + this.setState({ fibonacciNumbers: parseFloat(e.target.value) })} + /> + + + + + this.props.search(request, { signal }, ASYNC_DEMO_SEARCH_STRATEGY) + } + /> + + ); + }; + + render() { + return ( + + + + ); + } +} diff --git a/examples/search_explorer/public/do_search.tsx b/examples/search_explorer/public/do_search.tsx index f279b9fcd6e23..a6b6b9b57db4a 100644 --- a/examples/search_explorer/public/do_search.tsx +++ b/examples/search_explorer/public/do_search.tsx @@ -118,8 +118,8 @@ ${requestStr} Response: { ); }); -test('returns empty array if input is empty but type has default value', () => { - const type = schema.arrayOf(schema.string({ defaultValue: 'test' })); +test('fails if sparse content in array', () => { + const type = schema.arrayOf(schema.string()); expect(type.validate([])).toEqual([]); + expect(() => type.validate([undefined])).toThrowErrorMatchingInlineSnapshot( + `"[0]: sparse array are not allowed"` + ); }); -test('returns empty array if input is empty even if type is required', () => { - const type = schema.arrayOf(schema.string()); +test('fails if sparse content in array if optional', () => { + const type = schema.arrayOf(schema.maybe(schema.string())); + expect(type.validate([])).toEqual([]); + expect(() => type.validate([undefined])).toThrowErrorMatchingInlineSnapshot( + `"[0]: sparse array are not allowed"` + ); +}); + +test('fails if sparse content in array if nullable', () => { + const type = schema.arrayOf(schema.nullable(schema.string())); expect(type.validate([])).toEqual([]); + expect(type.validate([null])).toEqual([null]); + expect(() => type.validate([undefined])).toThrowErrorMatchingInlineSnapshot( + `"[0]: sparse array are not allowed"` + ); }); test('fails for null values if optional', () => { @@ -102,9 +117,19 @@ test('fails for null values if optional', () => { ); }); +test('returns empty array if input is empty but type has default value', () => { + const type = schema.arrayOf(schema.string({ defaultValue: 'test' })); + expect(type.validate([])).toEqual([]); +}); + +test('returns empty array if input is empty even if type is required', () => { + const type = schema.arrayOf(schema.string()); + expect(type.validate([])).toEqual([]); +}); + test('handles default values for undefined values', () => { - const type = schema.arrayOf(schema.string({ defaultValue: 'foo' })); - expect(type.validate([undefined])).toEqual(['foo']); + const type = schema.arrayOf(schema.string(), { defaultValue: ['foo'] }); + expect(type.validate(undefined)).toEqual(['foo']); }); test('array within array', () => { diff --git a/packages/kbn-config-schema/src/types/array_type.ts b/packages/kbn-config-schema/src/types/array_type.ts index ad74f375588ad..a0353e8348ddd 100644 --- a/packages/kbn-config-schema/src/types/array_type.ts +++ b/packages/kbn-config-schema/src/types/array_type.ts @@ -31,7 +31,7 @@ export class ArrayType extends Type { let schema = internals .array() .items(type.getSchema().optional()) - .sparse(); + .sparse(false); if (options.minSize !== undefined) { schema = schema.min(options.minSize); @@ -49,6 +49,8 @@ export class ArrayType extends Type { case 'any.required': case 'array.base': return `expected value of type [array] but got [${typeDetect(value)}]`; + case 'array.sparse': + return `sparse array are not allowed`; case 'array.parse': return `could not parse array value from [${value}]`; case 'array.min': diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json index d2f0b0c358284..5dede7fbf1aaa 100644 --- a/packages/kbn-interpreter/package.json +++ b/packages/kbn-interpreter/package.json @@ -11,7 +11,7 @@ "dependencies": { "@babel/runtime": "^7.5.5", "@kbn/i18n": "1.0.0", - "lodash": "npm:@elastic/lodash@3.10.1-kibana3", + "lodash": "npm:@elastic/lodash@3.10.1-kibana4", "lodash.clone": "^4.5.0", "uuid": "3.3.2" }, diff --git a/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts index 44234acd897dc..7750f9145667e 100644 --- a/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts +++ b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts @@ -17,14 +17,37 @@ * under the License. */ +import Path from 'path'; + import jestDiff from 'jest-diff'; import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils'; import { reformatJestDiff, getOptimizerCacheKey, diffCacheKey } from './cache_keys'; import { OptimizerConfig } from './optimizer_config'; -jest.mock('./get_changes.ts'); +jest.mock('./get_changes.ts', () => ({ + getChanges: async () => + new Map([ + ['/foo/bar/a', 'modified'], + ['/foo/bar/b', 'modified'], + ['/foo/bar/c', 'deleted'], + ]), +})); + +jest.mock('./get_mtimes.ts', () => ({ + getMtimes: async (paths: string[]) => new Map(paths.map(path => [path, 12345])), +})); + jest.mock('execa'); + +jest.mock('fs', () => { + const realFs = jest.requireActual('fs'); + return { + ...realFs, + readFile: jest.fn(realFs.readFile), + }; +}); + expect.addSnapshotSerializer(createAbsolutePathSerializer()); jest.requireMock('execa').mockImplementation(async (cmd: string, args: string[], opts: object) => { @@ -46,28 +69,35 @@ jest.requireMock('execa').mockImplementation(async (cmd: string, args: string[], }; }); -jest.requireMock('./get_changes.ts').getChanges.mockImplementation( - async () => - new Map([ - ['/foo/bar/a', 'modified'], - ['/foo/bar/b', 'modified'], - ['/foo/bar/c', 'deleted'], - ]) -); - describe('getOptimizerCacheKey()', () => { - it('uses latest commit and changes files to create unique value', async () => { + it('uses latest commit, bootstrap cache, and changed files to create unique value', async () => { + jest + .requireMock('fs') + .readFile.mockImplementation( + (path: string, enc: string, cb: (err: null, file: string) => void) => { + expect(path).toBe( + Path.resolve(REPO_ROOT, 'packages/kbn-optimizer/target/.bootstrap-cache') + ); + expect(enc).toBe('utf8'); + cb(null, ''); + } + ); + const config = OptimizerConfig.create({ repoRoot: REPO_ROOT, }); await expect(getOptimizerCacheKey(config)).resolves.toMatchInlineSnapshot(` Object { + "bootstrap": "", "deletedPaths": Array [ "/foo/bar/c", ], "lastCommit": "", - "modifiedPaths": Object {}, + "modifiedTimes": Object { + "/foo/bar/a": 12345, + "/foo/bar/b": 12345, + }, "workerConfig": Object { "browserslistEnv": "dev", "cache": true, diff --git a/packages/kbn-optimizer/src/optimizer/cache_keys.ts b/packages/kbn-optimizer/src/optimizer/cache_keys.ts index 3529ffa587f16..af6a8a648d29c 100644 --- a/packages/kbn-optimizer/src/optimizer/cache_keys.ts +++ b/packages/kbn-optimizer/src/optimizer/cache_keys.ts @@ -18,6 +18,8 @@ */ import Path from 'path'; +import Fs from 'fs'; +import { promisify } from 'util'; import Chalk from 'chalk'; import execa from 'execa'; @@ -116,9 +118,10 @@ export function reformatJestDiff(diff: string | null) { export interface OptimizerCacheKey { readonly lastCommit: string | undefined; + readonly bootstrap: string | undefined; readonly workerConfig: WorkerConfig; readonly deletedPaths: string[]; - readonly modifiedPaths: Record; + readonly modifiedTimes: Record; } async function getLastCommit() { @@ -133,21 +136,45 @@ async function getLastCommit() { return stdout.trim() || undefined; } +async function getBootstrapCacheKey() { + try { + return await promisify(Fs.readFile)( + Path.resolve(OPTIMIZER_DIR, 'target/.bootstrap-cache'), + 'utf8' + ); + } catch (error) { + if (error?.code !== 'ENOENT') { + throw error; + } + return undefined; + } +} + export async function getOptimizerCacheKey(config: OptimizerConfig) { - const changes = Array.from((await getChanges(OPTIMIZER_DIR)).entries()); + const [changes, lastCommit, bootstrap] = await Promise.all([ + getChanges(OPTIMIZER_DIR), + getLastCommit(), + getBootstrapCacheKey(), + ] as const); + + const deletedPaths: string[] = []; + const modifiedPaths: string[] = []; + for (const [path, type] of changes) { + (type === 'deleted' ? deletedPaths : modifiedPaths).push(path); + } const cacheKeys: OptimizerCacheKey = { - lastCommit: await getLastCommit(), workerConfig: config.getWorkerConfig('♻'), - deletedPaths: changes.filter(e => e[1] === 'deleted').map(e => e[0]), - modifiedPaths: {} as Record, + lastCommit, + bootstrap, + deletedPaths, + modifiedTimes: {} as Record, }; - const modified = changes.filter(e => e[1] === 'modified').map(e => e[0]); - const mtimes = await getMtimes(modified); + const mtimes = await getMtimes(modifiedPaths); for (const [path, mtime] of Array.from(mtimes.entries()).sort(ascending(e => e[0]))) { if (typeof mtime === 'number') { - cacheKeys.modifiedPaths[path] = mtime; + cacheKeys.modifiedTimes[path] = mtime; } } diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index a258e1010fce3..1c8ae265bf6bb 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -25,6 +25,15 @@ import { Bundle, WorkerConfig } from '../common'; import { findKibanaPlatformPlugins, KibanaPlatformPlugin } from './kibana_platform_plugins'; import { getBundles } from './get_bundles'; +function pickMaxWorkerCount(dist: boolean) { + // don't break if cpus() returns nothing, or an empty array + const cpuCount = Math.max(Os.cpus()?.length, 1); + // if we're buiding the dist then we can use more of the system's resources to get things done a little quicker + const maxWorkers = dist ? cpuCount - 1 : Math.ceil(cpuCount / 3); + // ensure we always have at least two workers + return Math.max(maxWorkers, 2); +} + interface Options { /** absolute path to root of the repo/build */ repoRoot: string; @@ -110,7 +119,7 @@ export class OptimizerConfig { const maxWorkerCount = process.env.KBN_OPTIMIZER_MAX_WORKERS ? parseInt(process.env.KBN_OPTIMIZER_MAX_WORKERS, 10) - : options.maxWorkerCount ?? Math.max(Math.ceil(Math.max(Os.cpus()?.length, 1) / 3), 2); + : options.maxWorkerCount ?? pickMaxWorkerCount(dist); if (typeof maxWorkerCount !== 'number' || !Number.isFinite(maxWorkerCount)) { throw new TypeError('worker count must be a number'); } diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 22b927d4638d7..3c6ae78bc4d91 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -252,6 +252,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { cache: false, sourceMap: false, extractComments: false, + parallel: false, terserOptions: { compress: false, mangle: false, diff --git a/packages/kbn-storybook/index.js b/packages/kbn-storybook/index.js index 78e2cf7f5073b..b595de8ea1c07 100644 --- a/packages/kbn-storybook/index.js +++ b/packages/kbn-storybook/index.js @@ -24,7 +24,7 @@ const { first } = require('rxjs/operators'); const storybook = require('@storybook/react/standalone'); const { run } = require('@kbn/dev-utils'); const { generateStorybookEntry } = require('./lib/storybook_entry'); -const { REPO_ROOT, CURRENT_CONFIG } = require('./lib/constants'); +const { REPO_ROOT, ASSET_DIR, CURRENT_CONFIG } = require('./lib/constants'); const { buildDll } = require('./lib/dll'); exports.runStorybookCli = config => { @@ -62,21 +62,30 @@ exports.runStorybookCli = config => { // route errors subj.toPromise(), - new Promise(() => { + new Promise(async () => { // storybook never completes, so neither will this promise const configDir = join(__dirname, 'storybook_config'); log.debug('Config dir:', configDir); - storybook({ - mode: 'dev', + + const config = { + mode: flags.site ? 'static' : 'dev', port: 9001, configDir, - }); + }; + if (flags.site) { + config.outputDir = join(ASSET_DIR, name); + } + + await storybook(config); + + // Line is only reached when building the static version + if (flags.site) process.exit(); }), ]); }, { flags: { - boolean: ['rebuildDll'], + boolean: ['rebuildDll', 'site'], }, description: ` Run the storybook examples for ${name} diff --git a/packages/kbn-storybook/storybook_config/preview-head.html b/packages/kbn-storybook/storybook_config/preview-head.html index bef08a5120a36..16754ad550da0 100644 --- a/packages/kbn-storybook/storybook_config/preview-head.html +++ b/packages/kbn-storybook/storybook_config/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. --> - - + + diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index fc245ca3fe921..0402a83d3d274 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -17,7 +17,7 @@ "dependencies": { "classnames": "2.2.6", "focus-trap-react": "^3.1.1", - "lodash": "npm:@elastic/lodash@3.10.1-kibana3", + "lodash": "npm:@elastic/lodash@3.10.1-kibana4", "prop-types": "15.6.0", "react": "^16.12.0", "react-ace": "^5.9.0", diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index 9a5fb479276f7..5028c6efdb40e 100644 --- a/packages/kbn-ui-shared-deps/entry.js +++ b/packages/kbn-ui-shared-deps/entry.js @@ -40,6 +40,8 @@ export const ReactDom = require('react-dom'); export const ReactIntl = require('react-intl'); export const ReactRouter = require('react-router'); // eslint-disable-line export const ReactRouterDom = require('react-router-dom'); +export const Monaco = require('./monaco.ts'); +export const MonacoBare = require('monaco-editor/esm/vs/editor/editor.api'); // load timezone data into moment-timezone Moment.tz.load(require('moment-timezone/data/packed/latest.json')); diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index 5f5ac3f1c9c2f..c7c004bd55794 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -41,4 +41,7 @@ exports.externals = { 'react-intl': '__kbnSharedDeps__.ReactIntl', 'react-router': '__kbnSharedDeps__.ReactRouter', 'react-router-dom': '__kbnSharedDeps__.ReactRouterDom', + '@kbn/ui-shared-deps/monaco': '__kbnSharedDeps__.Monaco', + // this is how plugins/consumers from npm load monaco + 'monaco-editor/esm/vs/editor/editor.api': '__kbnSharedDeps__.MonacoBare', }; diff --git a/packages/kbn-ui-shared-deps/monaco.ts b/packages/kbn-ui-shared-deps/monaco.ts new file mode 100644 index 0000000000000..570aca86c484c --- /dev/null +++ b/packages/kbn-ui-shared-deps/monaco.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; + +import 'monaco-editor/esm/vs/base/common/worker/simpleWorker'; +import 'monaco-editor/esm/vs/base/worker/defaultWorkerFactory'; + +import 'monaco-editor/esm/vs/editor/browser/controller/coreCommands.js'; +import 'monaco-editor/esm/vs/editor/browser/widget/codeEditorWidget.js'; + +import 'monaco-editor/esm/vs/editor/contrib/suggest/suggestController.js'; // Needed for suggestions +import 'monaco-editor/esm/vs/editor/contrib/hover/hover.js'; // Needed for hover +import 'monaco-editor/esm/vs/editor/contrib/parameterHints/parameterHints.js'; // Needed for signature + +export { monaco }; diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 6795d363a9f1d..e9ad227b235fa 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,9 +9,10 @@ "kbn:watch": "node scripts/build --watch" }, "devDependencies": { - "@elastic/charts": "^17.0.2", + "@elastic/charts": "^17.1.1", "abortcontroller-polyfill": "^1.4.0", "@elastic/eui": "19.0.0", + "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", "@kbn/i18n": "1.0.0", "@yarnpkg/lockfile": "^1.1.0", diff --git a/packages/kbn-ui-shared-deps/tsconfig.json b/packages/kbn-ui-shared-deps/tsconfig.json index c5c3cba147fcf..5d981c73f1d21 100644 --- a/packages/kbn-ui-shared-deps/tsconfig.json +++ b/packages/kbn-ui-shared-deps/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.json", "include": [ - "index.d.ts" + "index.d.ts", + "monaco.ts" ] } diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index 87cca2cc897f8..dc6e7ae33dbec 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -59,6 +59,17 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'], }, + { + include: [require.resolve('./monaco.ts')], + use: [ + { + loader: 'babel-loader', + options: { + presets: [require.resolve('@kbn/babel-preset/webpack_preset')], + }, + }, + ], + }, ], }, diff --git a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml index 23f33940283c0..594c2efc8adc9 100644 --- a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml +++ b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml @@ -1,4 +1,5 @@ server: + autoListen: false port: 8274 logging: json: true @@ -6,3 +7,5 @@ optimize: enabled: false plugins: initialize: false +migrations: + skip: true diff --git a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_console.test.yml b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_console.test.yml new file mode 100644 index 0000000000000..33dd4787efad9 --- /dev/null +++ b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_console.test.yml @@ -0,0 +1,22 @@ +server: + autoListen: false + port: 8274 +logging: + loggers: + - context: root + appenders: + - console + level: debug + appenders: + console: + kind: console + layout: + kind: json + root: + level: debug +optimize: + enabled: false +plugins: + initialize: false +migrations: + skip: true diff --git a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_file.test.yml b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_file.test.yml new file mode 100644 index 0000000000000..f5148899ff854 --- /dev/null +++ b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_file.test.yml @@ -0,0 +1,22 @@ +server: + autoListen: false + port: 8274 +logging: + loggers: + - context: root + appenders: + - file + level: debug + appenders: + file: + kind: file + layout: + kind: pattern + root: + level: debug +optimize: + enabled: false +plugins: + initialize: false +migrations: + skip: true diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.js b/src/cli/serve/integration_tests/reload_logging_config.test.js deleted file mode 100644 index 82d514877aff6..0000000000000 --- a/src/cli/serve/integration_tests/reload_logging_config.test.js +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { spawn } from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import os from 'os'; -import del from 'del'; - -import { safeDump } from 'js-yaml'; -import { - createMapStream, - createSplitStream, - createPromiseFromStreams, -} from '../../../legacy/utils/streams'; -import { getConfigFromFiles } from '../../../core/server/config/read_config'; - -const testConfigFile = follow('__fixtures__/reload_logging_config/kibana.test.yml'); -const kibanaPath = follow('../../../../scripts/kibana.js'); - -const second = 1000; -const minute = second * 60; - -const tempDir = path.join(os.tmpdir(), 'kbn-reload-test'); - -function follow(file) { - return path.relative(process.cwd(), path.resolve(__dirname, file)); -} - -function setLoggingJson(enabled) { - const conf = getConfigFromFiles([testConfigFile]); - conf.logging = conf.logging || {}; - conf.logging.json = enabled; - - const yaml = safeDump(conf); - - fs.writeFileSync(testConfigFile, yaml); -} - -describe('Server logging configuration', function() { - let child; - let isJson; - - beforeEach(() => { - isJson = true; - setLoggingJson(true); - - fs.mkdirSync(tempDir, { recursive: true }); - }); - - afterEach(() => { - isJson = true; - setLoggingJson(true); - - if (child !== undefined) { - child.kill(); - child = undefined; - } - - del.sync(tempDir, { force: true }); - }); - - const isWindows = /^win/.test(process.platform); - if (isWindows) { - it('SIGHUP is not a feature of Windows.', () => { - // nothing to do for Windows - }); - } else { - it( - 'should be reloadable via SIGHUP process signaling', - async function() { - expect.assertions(3); - - child = spawn( - process.execPath, - [kibanaPath, '--config', testConfigFile, '--oss', '--verbose'], - { - stdio: 'pipe', - } - ); - - let sawJson = false; - let sawNonjson = false; - - const [exitCode] = await Promise.all([ - Promise.race([ - new Promise(r => child.once('exit', r)).then(code => (code === null ? 0 : code)), - - new Promise(r => child.once('error', r)).then(err => { - throw new Error( - `error in child process while attempting to reload config. ${err.stack || - err.message || - err}` - ); - }), - ]), - - createPromiseFromStreams([ - child.stdout, - createSplitStream('\n'), - createMapStream(async line => { - if (!line) { - // skip empty lines - return; - } - - if (isJson) { - const data = JSON.parse(line); - sawJson = true; - - // We know the sighup handler will be registered before - // root.setup() is called - if (data.message.includes('setting up root')) { - isJson = false; - setLoggingJson(false); - - // Reload logging config. We give it a little bit of time to just make - // sure the process sighup handler is registered. - await new Promise(r => setTimeout(r, 100)); - child.kill('SIGHUP'); - } - } else if (line.startsWith('{')) { - // We have told Kibana to stop logging json, but it hasn't completed - // the switch yet, so we ignore before switching over. - } else { - // Kibana has successfully stopped logging json, so kill the server. - sawNonjson = true; - - child && child.kill(); - child = undefined; - } - }), - ]), - ]); - - expect(exitCode).toEqual(0); - expect(sawJson).toEqual(true); - expect(sawNonjson).toEqual(true); - }, - minute - ); - - it( - 'should recreate file handler on SIGHUP', - function(done) { - expect.hasAssertions(); - - const logPath = path.resolve(tempDir, 'kibana.log'); - const logPathArchived = path.resolve(tempDir, 'kibana_archive.log'); - - function watchFileUntil(path, matcher, timeout) { - return new Promise((resolve, reject) => { - const timeoutHandle = setTimeout(() => { - fs.unwatchFile(path); - reject(`watchFileUntil timed out for "${matcher}"`); - }, timeout); - - fs.watchFile(path, () => { - try { - const contents = fs.readFileSync(path); - - if (matcher.test(contents)) { - clearTimeout(timeoutHandle); - fs.unwatchFile(path); - resolve(contents); - } - } catch (e) { - // noop - } - }); - }); - } - - child = spawn(process.execPath, [ - kibanaPath, - '--oss', - '--config', - testConfigFile, - '--logging.dest', - logPath, - '--plugins.initialize', - 'false', - '--logging.json', - 'false', - '--verbose', - ]); - - watchFileUntil(logPath, /starting server/, 2 * minute) - .then(() => { - // once the server is running, archive the log file and issue SIGHUP - fs.renameSync(logPath, logPathArchived); - child.kill('SIGHUP'); - }) - .then(() => - watchFileUntil(logPath, /Reloaded logging configuration due to SIGHUP/, 10 * second) - ) - .then(contents => { - const lines = contents.toString().split('\n'); - // should be the first line of the new log file - expect(lines[0]).toMatch(/Reloaded logging configuration due to SIGHUP/); - child.kill(); - }) - .then(done, done); - }, - 3 * minute - ); - } -}); diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.ts b/src/cli/serve/integration_tests/reload_logging_config.test.ts new file mode 100644 index 0000000000000..2def3569828d3 --- /dev/null +++ b/src/cli/serve/integration_tests/reload_logging_config.test.ts @@ -0,0 +1,263 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Child from 'child_process'; +import Fs from 'fs'; +import Path from 'path'; +import Os from 'os'; +import Del from 'del'; + +import * as Rx from 'rxjs'; +import { map, filter, take } from 'rxjs/operators'; +import { safeDump } from 'js-yaml'; + +import { getConfigFromFiles } from '../../../core/server/config/read_config'; + +const legacyConfig = follow('__fixtures__/reload_logging_config/kibana.test.yml'); +const configFileLogConsole = follow( + '__fixtures__/reload_logging_config/kibana_log_console.test.yml' +); +const configFileLogFile = follow('__fixtures__/reload_logging_config/kibana_log_file.test.yml'); + +const kibanaPath = follow('../../../../scripts/kibana.js'); + +const second = 1000; +const minute = second * 60; + +const tempDir = Path.join(Os.tmpdir(), 'kbn-reload-test'); + +function follow(file: string) { + return Path.relative(process.cwd(), Path.resolve(__dirname, file)); +} + +function watchFileUntil(path: string, matcher: RegExp, timeout: number) { + return new Promise((resolve, reject) => { + const timeoutHandle = setTimeout(() => { + Fs.unwatchFile(path); + reject(`watchFileUntil timed out for "${matcher}"`); + }, timeout); + + Fs.watchFile(path, () => { + try { + const contents = Fs.readFileSync(path, 'utf-8'); + + if (matcher.test(contents)) { + clearTimeout(timeoutHandle); + Fs.unwatchFile(path); + resolve(contents); + } + } catch (e) { + // noop + } + }); + }); +} + +function containsJsonOnly(content: string[]) { + return content.every(line => line.startsWith('{')); +} + +function createConfigManager(configPath: string) { + return { + modify(fn: (input: Record) => Record) { + const oldContent = getConfigFromFiles([configPath]); + const yaml = safeDump(fn(oldContent)); + Fs.writeFileSync(configPath, yaml); + }, + }; +} + +describe('Server logging configuration', function() { + let child: Child.ChildProcess; + beforeEach(() => { + Fs.mkdirSync(tempDir, { recursive: true }); + }); + + afterEach(async () => { + if (child !== undefined) { + child.kill(); + // wait for child to be killed otherwise jest complains that process not finished + await new Promise(res => setTimeout(res, 1000)); + } + Del.sync(tempDir, { force: true }); + }); + + const isWindows = /^win/.test(process.platform); + if (isWindows) { + it('SIGHUP is not a feature of Windows.', () => { + // nothing to do for Windows + }); + } else { + describe('legacy logging', () => { + it( + 'should be reloadable via SIGHUP process signaling', + async function() { + const configFilePath = Path.resolve(tempDir, 'kibana.yml'); + Fs.copyFileSync(legacyConfig, configFilePath); + + child = Child.spawn(process.execPath, [ + kibanaPath, + '--oss', + '--config', + configFilePath, + '--verbose', + ]); + + const message$ = Rx.fromEvent(child.stdout, 'data').pipe( + map(messages => + String(messages) + .split('\n') + .filter(Boolean) + ) + ); + + await message$ + .pipe( + // We know the sighup handler will be registered before this message logged + filter(messages => messages.some(m => m.includes('setting up root'))), + take(1) + ) + .toPromise(); + + const lastMessage = await message$.pipe(take(1)).toPromise(); + expect(containsJsonOnly(lastMessage)).toBe(true); + + createConfigManager(configFilePath).modify(oldConfig => { + oldConfig.logging.json = false; + return oldConfig; + }); + + child.kill('SIGHUP'); + + await message$ + .pipe( + filter(messages => !containsJsonOnly(messages)), + take(1) + ) + .toPromise(); + }, + minute + ); + + it( + 'should recreate file handle on SIGHUP', + async function() { + const logPath = Path.resolve(tempDir, 'kibana.log'); + const logPathArchived = Path.resolve(tempDir, 'kibana_archive.log'); + + child = Child.spawn(process.execPath, [ + kibanaPath, + '--oss', + '--config', + legacyConfig, + '--logging.dest', + logPath, + '--verbose', + ]); + + await watchFileUntil(logPath, /setting up root/, 30 * second); + // once the server is running, archive the log file and issue SIGHUP + Fs.renameSync(logPath, logPathArchived); + child.kill('SIGHUP'); + + await watchFileUntil( + logPath, + /Reloaded logging configuration due to SIGHUP/, + 30 * second + ); + }, + minute + ); + }); + + describe('platform logging', () => { + it( + 'should be reloadable via SIGHUP process signaling', + async function() { + const configFilePath = Path.resolve(tempDir, 'kibana.yml'); + Fs.copyFileSync(configFileLogConsole, configFilePath); + + child = Child.spawn(process.execPath, [kibanaPath, '--oss', '--config', configFilePath]); + + const message$ = Rx.fromEvent(child.stdout, 'data').pipe( + map(messages => + String(messages) + .split('\n') + .filter(Boolean) + ) + ); + + await message$ + .pipe( + // We know the sighup handler will be registered before this message logged + filter(messages => messages.some(m => m.includes('setting up root'))), + take(1) + ) + .toPromise(); + + const lastMessage = await message$.pipe(take(1)).toPromise(); + expect(containsJsonOnly(lastMessage)).toBe(true); + + createConfigManager(configFilePath).modify(oldConfig => { + oldConfig.logging.appenders.console.layout.kind = 'pattern'; + return oldConfig; + }); + child.kill('SIGHUP'); + + await message$ + .pipe( + filter(messages => !containsJsonOnly(messages)), + take(1) + ) + .toPromise(); + }, + 30 * second + ); + it( + 'should recreate file handle on SIGHUP', + async function() { + const configFilePath = Path.resolve(tempDir, 'kibana.yml'); + Fs.copyFileSync(configFileLogFile, configFilePath); + + const logPath = Path.resolve(tempDir, 'kibana.log'); + const logPathArchived = Path.resolve(tempDir, 'kibana_archive.log'); + + createConfigManager(configFilePath).modify(oldConfig => { + oldConfig.logging.appenders.file.path = logPath; + return oldConfig; + }); + + child = Child.spawn(process.execPath, [kibanaPath, '--oss', '--config', configFilePath]); + + await watchFileUntil(logPath, /setting up root/, 30 * second); + // once the server is running, archive the log file and issue SIGHUP + Fs.renameSync(logPath, logPathArchived); + child.kill('SIGHUP'); + + await watchFileUntil( + logPath, + /Reloaded logging configuration due to SIGHUP/, + 30 * second + ); + }, + minute + ); + }); + } +}); diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 9e57fc4c36876..d33fd9bcce7a0 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1170,7 +1170,7 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | `import 'ui/query_bar'` | `import { QueryStringInput } from '../data/public'` | Directives are deprecated. | | `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../navigation/public'` | Directive is still available in `ui/kbn_top_nav`. | -| `ui/saved_objects/components/saved_object_finder` | `import { SavedObjectFinder } from '../kibana_react/public'` | | +| `ui/saved_objects/components/saved_object_finder` | `import { SavedObjectFinder } from '../saved_objects/public'` | | | `core_plugins/interpreter` | `data.expressions` | still in progress | | `ui/courier` | `data.search` | still in progress | | `ui/embeddable` | `embeddables` | still in progress | @@ -1200,9 +1200,9 @@ In server code, `core` can be accessed from either `server.newPlatform` or `kbnS | `server.plugins.elasticsearch.getCluster('data')` | [`context.core.elasticsearch.dataClient`](/docs/development/core/server/kibana-plugin-server.iscopedclusterclient.md) | | | `server.plugins.elasticsearch.getCluster('admin')` | [`context.core.elasticsearch.adminClient`](/docs/development/core/server/kibana-plugin-server.iscopedclusterclient.md) | | | `server.plugins.elasticsearch.createCluster(...)` | [`core.elasticsearch.createClient`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md) | | -| `server.savedObjects.setScopedSavedObjectsClientFactory` | [`core.savedObjects.setClientFactory`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.setclientfactory.md) | | +| `server.savedObjects.setScopedSavedObjectsClientFactory` | [`core.savedObjects.setClientFactoryProvider`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.setclientfactoryprovider.md) | | | `server.savedObjects.addScopedSavedObjectsClientWrapperFactory` | [`core.savedObjects.addClientWrapper`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.addclientwrapper.md) | | -| `server.savedObjects.getSavedObjectsRepository` | [`core.savedObjects.createInternalRepository`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createinternalrepository.md) [`core.savedObjects.createScopedRepository`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createscopedrepository.md) | | +| `server.savedObjects.getSavedObjectsRepository` | [`core.savedObjects.createInternalRepository`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.createinternalrepository.md) [`core.savedObjects.createScopedRepository`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.createscopedrepository.md) | | | `server.savedObjects.getScopedSavedObjectsClient` | [`core.savedObjects.getScopedClient`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.getscopedclient.md) | | | `request.getSavedObjectsClient` | [`context.core.savedObjects.client`](/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md) | | | `request.getUiSettingsService` | [`context.uiSettings.client`](/docs/development/core/server/kibana-plugin-server.iuisettingsclient.md) | | diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 6ab9fe158742a..2b0b115ce068e 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -193,7 +193,6 @@ export class ChromeService { recentlyAccessed$={recentlyAccessed.get$()} navControlsLeft$={navControls.getLeft$()} navControlsRight$={navControls.getRight$()} - navSetting$={uiSettings.get$('pageNavigation')} /> ), diff --git a/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap deleted file mode 100644 index cf3b48f237286..0000000000000 --- a/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap +++ /dev/null @@ -1,5283 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` - - - - - - - -`; - -exports[`NavDrawer Advanced setting set to grouped renders individual items if there are less than 7 1`] = ` - - - - - - - -`; - -exports[`NavDrawer Advanced setting set to grouped renders individual items if there is only 1 category 1`] = ` - - - - - - - -`; - -exports[`NavDrawer Advanced setting set to individual renders individual items 1`] = ` - - - - - - - -`; diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index c3cefd180b16f..c9a583f39b30c 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -42,7 +42,7 @@ import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { ChromeHelpExtension } from '../../chrome_service'; import { HeaderBadge } from './header_badge'; -import { NavSetting, OnIsLockedUpdate } from './'; +import { OnIsLockedUpdate } from './'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; import { HeaderHelpMenu } from './header_help_menu'; import { HeaderNavControls } from './header_nav_controls'; @@ -69,7 +69,6 @@ export interface HeaderProps { navControlsRight$: Rx.Observable; basePath: HttpStart['basePath']; isLocked?: boolean; - navSetting$: Rx.Observable; onIsLockedUpdate?: OnIsLockedUpdate; } @@ -81,7 +80,6 @@ interface State { forceNavigation: boolean; navControlsLeft: readonly ChromeNavControl[]; navControlsRight: readonly ChromeNavControl[]; - navSetting: NavSetting; currentAppId: string | undefined; } @@ -100,7 +98,6 @@ export class Header extends Component { forceNavigation: false, navControlsLeft: [], navControlsRight: [], - navSetting: 'grouped', currentAppId: '', }; } @@ -116,8 +113,7 @@ export class Header extends Component { Rx.combineLatest( this.props.navControlsLeft$, this.props.navControlsRight$, - this.props.application.currentAppId$, - this.props.navSetting$ + this.props.application.currentAppId$ ) ).subscribe({ next: ([ @@ -126,7 +122,7 @@ export class Header extends Component { forceNavigation, navLinks, recentlyAccessed, - [navControlsLeft, navControlsRight, currentAppId, navSetting], + [navControlsLeft, navControlsRight, currentAppId], ]) => { this.setState({ appTitle, @@ -136,7 +132,6 @@ export class Header extends Component { recentlyAccessed, navControlsLeft, navControlsRight, - navSetting, currentAppId, }); }, @@ -225,7 +220,6 @@ export class Header extends Component { void; diff --git a/src/core/public/chrome/ui/header/nav_drawer.test.tsx b/src/core/public/chrome/ui/header/nav_drawer.test.tsx deleted file mode 100644 index 7272935b93a52..0000000000000 --- a/src/core/public/chrome/ui/header/nav_drawer.test.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { cloneDeep } from 'lodash'; -import { mount } from 'enzyme'; -import React from 'react'; -import { NavSetting } from './'; -import { ChromeNavLink } from '../../../'; -import { AppCategory } from 'src/core/types'; -import { DEFAULT_APP_CATEGORIES } from '../../../../utils'; -import { NavDrawer } from './nav_drawer'; -import { euiNavLink } from './nav_link'; - -const { analyze, management, observability, security } = DEFAULT_APP_CATEGORIES; -const mockIBasePath = { - get: () => '/app', - prepend: () => '/app', - remove: () => '/app', -}; - -const getMockProps = (chromeNavLinks: ChromeNavLink[], navSetting: NavSetting = 'grouped') => ({ - navSetting, - navLinks: chromeNavLinks.map(link => - euiNavLink(link, true, undefined, mockIBasePath, () => Promise.resolve()) - ), - chromeNavLinks, - recentlyAccessedItems: [], - basePath: mockIBasePath, -}); - -const makeLink = (id: string, order: number, category?: AppCategory) => ({ - id, - category, - order, - title: id, - baseUrl: `http://localhost:5601/app/${id}`, - legacy: true, -}); - -const getMockChromeNavLink = () => - cloneDeep([ - makeLink('discover', 100, analyze), - makeLink('siem', 500, security), - makeLink('metrics', 600, observability), - makeLink('monitoring', 800, management), - makeLink('visualize', 200, analyze), - makeLink('dashboard', 300, analyze), - makeLink('canvas', 400, { label: 'customCategory' }), - makeLink('logs', 700, observability), - ]); - -describe('NavDrawer', () => { - describe('Advanced setting set to individual', () => { - it('renders individual items', () => { - const component = mount( - - ); - expect(component).toMatchSnapshot(); - }); - }); - describe('Advanced setting set to grouped', () => { - it('renders individual items if there are less than 7', () => { - const links = getMockChromeNavLink().slice(0, 5); - const component = mount(); - expect(component).toMatchSnapshot(); - }); - it('renders individual items if there is only 1 category', () => { - // management doesn't count as a category - const navLinks = [ - makeLink('discover', 100, analyze), - makeLink('siem', 500, analyze), - makeLink('metrics', 600, analyze), - makeLink('monitoring', 800, analyze), - makeLink('visualize', 200, analyze), - makeLink('dashboard', 300, management), - makeLink('canvas', 400, management), - makeLink('logs', 700, management), - ]; - const component = mount(); - expect(component).toMatchSnapshot(); - }); - it('renders grouped items', () => { - const component = mount(); - expect(component).toMatchSnapshot(); - }); - }); -}); diff --git a/src/core/public/chrome/ui/header/nav_drawer.tsx b/src/core/public/chrome/ui/header/nav_drawer.tsx index dbb68d5dd3901..c57faec1e428d 100644 --- a/src/core/public/chrome/ui/header/nav_drawer.tsx +++ b/src/core/public/chrome/ui/header/nav_drawer.tsx @@ -18,39 +18,16 @@ */ import React from 'react'; -import { groupBy, sortBy } from 'lodash'; import { i18n } from '@kbn/i18n'; // @ts-ignore import { EuiNavDrawer, EuiHorizontalRule, EuiNavDrawerGroup } from '@elastic/eui'; -import { NavSetting, OnIsLockedUpdate } from './'; +import { OnIsLockedUpdate } from './'; import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../../..'; -import { AppCategory } from '../../../../types'; import { HttpStart } from '../../../http'; import { NavLink } from './nav_link'; import { RecentLinks } from './recent_links'; -function getAllCategories(allCategorizedLinks: Record) { - const allCategories = {} as Record; - - for (const [key, value] of Object.entries(allCategorizedLinks)) { - allCategories[key] = value[0].category; - } - - return allCategories; -} - -function getOrderedCategories( - mainCategories: Record, - categoryDictionary: ReturnType -) { - return sortBy( - Object.keys(mainCategories), - categoryName => categoryDictionary[categoryName]?.order - ); -} - export interface Props { - navSetting: NavSetting; isLocked?: boolean; onIsLockedUpdate?: OnIsLockedUpdate; navLinks: NavLink[]; @@ -60,26 +37,9 @@ export interface Props { } function navDrawerRenderer( - { - navSetting, - isLocked, - onIsLockedUpdate, - navLinks, - chromeNavLinks, - recentlyAccessedItems, - basePath, - }: Props, + { isLocked, onIsLockedUpdate, navLinks, chromeNavLinks, recentlyAccessedItems, basePath }: Props, ref: React.Ref ) { - const disableGroupedNavSetting = navSetting === 'individual'; - const groupedNavLinks = groupBy(navLinks, link => link?.category?.label); - const { undefined: unknowns, ...allCategorizedLinks } = groupedNavLinks; - const { Management: management, ...mainCategories } = allCategorizedLinks; - const categoryDictionary = getAllCategories(allCategorizedLinks); - const orderedCategories = getOrderedCategories(mainCategories, categoryDictionary); - const showUngroupedNav = - disableGroupedNavSetting || navLinks.length < 7 || Object.keys(mainCategories).length === 1; - return ( - {showUngroupedNav ? ( - - ) : ( - <> - { - const category = categoryDictionary[categoryName]!; - const links = mainCategories[categoryName]; - - if (links.length === 1) { - return { - ...links[0], - label: category.label, - iconType: category.euiIconType || links[0].iconType, - }; - } - - return { - 'data-test-subj': 'navDrawerCategory', - iconType: category.euiIconType, - label: category.label, - flyoutMenu: { - title: category.label, - listItems: sortBy(links, 'order').map(link => { - link['data-test-subj'] = 'navDrawerFlyoutLink'; - return link; - }), - }, - }; - }), - ...sortBy(unknowns, 'order'), - ]} - /> - - { - link['data-test-subj'] = 'navDrawerFlyoutLink'; - return link; - }), - }, - }, - ]} - /> - - )} + ); } diff --git a/src/core/public/http/base_path.test.ts b/src/core/public/http/base_path.test.ts index 63b7fa61cee84..6468e674d5e78 100644 --- a/src/core/public/http/base_path.test.ts +++ b/src/core/public/http/base_path.test.ts @@ -88,4 +88,14 @@ describe('BasePath', () => { }); }); }); + + describe('serverBasePath', () => { + it('defaults to basePath', () => { + expect(new BasePath('/foo/bar').serverBasePath).toEqual('/foo/bar'); + }); + + it('returns value when passed into constructor', () => { + expect(new BasePath('/foo/bar', '/foo').serverBasePath).toEqual('/foo'); + }); + }); }); diff --git a/src/core/public/http/base_path.ts b/src/core/public/http/base_path.ts index 6352327c41625..67464a6196b02 100644 --- a/src/core/public/http/base_path.ts +++ b/src/core/public/http/base_path.ts @@ -38,7 +38,10 @@ import { modifyUrl } from '../../utils'; export class BasePath { - constructor(private readonly basePath: string = '') {} + constructor( + private readonly basePath: string = '', + public readonly serverBasePath: string = basePath + ) {} public get = () => { return this.basePath; diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index 8965747ba6837..44fc9d65565d4 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -39,7 +39,10 @@ export class HttpService implements CoreService { public setup({ injectedMetadata, fatalErrors }: HttpDeps): HttpSetup { const kibanaVersion = injectedMetadata.getKibanaVersion(); - const basePath = new BasePath(injectedMetadata.getBasePath()); + const basePath = new BasePath( + injectedMetadata.getBasePath(), + injectedMetadata.getServerBasePath() + ); const fetchService = new Fetch({ basePath, kibanaVersion }); const loadingCount = this.loadingCount.setup({ fatalErrors }); diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index 5909572c7e545..6370ae165282b 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -94,6 +94,13 @@ export interface IBasePath { * Removes the prepended basePath from the `path`. */ remove: (url: string) => string; + + /** + * Returns the server's root basePath as configured, without any namespace prefix. + * + * See {@link BasePath.get} for getting the basePath value for a specific request + */ + readonly serverBasePath: string; } /** diff --git a/src/core/public/injected_metadata/injected_metadata_service.mock.ts b/src/core/public/injected_metadata/injected_metadata_service.mock.ts index 3c06f40d976db..5caa9830a643d 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.mock.ts @@ -21,6 +21,7 @@ import { InjectedMetadataService, InjectedMetadataSetup } from './injected_metad const createSetupContractMock = () => { const setupContract: jest.Mocked = { getBasePath: jest.fn(), + getServerBasePath: jest.fn(), getKibanaVersion: jest.fn(), getKibanaBranch: jest.fn(), getCspConfig: jest.fn(), diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 64a8b8a855fb4..75abdd6d87d5a 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -54,6 +54,7 @@ export interface InjectedMetadataParams { buildNumber: number; branch: string; basePath: string; + serverBasePath: string; category?: AppCategory; csp: { warnLegacyBrowsers: boolean; @@ -115,6 +116,10 @@ export class InjectedMetadataService { return this.state.basePath; }, + getServerBasePath: () => { + return this.state.serverBasePath; + }, + getKibanaVersion: () => { return this.state.version; }, @@ -161,6 +166,7 @@ export class InjectedMetadataService { */ export interface InjectedMetadataSetup { getBasePath: () => string; + getServerBasePath: () => string; getKibanaBuildNumber: () => number; getKibanaBranch: () => string; getKibanaVersion: () => string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index f0289cc2b8355..ba1988b857385 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -718,6 +718,8 @@ export interface IBasePath { get: () => string; prepend: (url: string) => string; remove: (url: string) => string; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "BasePath" + readonly serverBasePath: string; } // @public @@ -930,7 +932,7 @@ export type RecursiveReadonly = T extends (...args: any[]) => any ? T : T ext }> : T; // @public (undocumented) -export interface SavedObject { +export interface SavedObject { attributes: T; // (undocumented) error?: { @@ -973,13 +975,13 @@ export interface SavedObjectsBaseOptions { } // @public (undocumented) -export interface SavedObjectsBatchResponse { +export interface SavedObjectsBatchResponse { // (undocumented) savedObjects: Array>; } // @public (undocumented) -export interface SavedObjectsBulkCreateObject extends SavedObjectsCreateOptions { +export interface SavedObjectsBulkCreateObject extends SavedObjectsCreateOptions { // (undocumented) attributes: T; // (undocumented) @@ -992,7 +994,7 @@ export interface SavedObjectsBulkCreateOptions { } // @public (undocumented) -export interface SavedObjectsBulkUpdateObject { +export interface SavedObjectsBulkUpdateObject { // (undocumented) attributes: T; // (undocumented) @@ -1015,17 +1017,17 @@ export interface SavedObjectsBulkUpdateOptions { export class SavedObjectsClient { // @internal constructor(http: HttpSetup); - bulkCreate: (objects?: SavedObjectsBulkCreateObject[], options?: SavedObjectsBulkCreateOptions) => Promise>; + bulkCreate: (objects?: SavedObjectsBulkCreateObject[], options?: SavedObjectsBulkCreateOptions) => Promise>; bulkGet: (objects?: { id: string; type: string; - }[]) => Promise>; - bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; - create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; + }[]) => Promise>; + bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; + create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; delete: (type: string, id: string) => Promise<{}>; - find: (options: Pick) => Promise>; - get: (type: string, id: string) => Promise>; - update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; + find: (options: Pick) => Promise>; + get: (type: string, id: string) => Promise>; + update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; } // @public @@ -1067,7 +1069,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { } // @public -export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse { +export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse { // (undocumented) page: number; // (undocumented) @@ -1174,7 +1176,7 @@ export interface SavedObjectsUpdateOptions { } // @public -export class SimpleSavedObject { +export class SimpleSavedObject { constructor(client: SavedObjectsClientContract, { id, type, version, attributes, error, references, migrationVersion }: SavedObject); // (undocumented) attributes: T; diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index ccb23793a8534..afc77806afb91 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -22,7 +22,6 @@ import { resolve as resolveUrl } from 'url'; import { SavedObject, - SavedObjectAttributes, SavedObjectReference, SavedObjectsClientContract as SavedObjectsApi, SavedObjectsFindOptions as SavedObjectFindOptionsServer, @@ -61,9 +60,7 @@ export interface SavedObjectsCreateOptions { * * @public */ -export interface SavedObjectsBulkCreateObject< - T extends SavedObjectAttributes = SavedObjectAttributes -> extends SavedObjectsCreateOptions { +export interface SavedObjectsBulkCreateObject extends SavedObjectsCreateOptions { type: string; attributes: T; } @@ -75,9 +72,7 @@ export interface SavedObjectsBulkCreateOptions { } /** @public */ -export interface SavedObjectsBulkUpdateObject< - T extends SavedObjectAttributes = SavedObjectAttributes -> { +export interface SavedObjectsBulkUpdateObject { type: string; id: string; attributes: T; @@ -99,9 +94,7 @@ export interface SavedObjectsUpdateOptions { } /** @public */ -export interface SavedObjectsBatchResponse< - T extends SavedObjectAttributes = SavedObjectAttributes -> { +export interface SavedObjectsBatchResponse { savedObjects: Array>; } @@ -113,9 +106,7 @@ export interface SavedObjectsBatchResponse< * * @public */ -export interface SavedObjectsFindResponsePublic< - T extends SavedObjectAttributes = SavedObjectAttributes -> extends SavedObjectsBatchResponse { +export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse { total: number; perPage: number; page: number; @@ -124,7 +115,7 @@ export interface SavedObjectsFindResponsePublic< interface BatchQueueEntry { type: string; id: string; - resolve: (value: SimpleSavedObject | SavedObject) => void; + resolve: (value: SimpleSavedObject | SavedObject) => void; reject: (reason?: any) => void; } @@ -207,7 +198,7 @@ export class SavedObjectsClient { * @param options * @returns */ - public create = ( + public create = ( type: string, attributes: T, options: SavedObjectsCreateOptions = {} @@ -300,7 +291,7 @@ export class SavedObjectsClient { * @property {object} [options.hasReference] - { type, id } * @returns A find result with objects matching the specified search. */ - public find = ( + public find = ( options: SavedObjectsFindOptions ): Promise> => { const path = this.getPath(['_find']); @@ -348,10 +339,7 @@ export class SavedObjectsClient { * @param {string} id * @returns The saved object for the given type and id. */ - public get = ( - type: string, - id: string - ): Promise> => { + public get = (type: string, id: string): Promise> => { if (!type || !id) { return Promise.reject(new Error('requires type and id')); } @@ -402,7 +390,7 @@ export class SavedObjectsClient { * @prop {object} options.migrationVersion - The optional migrationVersion of this document * @returns */ - public update( + public update( type: string, id: string, attributes: T, @@ -434,7 +422,7 @@ export class SavedObjectsClient { * @param {array} objects - [{ type, id, attributes, options: { version, references } }] * @returns The result of the update operation containing both failed and updated saved objects. */ - public bulkUpdate(objects: SavedObjectsBulkUpdateObject[] = []) { + public bulkUpdate(objects: SavedObjectsBulkUpdateObject[] = []) { const path = this.getPath(['_bulk_update']); return this.savedObjectsFetch(path, { @@ -449,9 +437,7 @@ export class SavedObjectsClient { }); } - private createSavedObject( - options: SavedObject - ): SimpleSavedObject { + private createSavedObject(options: SavedObject): SimpleSavedObject { return new SimpleSavedObject(this, options); } diff --git a/src/legacy/ui/public/saved_objects/__tests__/simple_saved_object.js b/src/core/public/saved_objects/simple_saved_object.test.ts similarity index 67% rename from src/legacy/ui/public/saved_objects/__tests__/simple_saved_object.js rename to src/core/public/saved_objects/simple_saved_object.test.ts index f2fc9bfe232e2..99676f6b78d42 100644 --- a/src/legacy/ui/public/saved_objects/__tests__/simple_saved_object.js +++ b/src/core/public/saved_objects/simple_saved_object.test.ts @@ -17,36 +17,43 @@ * under the License. */ -import sinon from 'sinon'; -import expect from '@kbn/expect'; -import { SimpleSavedObject } from '../../../../../core/public'; +import { SavedObject } from '../../server'; +import { SimpleSavedObject } from './simple_saved_object'; +import { SavedObjectsClientContract } from './saved_objects_client'; describe('SimpleSavedObject', () => { + let client: SavedObjectsClientContract; + + beforeEach(() => { + client = { + update: jest.fn(), + create: jest.fn(), + delete: jest.fn(), + } as any; + }); + it('persists type and id', () => { const id = 'logstash-*'; const type = 'index-pattern'; - const client = sinon.stub(); - const savedObject = new SimpleSavedObject(client, { id, type }); + const savedObject = new SimpleSavedObject(client, { id, type } as SavedObject); - expect(savedObject.id).to.be(id); - expect(savedObject.type).to.be(type); + expect(savedObject.id).toEqual(id); + expect(savedObject.type).toEqual(type); }); it('persists attributes', () => { const attributes = { title: 'My title' }; - const client = sinon.stub(); - const savedObject = new SimpleSavedObject(client, { attributes }); + const savedObject = new SimpleSavedObject(client, { attributes } as SavedObject); - expect(savedObject.attributes).to.be(attributes); + expect(savedObject.attributes).toEqual(attributes); }); it('persists version', () => { - const version = 2; + const version = '2'; - const client = sinon.stub(); - const savedObject = new SimpleSavedObject(client, { version }); - expect(savedObject._version).to.be(version); + const savedObject = new SimpleSavedObject(client, { version } as SavedObject); + expect(savedObject._version).toEqual(version); }); }); diff --git a/src/core/public/saved_objects/simple_saved_object.ts b/src/core/public/saved_objects/simple_saved_object.ts index 8e464680bcf17..d3ba506b865a4 100644 --- a/src/core/public/saved_objects/simple_saved_object.ts +++ b/src/core/public/saved_objects/simple_saved_object.ts @@ -18,7 +18,7 @@ */ import { get, has, set } from 'lodash'; -import { SavedObject as SavedObjectType, SavedObjectAttributes } from '../../server'; +import { SavedObject as SavedObjectType } from '../../server'; import { SavedObjectsClientContract } from './saved_objects_client'; /** @@ -30,7 +30,7 @@ import { SavedObjectsClientContract } from './saved_objects_client'; * * @public */ -export class SimpleSavedObject { +export class SimpleSavedObject { public attributes: T; // We want to use the same interface this class had in JS public _version?: SavedObjectType['version']; @@ -46,7 +46,7 @@ export class SimpleSavedObject { ) { this.id = id; this.type = type; - this.attributes = attributes || {}; + this.attributes = attributes || ({} as T); this.references = references || []; this._version = version; this.migrationVersion = migrationVersion; diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index 50866e5550d8e..b2f4e388b337d 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -31,7 +31,12 @@ export const DEFAULT_API_VERSION = 'master'; export type ElasticsearchConfigType = TypeOf; type SslConfigSchema = ElasticsearchConfigType['ssl']; -const configSchema = schema.object({ +/** + * Validation schema for elasticsearch service config. It can be reused when plugins allow users + * to specify a local elasticsearch config. + * @public + */ +export const configSchema = schema.object({ sniffOnStart: schema.boolean({ defaultValue: false }), sniffInterval: schema.oneOf([schema.duration(), schema.literal(false)], { defaultValue: false, @@ -148,6 +153,10 @@ export const config: ServiceConfigDescriptor = { deprecations, }; +/** + * Wrapper of config schema. + * @public + */ export class ElasticsearchConfig { /** * The interval between health check requests Kibana sends to the Elasticsearch. diff --git a/src/core/server/elasticsearch/index.ts b/src/core/server/elasticsearch/index.ts index 5d64fadfaa184..cfd72a6fd5e47 100644 --- a/src/core/server/elasticsearch/index.ts +++ b/src/core/server/elasticsearch/index.ts @@ -27,7 +27,7 @@ export { } from './cluster_client'; export { IScopedClusterClient, ScopedClusterClient, Headers } from './scoped_cluster_client'; export { ElasticsearchClientConfig } from './elasticsearch_client_config'; -export { config } from './elasticsearch_config'; +export { config, configSchema, ElasticsearchConfig } from './elasticsearch_config'; export { ElasticsearchError, ElasticsearchErrorHelpers } from './errors'; export * from './api_types'; export * from './types'; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index cc838ddd1351d..52827b72ee0cc 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -39,7 +39,12 @@ * @packageDocumentation */ -import { ElasticsearchServiceSetup, IScopedClusterClient } from './elasticsearch'; +import { + ElasticsearchServiceSetup, + IScopedClusterClient, + configSchema as elasticsearchConfigSchema, +} from './elasticsearch'; + import { HttpServiceSetup } from './http'; import { IScopedRenderingClient } from './rendering'; import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins'; @@ -78,6 +83,7 @@ export { Headers, ScopedClusterClient, IScopedClusterClient, + ElasticsearchConfig, ElasticsearchClientConfig, ElasticsearchError, ElasticsearchErrorHelpers, @@ -347,3 +353,14 @@ export { PluginOpaqueId, UuidServiceSetup, }; + +/** + * Config schemas for the platform services. + * + * @alpha + */ +export const config = { + elasticsearch: { + schema: elasticsearchConfigSchema, + }, +}; diff --git a/src/core/server/logging/README.md b/src/core/server/logging/README.md index 3fbec7a45148d..ed64e7c4ce0b1 100644 --- a/src/core/server/logging/README.md +++ b/src/core/server/logging/README.md @@ -7,6 +7,8 @@ - [JSON layout](#json-layout) - [Configuration](#configuration) - [Usage](#usage) +- [Logging config migration](#logging-config-migration) +- [Log record format changes](#log-record-format-changes) The way logging works in Kibana is inspired by `log4j 2` logging framework used by [Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html#logging). The main idea is to have consistent logging behaviour (configuration, log format etc.) across the entire Elastic Stack @@ -321,3 +323,23 @@ Define a custom logger for a specific context. #### logging.filter TBD + +### Log record format changes + +| Parameter | Platform log record in **pattern** format | Legacy Platform log record **text** format | +| --------------- | ------------------------------------------ | ------------------------------------------ | +| @timestamp | ISO8601 `2012-01-31T23:33:22.011Z` | Absolute `23:33:22.011` | +| context | `parent.child` | `['parent', 'child']` | +| level | `DEBUG` | `['debug']` | +| meta | stringified JSON object `{"to": "v8"}` | N/A | +| pid | can be configured as `%pid` | N/A | + +| Parameter | Platform log record in **json** format | Legacy Platform log record **json** format | +| --------------- | ------------------------------------------ | -------------------------------------------- | +| @timestamp | ISO8601_TZ `2012-01-31T23:33:22.011-05:00` | ISO8601 `2012-01-31T23:33:22.011Z` | +| context | `context: parent.child` | `tags: ['parent', 'child']` | +| level | `level: DEBUG` | `tags: ['debug']` | +| meta | separate property `"meta": {"to": "v8"}` | merged in log record `{... "to": "v8"}` | +| pid | `pid: 12345` | `pid: 12345` | +| type | N/A | `type: log` | +| error | `{ message, name, stack }` | `{ message, name, stack, code, signal }` | \ No newline at end of file diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index 5e6e977663bc4..3b11313367d9c 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -65,6 +65,7 @@ Object { "version": Any, }, "legacyMode": false, + "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, "version": Any, @@ -136,6 +137,7 @@ Object { "version": Any, }, "legacyMode": false, + "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, "version": Any, @@ -211,6 +213,7 @@ Object { "version": Any, }, "legacyMode": false, + "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, "version": Any, @@ -282,6 +285,7 @@ Object { "version": Any, }, "legacyMode": false, + "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, "version": Any, @@ -353,6 +357,7 @@ Object { "version": Any, }, "legacyMode": false, + "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, "version": Any, @@ -424,6 +429,7 @@ Object { "version": Any, }, "legacyMode": true, + "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, "version": Any, @@ -495,6 +501,7 @@ Object { "version": Any, }, "legacyMode": true, + "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, "version": Any, @@ -566,6 +573,7 @@ Object { "version": Any, }, "legacyMode": true, + "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object { "fake": "__TEST_TOKEN__", @@ -639,6 +647,7 @@ Object { "version": Any, }, "legacyMode": true, + "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, "version": Any, @@ -710,6 +719,7 @@ Object { "version": Any, }, "legacyMode": true, + "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object { "fake": "__TEST_TOKEN__", diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index 11d1fb271c81d..dbafd5806bd74 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -60,6 +60,7 @@ export class RenderingService implements CoreService { ) => { const { env } = this.coreContext; const basePath = http.basePath.get(request); + const serverBasePath = http.basePath.serverBasePath; const settings = { defaults: uiSettings.getRegistered(), user: includeUserSettings ? await uiSettings.getUserProvided() : {}, @@ -79,6 +80,7 @@ export class RenderingService implements CoreService { buildNumber: env.packageInfo.buildNum, branch: env.packageInfo.branch, basePath, + serverBasePath, env, legacyMode: appId !== 'core', i18n: { diff --git a/src/core/server/rendering/types.ts b/src/core/server/rendering/types.ts index 3f9f6ff294909..cfaa23d491139 100644 --- a/src/core/server/rendering/types.ts +++ b/src/core/server/rendering/types.ts @@ -39,6 +39,7 @@ export interface RenderingMetadata { buildNumber: number; branch: string; basePath: string; + serverBasePath: string; env: Env; legacyMode: boolean; i18n: { diff --git a/src/core/server/saved_objects/import/collect_saved_objects.ts b/src/core/server/saved_objects/import/collect_saved_objects.ts index 65ffd4d9a1d57..1a8ede41d0b2c 100644 --- a/src/core/server/saved_objects/import/collect_saved_objects.ts +++ b/src/core/server/saved_objects/import/collect_saved_objects.ts @@ -42,10 +42,10 @@ export async function collectSavedObjects({ supportedTypes, }: CollectSavedObjectsOptions) { const errors: SavedObjectsImportError[] = []; - const collectedObjects: SavedObject[] = await createPromiseFromStreams([ + const collectedObjects: Array> = await createPromiseFromStreams([ readStream, createLimitStream(objectLimit), - createFilterStream(obj => { + createFilterStream>(obj => { if (supportedTypes.includes(obj.type)) { return true; } diff --git a/src/core/server/saved_objects/import/extract_errors.ts b/src/core/server/saved_objects/import/extract_errors.ts index 725e935f6e21d..5728ce8b7b59f 100644 --- a/src/core/server/saved_objects/import/extract_errors.ts +++ b/src/core/server/saved_objects/import/extract_errors.ts @@ -20,11 +20,12 @@ import { SavedObject } from '../types'; import { SavedObjectsImportError } from './types'; export function extractErrors( - savedObjectResults: SavedObject[], - savedObjectsToImport: SavedObject[] + // TODO: define saved object type + savedObjectResults: Array>, + savedObjectsToImport: Array> ) { const errors: SavedObjectsImportError[] = []; - const originalSavedObjectsMap = new Map(); + const originalSavedObjectsMap = new Map>(); for (const savedObject of savedObjectsToImport) { originalSavedObjectsMap.set(`${savedObject.type}:${savedObject.id}`, savedObject); } diff --git a/src/core/server/saved_objects/import/validate_references.ts b/src/core/server/saved_objects/import/validate_references.ts index 4d9ee59f9df15..f0c033c1d00b4 100644 --- a/src/core/server/saved_objects/import/validate_references.ts +++ b/src/core/server/saved_objects/import/validate_references.ts @@ -77,7 +77,7 @@ export async function getNonExistingReferenceAsKeys( } export async function validateReferences( - savedObjects: SavedObject[], + savedObjects: Array>, savedObjectsClient: SavedObjectsClientContract, namespace?: string ) { diff --git a/src/core/server/saved_objects/management/management.ts b/src/core/server/saved_objects/management/management.ts index 7b5274da91fc8..b7dce2c087c5f 100644 --- a/src/core/server/saved_objects/management/management.ts +++ b/src/core/server/saved_objects/management/management.ts @@ -23,9 +23,9 @@ interface SavedObjectsManagementTypeDefinition { isImportableAndExportable?: boolean; defaultSearchField?: string; icon?: string; - getTitle?: (savedObject: SavedObject) => string; - getEditUrl?: (savedObject: SavedObject) => string; - getInAppUrl?: (savedObject: SavedObject) => { path: string; uiCapabilitiesPath: string }; + getTitle?: (savedObject: SavedObject) => string; + getEditUrl?: (savedObject: SavedObject) => string; + getInAppUrl?: (savedObject: SavedObject) => { path: string; uiCapabilitiesPath: string }; } export interface SavedObjectsManagementDefinition { diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts index aaf6f45c244ec..524c2c8ffae7a 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -50,7 +50,7 @@ export interface SavedObjectsRawDocSource { * scenario out of the box. */ interface SavedObjectDoc { - attributes: object; + attributes: unknown; id?: string; // NOTE: SavedObjectDoc is used for uncreated objects where `id` is optional type: string; namespace?: string; diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index b485b8dfe398c..72a7867854b60 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -48,7 +48,6 @@ import { } from '../saved_objects_client'; import { SavedObject, - SavedObjectAttributes, SavedObjectsBaseOptions, SavedObjectsFindOptions, SavedObjectsMigrationVersion, @@ -213,7 +212,7 @@ export class SavedObjectsRepository { * @property {array} [options.references=[]] - [{ name, type, id }] * @returns {promise} - { id, type, version, attributes } */ - public async create( + public async create( type: string, attributes: T, options: SavedObjectsCreateOptions = {} @@ -254,7 +253,7 @@ export class SavedObjectsRepository { body: raw._source, }); - return this._rawToSavedObject({ + return this._rawToSavedObject({ ...raw, ...response, }); @@ -277,7 +276,7 @@ export class SavedObjectsRepository { * @property {string} [options.namespace] * @returns {promise} - {saved_objects: [[{ id, type, version, references, attributes, error: { message } }]} */ - async bulkCreate( + async bulkCreate( objects: Array>, options: SavedObjectsCreateOptions = {} ): Promise> { @@ -464,7 +463,7 @@ export class SavedObjectsRepository { * @property {object} [options.hasReference] - { type, id } * @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page } */ - async find({ + async find({ search, defaultSearchOperator = 'OR', searchFields, @@ -577,7 +576,7 @@ export class SavedObjectsRepository { * { id: 'foo', type: 'index-pattern' } * ]) */ - async bulkGet( + async bulkGet( objects: SavedObjectsBulkGetObject[] = [], options: SavedObjectsBaseOptions = {} ): Promise> { @@ -648,7 +647,7 @@ export class SavedObjectsRepository { * @property {string} [options.namespace] * @returns {promise} - { id, type, version, attributes } */ - async get( + async get( type: string, id: string, options: SavedObjectsBaseOptions = {} @@ -696,7 +695,7 @@ export class SavedObjectsRepository { * @property {array} [options.references] - [{ name, type, id }] * @returns {promise} */ - async update( + async update( type: string, id: string, attributes: Partial, @@ -753,7 +752,7 @@ export class SavedObjectsRepository { * @property {string} [options.namespace] * @returns {promise} - {saved_objects: [[{ id, type, version, references, attributes, error: { message } }]} */ - async bulkUpdate( + async bulkUpdate( objects: Array>, options: SavedObjectsBulkUpdateOptions = {} ): Promise> { @@ -972,7 +971,7 @@ export class SavedObjectsRepository { // includes the namespace, and we use this for migrating documents. However, we don't // want the namespace to be returned from the repository, as the repository scopes each // method transparently to the specified namespace. - private _rawToSavedObject(raw: SavedObjectsRawDoc): SavedObject { + private _rawToSavedObject(raw: SavedObjectsRawDoc): SavedObject { const savedObject = this._serializer.rawToSavedObject(raw); return omit(savedObject, 'namespace'); } diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index b0b2633646e10..70d69374ba8fe 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -20,7 +20,6 @@ import { ISavedObjectsRepository } from './lib'; import { SavedObject, - SavedObjectAttributes, SavedObjectReference, SavedObjectsMigrationVersion, SavedObjectsBaseOptions, @@ -49,7 +48,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { * * @public */ -export interface SavedObjectsBulkCreateObject { +export interface SavedObjectsBulkCreateObject { id?: string; type: string; attributes: T; @@ -62,7 +61,7 @@ export interface SavedObjectsBulkCreateObject +export interface SavedObjectsBulkUpdateObject extends Pick { /** The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` */ id: string; @@ -76,7 +75,7 @@ export interface SavedObjectsBulkUpdateObject { +export interface SavedObjectsBulkResponse { saved_objects: Array>; } @@ -88,7 +87,7 @@ export interface SavedObjectsBulkResponse * * @public */ -export interface SavedObjectsFindResponse { +export interface SavedObjectsFindResponse { saved_objects: Array>; total: number; per_page: number; @@ -141,7 +140,7 @@ export interface SavedObjectsBulkGetObject { * * @public */ -export interface SavedObjectsBulkResponse { +export interface SavedObjectsBulkResponse { saved_objects: Array>; } @@ -149,7 +148,7 @@ export interface SavedObjectsBulkResponse * * @public */ -export interface SavedObjectsBulkUpdateResponse { +export interface SavedObjectsBulkUpdateResponse { saved_objects: Array>; } @@ -157,7 +156,7 @@ export interface SavedObjectsBulkUpdateResponse +export interface SavedObjectsUpdateResponse extends Omit, 'attributes' | 'references'> { attributes: Partial; references: SavedObjectReference[] | undefined; @@ -185,11 +184,7 @@ export class SavedObjectsClient { * @param attributes * @param options */ - async create( - type: string, - attributes: T, - options?: SavedObjectsCreateOptions - ) { + async create(type: string, attributes: T, options?: SavedObjectsCreateOptions) { return await this._repository.create(type, attributes, options); } @@ -199,7 +194,7 @@ export class SavedObjectsClient { * @param objects * @param options */ - async bulkCreate( + async bulkCreate( objects: Array>, options?: SavedObjectsCreateOptions ) { @@ -222,9 +217,7 @@ export class SavedObjectsClient { * * @param options */ - async find( - options: SavedObjectsFindOptions - ): Promise> { + async find(options: SavedObjectsFindOptions): Promise> { return await this._repository.find(options); } @@ -239,7 +232,7 @@ export class SavedObjectsClient { * { id: 'foo', type: 'index-pattern' } * ]) */ - async bulkGet( + async bulkGet( objects: SavedObjectsBulkGetObject[] = [], options: SavedObjectsBaseOptions = {} ): Promise> { @@ -253,7 +246,7 @@ export class SavedObjectsClient { * @param id - The ID of the SavedObject to retrieve * @param options */ - async get( + async get( type: string, id: string, options: SavedObjectsBaseOptions = {} @@ -268,7 +261,7 @@ export class SavedObjectsClient { * @param id * @param options */ - async update( + async update( type: string, id: string, attributes: Partial, @@ -282,7 +275,7 @@ export class SavedObjectsClient { * * @param objects */ - async bulkUpdate( + async bulkUpdate( objects: Array>, options?: SavedObjectsBulkUpdateOptions ): Promise> { diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index a4fde1765b7d3..9c204784b0aeb 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -33,7 +33,6 @@ export { SavedObjectsImportRetry, } from './import/types'; -import { SavedObjectAttributes } from '../../types'; import { LegacyConfig } from '../legacy'; export { SavedObjectAttributes, @@ -64,7 +63,7 @@ export interface SavedObjectsMigrationVersion { * * @public */ -export interface SavedObject { +export interface SavedObject { /** The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` */ id: string; /** The type of Saved Object. Each plugin can define it's own custom Saved Object types. */ diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index ad1907df571fb..f717f30fdb0cf 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -503,6 +503,49 @@ export class ClusterClient implements IClusterClient { close(): void; } +// @alpha +export const config: { + elasticsearch: { + schema: import("@kbn/config-schema").ObjectType<{ + sniffOnStart: import("@kbn/config-schema").Type; + sniffInterval: import("@kbn/config-schema").Type; + sniffOnConnectionFault: import("@kbn/config-schema").Type; + hosts: import("@kbn/config-schema").Type; + preserveHost: import("@kbn/config-schema").Type; + username: import("@kbn/config-schema").Type; + password: import("@kbn/config-schema").Type; + requestHeadersWhitelist: import("@kbn/config-schema").Type; + customHeaders: import("@kbn/config-schema").Type>; + shardTimeout: import("@kbn/config-schema").Type; + requestTimeout: import("@kbn/config-schema").Type; + pingTimeout: import("@kbn/config-schema").Type; + startupTimeout: import("@kbn/config-schema").Type; + logQueries: import("@kbn/config-schema").Type; + ssl: import("@kbn/config-schema").ObjectType<{ + verificationMode: import("@kbn/config-schema").Type<"none" | "full" | "certificate">; + certificateAuthorities: import("@kbn/config-schema").Type; + certificate: import("@kbn/config-schema").Type; + key: import("@kbn/config-schema").Type; + keyPassphrase: import("@kbn/config-schema").Type; + keystore: import("@kbn/config-schema").ObjectType<{ + path: import("@kbn/config-schema").Type; + password: import("@kbn/config-schema").Type; + }>; + truststore: import("@kbn/config-schema").ObjectType<{ + path: import("@kbn/config-schema").Type; + password: import("@kbn/config-schema").Type; + }>; + alwaysPresentCertificate: import("@kbn/config-schema").Type; + }>; + apiVersion: import("@kbn/config-schema").Type; + healthCheck: import("@kbn/config-schema").ObjectType<{ + delay: import("@kbn/config-schema").Type; + }>; + ignoreVersionMismatch: import("@kbn/config-schema/target/types/types").ConditionalType; + }>; + }; +}; + // @public export type ConfigDeprecation = (config: Record, fromPath: string, logger: ConfigDeprecationLogger) => Record; @@ -650,8 +693,6 @@ export interface DiscoveredPlugin { readonly requiredPlugins: readonly PluginName[]; } -// Warning: (ae-forgotten-export) The symbol "ElasticsearchConfig" needs to be exported by the entry point index.d.ts -// // @public (undocumented) export type ElasticsearchClientConfig = Pick & Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; @@ -660,6 +701,31 @@ export type ElasticsearchClientConfig = Pick; }; +// @public +export class ElasticsearchConfig { + constructor(rawConfig: ElasticsearchConfigType); + readonly apiVersion: string; + // Warning: (ae-forgotten-export) The symbol "ElasticsearchConfigType" needs to be exported by the entry point index.d.ts + readonly customHeaders: ElasticsearchConfigType['customHeaders']; + readonly healthCheckDelay: Duration; + readonly hosts: string[]; + readonly ignoreVersionMismatch: boolean; + readonly logQueries: boolean; + readonly password?: string; + readonly pingTimeout: Duration; + readonly requestHeadersWhitelist: string[]; + readonly requestTimeout: Duration; + readonly shardTimeout: Duration; + readonly sniffInterval: false | Duration; + readonly sniffOnConnectionFault: boolean; + readonly sniffOnStart: boolean; + // Warning: (ae-forgotten-export) The symbol "SslConfigSchema" needs to be exported by the entry point index.d.ts + readonly ssl: Pick> & { + certificateAuthorities?: string[]; + }; + readonly username?: string; +} + // @public (undocumented) export interface ElasticsearchError extends Boom { // (undocumented) @@ -1396,7 +1462,7 @@ export interface RouteValidatorOptions { } // @public (undocumented) -export interface SavedObject { +export interface SavedObject { attributes: T; // (undocumented) error?: { @@ -1458,7 +1524,7 @@ export interface SavedObjectsBaseOptions { } // @public (undocumented) -export interface SavedObjectsBulkCreateObject { +export interface SavedObjectsBulkCreateObject { // (undocumented) attributes: T; // (undocumented) @@ -1480,19 +1546,19 @@ export interface SavedObjectsBulkGetObject { } // @public (undocumented) -export interface SavedObjectsBulkResponse { +export interface SavedObjectsBulkResponse { // (undocumented) saved_objects: Array>; } // @public (undocumented) -export interface SavedObjectsBulkResponse { +export interface SavedObjectsBulkResponse { // (undocumented) saved_objects: Array>; } // @public (undocumented) -export interface SavedObjectsBulkUpdateObject extends Pick { +export interface SavedObjectsBulkUpdateObject extends Pick { attributes: Partial; id: string; type: string; @@ -1504,7 +1570,7 @@ export interface SavedObjectsBulkUpdateOptions extends SavedObjectsBaseOptions { } // @public (undocumented) -export interface SavedObjectsBulkUpdateResponse { +export interface SavedObjectsBulkUpdateResponse { // (undocumented) saved_objects: Array>; } @@ -1513,18 +1579,18 @@ export interface SavedObjectsBulkUpdateResponse(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; - bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; - bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; - create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; + bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; + bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; + bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; + create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; // (undocumented) static errors: typeof SavedObjectsErrorHelpers; // (undocumented) errors: typeof SavedObjectsErrorHelpers; - find(options: SavedObjectsFindOptions): Promise>; - get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; - update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; + find(options: SavedObjectsFindOptions): Promise>; + get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; + update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; } // @public @@ -1706,7 +1772,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { } // @public -export interface SavedObjectsFindResponse { +export interface SavedObjectsFindResponse { // (undocumented) page: number; // (undocumented) @@ -1885,10 +1951,10 @@ export interface SavedObjectsRawDoc { // @public (undocumented) export class SavedObjectsRepository { - bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; - bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; - bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; - create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; + bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; + bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; + bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; + create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; // Warning: (ae-forgotten-export) The symbol "KibanaMigrator" needs to be exported by the entry point index.d.ts // // @internal @@ -1896,8 +1962,8 @@ export class SavedObjectsRepository { delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; // (undocumented) - find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise>; - get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; + find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise>; + get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise<{ id: string; type: string; @@ -1906,7 +1972,7 @@ export class SavedObjectsRepository { version: string; attributes: any; }>; - update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; + update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; } // @public @@ -1996,7 +2062,7 @@ export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions { } // @public (undocumented) -export interface SavedObjectsUpdateResponse extends Omit, 'attributes' | 'references'> { +export interface SavedObjectsUpdateResponse extends Omit, 'attributes' | 'references'> { // (undocumented) attributes: Partial; // (undocumented) @@ -2133,7 +2199,6 @@ export const validBodyOutput: readonly ["data", "stream"]; // src/core/server/plugins/plugins_service.ts:44:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:226:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:226:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:227:3 - (ae-forgotten-export) The symbol "ElasticsearchConfigType" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:228:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts index 0544a1806e09a..55e32b1e3bb37 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts @@ -45,7 +45,10 @@ export async function createOrUpgradeSavedConfig( }); // default to the attributes of the upgradeableConfig if available - const attributes = defaults({ buildNum }, upgradeableConfig ? upgradeableConfig.attributes : {}); + const attributes = defaults( + { buildNum }, + upgradeableConfig ? (upgradeableConfig.attributes as any) : {} + ); try { // create the new SavedConfig diff --git a/src/core/server/ui_settings/ui_settings_client.ts b/src/core/server/ui_settings/ui_settings_client.ts index 3c9c232bff280..a7e55d2b2da65 100644 --- a/src/core/server/ui_settings/ui_settings_client.ts +++ b/src/core/server/ui_settings/ui_settings_client.ts @@ -185,7 +185,7 @@ export class UiSettingsClient implements IUiSettingsClient { autoCreateOrUpgradeIfMissing = true, }: ReadOptions = {}): Promise> { try { - const resp = await this.savedObjectsClient.get(this.type, this.id); + const resp = await this.savedObjectsClient.get>(this.type, this.id); return resp.attributes; } catch (error) { if (SavedObjectsErrorHelpers.isNotFoundError(error) && autoCreateOrUpgradeIfMissing) { diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 34ba25f92beb6..d4d2e86e1e96b 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -142,6 +142,7 @@ kibana_vars=( xpack.code.security.enableGitCertCheck xpack.code.security.gitHostWhitelist xpack.code.security.gitProtocolWhitelist + xpack.encryptedSavedObjects.encryptionKey xpack.graph.enabled xpack.graph.canEditDrillDownUrls xpack.graph.savePolicy diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 1dce53b6c2a84..fb91b865097fa 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -20,6 +20,7 @@ export const storybookAliases = { apm: 'x-pack/legacy/plugins/apm/scripts/storybook.js', canvas: 'x-pack/legacy/plugins/canvas/scripts/storybook_new.js', + drilldowns: 'x-pack/plugins/drilldowns/scripts/storybook.js', embeddable: 'src/plugins/embeddable/scripts/storybook.js', infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js', siem: 'x-pack/legacy/plugins/siem/scripts/storybook.js', diff --git a/src/dev/storybook/run_storybook_cli.ts b/src/dev/storybook/run_storybook_cli.ts index 0f7dc40ceef0b..efb618a48cd6e 100644 --- a/src/dev/storybook/run_storybook_cli.ts +++ b/src/dev/storybook/run_storybook_cli.ts @@ -52,6 +52,7 @@ run( log.verbose('Loading Storybook:', absolute); process.chdir(join(absolute, '..', '..')); + require(absolute); }, { @@ -69,9 +70,10 @@ run( flags: { default: {}, string: [], - boolean: ['clean'], + boolean: ['clean', 'site'], help: ` --clean Clean Storybook build folder. + --site Build static version of Storybook. `, }, } diff --git a/src/fixtures/fake_hierarchical_data.js b/src/fixtures/fake_hierarchical_data.ts similarity index 98% rename from src/fixtures/fake_hierarchical_data.js rename to src/fixtures/fake_hierarchical_data.ts index b4ae02a487049..4480caae39664 100644 --- a/src/fixtures/fake_hierarchical_data.js +++ b/src/fixtures/fake_hierarchical_data.ts @@ -17,16 +17,14 @@ * under the License. */ -const data = {}; - -data.metricOnly = { +export const metricOnly = { hits: { total: 1000, hits: [], max_score: 0 }, aggregations: { agg_1: { value: 412032 }, }, }; -data.threeTermBuckets = { +export const threeTermBuckets = { hits: { total: 1000, hits: [], max_score: 0 }, aggregations: { agg_2: { @@ -129,7 +127,7 @@ data.threeTermBuckets = { }, }; -data.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = { +export const oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = { hits: { total: 1000, hits: [], max_score: 0 }, aggregations: { agg_3: { @@ -520,7 +518,7 @@ data.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = { }, }; -data.oneRangeBucket = { +export const oneRangeBucket = { took: 35, timed_out: false, _shards: { @@ -555,7 +553,7 @@ data.oneRangeBucket = { }, }; -data.oneFilterBucket = { +export const oneFilterBucket = { took: 11, timed_out: false, _shards: { @@ -582,7 +580,7 @@ data.oneFilterBucket = { }, }; -data.oneHistogramBucket = { +export const oneHistogramBucket = { took: 37, timed_out: false, _shards: { @@ -632,5 +630,3 @@ data.oneHistogramBucket = { }, }, }; - -export default data; diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 50120292a627a..8cde5d0a1fc11 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -54,6 +54,7 @@ export * from '../common'; export { FilterStateManager } from './filter/filter_manager'; export { // agg_types TODO need to group these under a namespace or prefix + AggConfigs, AggParamType, AggTypeFilters, // TODO convert to interface aggTypeFilters, @@ -66,6 +67,7 @@ export { convertIPRangeToString, intervalOptions, // only used in Discover isDateHistogramBucketAggConfig, + setBounds, isStringType, isType, isValidInterval, @@ -81,4 +83,6 @@ export { // search_source getRequestInspectorStats, getResponseInspectorStats, + tabifyAggResponse, + tabifyGetColumns, } from './search'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts index 7e7e4944b00da..8e091ed5f21ae 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts @@ -27,7 +27,7 @@ */ import _ from 'lodash'; -import { AggConfig, AggConfigOptions } from './agg_config'; +import { AggConfig, AggConfigOptions, IAggConfig } from './agg_config'; import { Schema } from './schemas'; import { AggGroupNames } from './agg_groups'; import { @@ -63,7 +63,7 @@ export class AggConfigs { public schemas: any; public timeRange?: TimeRange; - aggs: AggConfig[]; + aggs: IAggConfig[]; constructor(indexPattern: IndexPattern, configStates = [] as any, schemas?: any) { configStates = AggConfig.ensureIds(configStates); @@ -74,7 +74,7 @@ export class AggConfigs { configStates.forEach((params: any) => this.createAggConfig(params)); - if (this.schemas) { + if (schemas) { this.initializeDefaultsFromSchemas(schemas); } } diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_params.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_params.ts index 34727ff4614b9..551cb81529a0a 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_params.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_params.ts @@ -76,7 +76,9 @@ export const writeParams = < aggs?: IAggConfigs, locals?: Record ) => { - const output = { params: {} as Record }; + const output: Record = { + params: {} as Record, + }; locals = locals || {}; params.forEach(param => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/index.ts b/src/legacy/core_plugins/data/public/search/aggs/index.ts index 0fef7f38aae74..0bdb92b8de65e 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/index.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/index.ts @@ -50,3 +50,6 @@ export { isValidJson, isValidInterval } from './utils'; export { BUCKET_TYPES } from './buckets/bucket_agg_types'; export { METRIC_TYPES } from './metrics/metric_agg_types'; export { ISchemas, Schema, Schemas } from './schemas'; + +// types +export { IAggConfig, IAggConfigs } from './types'; diff --git a/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts index 8f7953c408a97..e85e9deff6ddf 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts @@ -22,20 +22,8 @@ import { set } from 'lodash'; import { FormattedData } from '../../../../../../plugins/inspector/public'; // @ts-ignore import { createFilter } from './create_filter'; -interface Column { - id: string; - name: string; - aggConfig: any; -} - -interface Row { - [key: string]: any; -} -interface Table { - columns: Column[]; - rows: Row[]; -} +import { TabbedTable } from '../tabify'; /** * @deprecated @@ -52,7 +40,7 @@ interface Table { * inspector. It will only be called when the data view in the inspector is opened. */ export async function buildTabularInspectorData( - table: Table, + table: TabbedTable, queryFilter: { addFilters: (filter: any) => void } ) { const aggConfigs = table.columns.map(column => column.aggConfig); diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts index 9aee7124c9521..302527e4ed549 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -39,8 +39,7 @@ import { import { buildTabularInspectorData } from './build_tabular_inspector_data'; import { calculateObjectHash } from '../../../../visualizations/public'; -// @ts-ignore -import { tabifyAggResponse } from '../../../../../ui/public/agg_response/tabify/tabify'; +import { tabifyAggResponse } from '../../../../../core_plugins/data/public'; import { PersistedState } from '../../../../../ui/public/persisted_state'; import { Adapters } from '../../../../../../plugins/inspector/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths diff --git a/src/legacy/core_plugins/data/public/search/index.ts b/src/legacy/core_plugins/data/public/search/index.ts index 90e191b769a8d..96d2825559da2 100644 --- a/src/legacy/core_plugins/data/public/search/index.ts +++ b/src/legacy/core_plugins/data/public/search/index.ts @@ -20,3 +20,4 @@ export * from './aggs'; export { getRequestInspectorStats, getResponseInspectorStats } from './utils'; export { serializeAggConfig } from './expressions/utils'; +export { tabifyAggResponse, tabifyGetColumns } from './tabify'; diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/_buckets.js b/src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts similarity index 66% rename from src/legacy/ui/public/agg_response/tabify/__tests__/_buckets.js rename to src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts index b85b45d3c5820..ef2748102623a 100644 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/_buckets.js +++ b/src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts @@ -17,31 +17,36 @@ * under the License. */ -import expect from '@kbn/expect'; -import { TabifyBuckets } from '../_buckets'; +import { TabifyBuckets } from './buckets'; +import { AggGroupNames } from '../aggs'; -describe('Buckets wrapper', function() { - function test(aggResp, count, keys) { - it('reads the length', function() { +jest.mock('ui/new_platform'); + +describe('Buckets wrapper', () => { + const check = (aggResp: any, count: number, keys: string[]) => { + test('reads the length', () => { const buckets = new TabifyBuckets(aggResp); - expect(buckets).to.have.length(count); + expect(buckets).toHaveLength(count); }); - it('iterates properly, passing in the key', function() { + test('iterates properly, passing in the key', () => { const buckets = new TabifyBuckets(aggResp); - const keysSent = []; - buckets.forEach(function(bucket, key) { - keysSent.push(key); + const keysSent: any[] = []; + + buckets.forEach((bucket, key) => { + if (key) { + keysSent.push(key); + } }); - expect(keysSent).to.have.length(count); - expect(keysSent).to.eql(keys); + expect(keysSent).toHaveLength(count); + expect(keysSent).toEqual(keys); }); - } + }; - describe('with object style buckets', function() { - const aggResp = { - buckets: { + describe('with object style buckets', () => { + let aggResp: any = { + [AggGroupNames.Buckets]: { '0-100': {}, '100-200': {}, '200-300': {}, @@ -51,11 +56,11 @@ describe('Buckets wrapper', function() { const count = 3; const keys = ['0-100', '100-200', '200-300']; - test(aggResp, count, keys); + check(aggResp, count, keys); - it('should accept filters agg queries with strings', () => { - const aggResp = { - buckets: { + test('should accept filters agg queries with strings', () => { + aggResp = { + [AggGroupNames.Buckets]: { 'response:200': {}, 'response:404': {}, }, @@ -75,15 +80,17 @@ describe('Buckets wrapper', function() { }; const buckets = new TabifyBuckets(aggResp, aggParams); - expect(buckets).to.have.length(2); + + expect(buckets).toHaveLength(2); + buckets._keys.forEach(key => { - expect(key).to.be.a('string'); + expect(typeof key).toBe('string'); }); }); - it('should accept filters agg queries with query_string queries', () => { - const aggResp = { - buckets: { + test('should accept filters agg queries with query_string queries', () => { + aggResp = { + [AggGroupNames.Buckets]: { 'response:200': {}, 'response:404': {}, }, @@ -103,15 +110,17 @@ describe('Buckets wrapper', function() { }; const buckets = new TabifyBuckets(aggResp, aggParams); - expect(buckets).to.have.length(2); + + expect(buckets).toHaveLength(2); + buckets._keys.forEach(key => { - expect(key).to.be.a('string'); + expect(typeof key).toBe('string'); }); }); - it('should accept filters agg queries with query dsl queries', () => { - const aggResp = { - buckets: { + test('should accept filters agg queries with query dsl queries', () => { + aggResp = { + [AggGroupNames.Buckets]: { '{match_all: {}}': {}, }, }; @@ -126,16 +135,18 @@ describe('Buckets wrapper', function() { }; const buckets = new TabifyBuckets(aggResp, aggParams); - expect(buckets).to.have.length(1); + + expect(buckets).toHaveLength(1); + buckets._keys.forEach(key => { - expect(key).to.be.a('string'); + expect(typeof key).toBe('string'); }); }); }); - describe('with array style buckets', function() { + describe('with array style buckets', () => { const aggResp = { - buckets: [ + [AggGroupNames.Buckets]: [ { key: '0-100', value: {} }, { key: '100-200', value: {} }, { key: '200-300', value: {} }, @@ -145,23 +156,24 @@ describe('Buckets wrapper', function() { const count = 3; const keys = ['0-100', '100-200', '200-300']; - test(aggResp, count, keys); + check(aggResp, count, keys); }); - describe('with single bucket aggregations (filter)', function() { - it('creates single bucket from agg content', function() { + describe('with single bucket aggregations (filter)', () => { + test('creates single bucket from agg content', () => { const aggResp = { single_bucket: {}, doc_count: 5, }; const buckets = new TabifyBuckets(aggResp); - expect(buckets).to.have.length(1); + + expect(buckets).toHaveLength(1); }); }); - describe('drop_partial option', function() { + describe('drop_partial option', () => { const aggResp = { - buckets: [ + [AggGroupNames.Buckets]: [ { key: 0, value: {} }, { key: 100, value: {} }, { key: 200, value: {} }, @@ -169,7 +181,7 @@ describe('Buckets wrapper', function() { ], }; - it('drops partial buckets when enabled', function() { + test('drops partial buckets when enabled', () => { const aggParams = { drop_partials: true, field: { @@ -182,10 +194,11 @@ describe('Buckets wrapper', function() { name: 'date', }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); - expect(buckets).to.have.length(1); + + expect(buckets).toHaveLength(1); }); - it('keeps partial buckets when disabled', function() { + test('keeps partial buckets when disabled', () => { const aggParams = { drop_partials: false, field: { @@ -198,10 +211,11 @@ describe('Buckets wrapper', function() { name: 'date', }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); - expect(buckets).to.have.length(4); + + expect(buckets).toHaveLength(4); }); - it('keeps aligned buckets when enabled', function() { + test('keeps aligned buckets when enabled', () => { const aggParams = { drop_partials: true, field: { @@ -214,10 +228,11 @@ describe('Buckets wrapper', function() { name: 'date', }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); - expect(buckets).to.have.length(3); + + expect(buckets).toHaveLength(3); }); - it('does not drop buckets for non-timerange fields', function() { + test('does not drop buckets for non-timerange fields', () => { const aggParams = { drop_partials: true, field: { @@ -230,7 +245,8 @@ describe('Buckets wrapper', function() { name: 'date', }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); - expect(buckets).to.have.length(4); + + expect(buckets).toHaveLength(4); }); }); }); diff --git a/src/legacy/core_plugins/data/public/search/tabify/buckets.ts b/src/legacy/core_plugins/data/public/search/tabify/buckets.ts new file mode 100644 index 0000000000000..8078136299f8c --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/buckets.ts @@ -0,0 +1,135 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { get, isPlainObject, keys, findKey } from 'lodash'; +import moment from 'moment'; +import { IAggConfig } from '../aggs'; +import { TabbedRangeFilterParams } from './types'; +import { AggResponseBucket } from '../types'; + +type AggParams = IAggConfig['params'] & { + drop_partials: boolean; + ranges: TabbedRangeFilterParams[]; +}; + +const isRangeEqual = (range1: TabbedRangeFilterParams, range2: TabbedRangeFilterParams) => + range1?.from === range2?.from && range1?.to === range2?.to; + +export class TabifyBuckets { + length: number; + objectMode: boolean; + buckets: any; + _keys: any[] = []; + + constructor(aggResp: any, aggParams?: AggParams, timeRange?: TabbedRangeFilterParams) { + if (aggResp && aggResp.buckets) { + this.buckets = aggResp.buckets; + } else if (aggResp) { + // Some Bucket Aggs only return a single bucket (like filter). + // In those instances, the aggResp is the content of the single bucket. + this.buckets = [aggResp]; + } else { + this.buckets = []; + } + + this.objectMode = isPlainObject(this.buckets); + + if (this.objectMode) { + this._keys = keys(this.buckets); + this.length = this._keys.length; + } else { + this.length = this.buckets.length; + } + + if (this.length && aggParams) { + this.orderBucketsAccordingToParams(aggParams); + if (aggParams.drop_partials) { + this.dropPartials(aggParams, timeRange); + } + } + } + + forEach(fn: (bucket: any, key: any) => void) { + const buckets = this.buckets; + + if (this.objectMode) { + this._keys.forEach(key => { + fn(buckets[key], key); + }); + } else { + buckets.forEach((bucket: AggResponseBucket) => { + fn(bucket, bucket.key); + }); + } + } + + private orderBucketsAccordingToParams(params: AggParams) { + if (params.filters && this.objectMode) { + this._keys = params.filters.map((filter: any) => { + const query = get(filter, 'input.query.query_string.query', filter.input.query); + const queryString = typeof query === 'string' ? query : JSON.stringify(query); + + return filter.label || queryString || '*'; + }); + } else if (params.ranges && this.objectMode) { + this._keys = params.ranges.map((range: TabbedRangeFilterParams) => + findKey(this.buckets, (el: TabbedRangeFilterParams) => isRangeEqual(el, range)) + ); + } else if (params.ranges && params.field.type !== 'date') { + let ranges = params.ranges; + if (params.ipRangeType) { + ranges = params.ipRangeType === 'mask' ? ranges.mask : ranges.fromTo; + } + this.buckets = ranges.map((range: any) => { + if (range.mask) { + return this.buckets.find((el: AggResponseBucket) => el.key === range.mask); + } + + return this.buckets.find((el: TabbedRangeFilterParams) => isRangeEqual(el, range)); + }); + } + } + + // dropPartials should only be called if the aggParam setting is enabled, + // and the agg field is the same as the Time Range. + private dropPartials(params: AggParams, timeRange?: TabbedRangeFilterParams) { + if ( + !timeRange || + this.buckets.length <= 1 || + this.objectMode || + params.field.name !== timeRange.name + ) { + return; + } + + const interval = this.buckets[1].key - this.buckets[0].key; + + this.buckets = this.buckets.filter((bucket: AggResponseBucket) => { + if (moment(bucket.key).isBefore(timeRange.gte)) { + return false; + } + if (moment(bucket.key + interval).isAfter(timeRange.lte)) { + return false; + } + return true; + }); + + this.length = this.buckets.length; + } +} diff --git a/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts b/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts new file mode 100644 index 0000000000000..cfd4cd7de640b --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts @@ -0,0 +1,192 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { tabifyGetColumns } from './get_columns'; +import { AggConfigs, AggGroupNames, Schemas } from '../aggs'; +import { TabbedAggColumn } from './types'; + +jest.mock('ui/new_platform'); + +describe('get columns', () => { + const createAggConfigs = (aggs: any[] = []) => { + const field = { + name: '@timestamp', + }; + + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + return new AggConfigs( + indexPattern, + aggs, + new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + min: 1, + defaults: [{ schema: 'metric', type: 'count' }], + }, + ]).all + ); + }; + + test('should inject a count metric if no aggs exist', () => { + const columns = tabifyGetColumns(createAggConfigs().aggs, true); + + expect(columns).toHaveLength(1); + expect(columns[0]).toHaveProperty('aggConfig'); + expect(columns[0].aggConfig.type).toHaveProperty('name', 'count'); + }); + + test('should inject a count metric if only buckets exist', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + ]).aggs, + true + ); + + expect(columns).toHaveLength(2); + expect(columns[1]).toHaveProperty('aggConfig'); + expect(columns[1].aggConfig.type).toHaveProperty('name', 'count'); + }); + + test('should inject the metric after each bucket if the vis is hierarchical', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + ]).aggs, + false + ); + + expect(columns).toHaveLength(8); + + columns.forEach((column, i) => { + expect(column).toHaveProperty('aggConfig'); + expect(column.aggConfig.type).toHaveProperty('name', i % 2 ? 'count' : 'date_histogram'); + }); + }); + + test('should inject the multiple metrics after each bucket if the vis is hierarchical', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { type: 'sum', schema: 'metric', params: { field: 'bytes' } }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + ]).aggs, + false + ); + + function checkColumns(column: TabbedAggColumn, i: number) { + expect(column).toHaveProperty('aggConfig'); + + switch (i) { + case 0: + expect(column.aggConfig.type).toHaveProperty('name', 'date_histogram'); + break; + case 1: + expect(column.aggConfig.type).toHaveProperty('name', 'avg'); + break; + case 2: + expect(column.aggConfig.type).toHaveProperty('name', 'sum'); + break; + } + } + + expect(columns).toHaveLength(12); + + for (let i = 0; i < columns.length; i += 3) { + columns.slice(i, i + 3).forEach(checkColumns); + } + }); + + test('should put all metrics at the end of the columns if the vis is not hierarchical', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '20s' }, + }, + { type: 'sum', schema: 'metric', params: { field: '@timestamp' } }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + ]).aggs, + false + ); + + expect(columns.map(c => c.name)).toEqual([ + '@timestamp per 20 seconds', + 'Sum of @timestamp', + '@timestamp per 10 seconds', + 'Sum of @timestamp', + ]); + }); +}); diff --git a/src/legacy/ui/public/agg_response/tabify/_get_columns.ts b/src/legacy/core_plugins/data/public/search/tabify/get_columns.ts similarity index 88% rename from src/legacy/ui/public/agg_response/tabify/_get_columns.ts rename to src/legacy/core_plugins/data/public/search/tabify/get_columns.ts index 4144d5be16012..8bffca65b4ae2 100644 --- a/src/legacy/ui/public/agg_response/tabify/_get_columns.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/get_columns.ts @@ -18,15 +18,9 @@ */ import { groupBy } from 'lodash'; -import { IAggConfig } from '../../agg_types'; - -export interface AggColumn { - aggConfig: IAggConfig; - id: string; - name: string; -} - -const getColumn = (agg: IAggConfig, i: number): AggColumn => { +import { IAggConfig } from '../aggs'; +import { TabbedAggColumn } from './types'; +const getColumn = (agg: IAggConfig, i: number): TabbedAggColumn => { return { aggConfig: agg, id: `col-${i}-${agg.id}`, @@ -40,14 +34,14 @@ const getColumn = (agg: IAggConfig, i: number): AggColumn => { * @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates * @param {boolean} minimalColumns - setting to true will only return a column for the last bucket/metric instead of one for each level */ -export function tabifyGetColumns(aggs: IAggConfig[], minimalColumns: boolean) { +export function tabifyGetColumns(aggs: IAggConfig[], minimalColumns: boolean): TabbedAggColumn[] { // pick the columns if (minimalColumns) { return aggs.map((agg, i) => getColumn(agg, i)); } // supposed to be bucket,...metrics,bucket,...metrics - const columns: AggColumn[] = []; + const columns: TabbedAggColumn[] = []; // separate the metrics const grouped = groupBy(aggs, agg => { diff --git a/src/legacy/core_plugins/data/public/search/tabify/index.ts b/src/legacy/core_plugins/data/public/search/tabify/index.ts new file mode 100644 index 0000000000000..90ac3f2fb730b --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { tabifyAggResponse } from './tabify'; +export { tabifyGetColumns } from './get_columns'; + +export { TabbedTable, TabbedAggRow, TabbedAggColumn } from './types'; diff --git a/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts b/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts new file mode 100644 index 0000000000000..f5df0a683ca00 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts @@ -0,0 +1,170 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TabbedAggResponseWriter } from './response_writer'; +import { AggConfigs, AggGroupNames, Schemas, BUCKET_TYPES } from '../aggs'; + +import { TabbedResponseWriterOptions } from './types'; + +jest.mock('ui/new_platform'); + +describe('TabbedAggResponseWriter class', () => { + let responseWriter: TabbedAggResponseWriter; + + const splitAggConfig = [ + { + type: BUCKET_TYPES.TERMS, + params: { + field: 'geo.src', + }, + }, + ]; + + const twoSplitsAggConfig = [ + { + type: BUCKET_TYPES.TERMS, + params: { + field: 'geo.src', + }, + }, + { + type: BUCKET_TYPES.TERMS, + params: { + field: 'machine.os.raw', + }, + }, + ]; + + const createResponseWritter = (aggs: any[] = [], opts?: Partial) => { + const field = { + name: 'geo.src', + }; + + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + return new TabbedAggResponseWriter( + new AggConfigs( + indexPattern, + aggs, + new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + min: 1, + defaults: [{ schema: 'metric', type: 'count' }], + }, + ]).all + ), + { + metricsAtAllLevels: false, + partialRows: false, + ...opts, + } + ); + }; + + describe('Constructor', () => { + beforeEach(() => { + responseWriter = createResponseWritter(twoSplitsAggConfig); + }); + + test('generates columns', () => { + expect(responseWriter.columns.length).toEqual(3); + }); + + test('correctly generates columns with metricsAtAllLevels set to true', () => { + const minimalColumnsResponseWriter = createResponseWritter(twoSplitsAggConfig, { + metricsAtAllLevels: true, + }); + + expect(minimalColumnsResponseWriter.columns.length).toEqual(4); + }); + + describe('row()', () => { + beforeEach(() => { + responseWriter = createResponseWritter(splitAggConfig); + }); + + test('adds the row to the array', () => { + responseWriter.bucketBuffer = [{ id: 'col-0', value: 'US' }]; + responseWriter.metricBuffer = [{ id: 'col-1', value: 5 }]; + + responseWriter.row(); + + expect(responseWriter.rows.length).toEqual(1); + expect(responseWriter.rows[0]).toEqual({ 'col-0': 'US', 'col-1': 5 }); + }); + + test("doesn't add an empty row", () => { + responseWriter.row(); + + expect(responseWriter.rows.length).toEqual(0); + }); + }); + + describe('response()', () => { + beforeEach(() => { + responseWriter = createResponseWritter(splitAggConfig); + }); + + test('produces correct response', () => { + responseWriter.bucketBuffer = [ + { id: 'col-0-1', value: 'US' }, + { id: 'col-1-2', value: 5 }, + ]; + responseWriter.row(); + + const response = responseWriter.response(); + + expect(response).toHaveProperty('rows'); + expect(response.rows).toEqual([{ 'col-0-1': 'US', 'col-1-2': 5 }]); + expect(response).toHaveProperty('columns'); + expect(response.columns.length).toEqual(2); + expect(response.columns[0]).toHaveProperty('id', 'col-0-1'); + expect(response.columns[0]).toHaveProperty('name', 'geo.src: Descending'); + expect(response.columns[0]).toHaveProperty('aggConfig'); + expect(response.columns[1]).toHaveProperty('id', 'col-1-2'); + expect(response.columns[1]).toHaveProperty('name', 'Count'); + expect(response.columns[1]).toHaveProperty('aggConfig'); + }); + + test('produces correct response for no data', () => { + const response = responseWriter.response(); + + expect(response).toHaveProperty('rows'); + expect(response.rows.length).toBe(0); + expect(response).toHaveProperty('columns'); + expect(response.columns.length).toEqual(2); + expect(response.columns[0]).toHaveProperty('id', 'col-0-1'); + expect(response.columns[0]).toHaveProperty('name', 'geo.src: Descending'); + expect(response.columns[0]).toHaveProperty('aggConfig'); + expect(response.columns[1]).toHaveProperty('id', 'col-1-2'); + expect(response.columns[1]).toHaveProperty('name', 'Count'); + expect(response.columns[1]).toHaveProperty('aggConfig'); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/search/tabify/response_writer.ts b/src/legacy/core_plugins/data/public/search/tabify/response_writer.ts new file mode 100644 index 0000000000000..c910eda024540 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/response_writer.ts @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { isEmpty } from 'lodash'; +import { IAggConfigs } from '../aggs/agg_configs'; +import { tabifyGetColumns } from './get_columns'; + +import { TabbedResponseWriterOptions, TabbedAggColumn, TabbedAggRow, TabbedTable } from './types'; + +interface BufferColumn { + id: string; + value: string | number; +} + +/** + * Writer class that collects information about an aggregation response and + * produces a table, or a series of tables. + */ +export class TabbedAggResponseWriter { + columns: TabbedAggColumn[]; + rows: TabbedAggRow[] = []; + bucketBuffer: BufferColumn[] = []; + metricBuffer: BufferColumn[] = []; + + private readonly partialRows: boolean; + + /** + * @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates + * @param {boolean} metricsAtAllLevels - setting to true will produce metrics for every bucket + * @param {boolean} partialRows - setting to true will not remove rows with missing values + */ + constructor( + aggs: IAggConfigs, + { metricsAtAllLevels = false, partialRows = false }: Partial + ) { + this.partialRows = partialRows; + + this.columns = tabifyGetColumns(aggs.getResponseAggs(), !metricsAtAllLevels); + this.rows = []; + } + + /** + * Create a new row by reading the row buffer and bucketBuffer + */ + row() { + const rowBuffer: TabbedAggRow = {}; + + this.bucketBuffer.forEach(bucket => { + rowBuffer[bucket.id] = bucket.value; + }); + + this.metricBuffer.forEach(metric => { + rowBuffer[metric.id] = metric.value; + }); + + const isPartialRow = + this.partialRows && !this.columns.every(column => rowBuffer.hasOwnProperty(column.id)); + + if (!isEmpty(rowBuffer) && !isPartialRow) { + this.rows.push(rowBuffer); + } + } + + response(): TabbedTable { + return { + columns: this.columns, + rows: this.rows, + }; + } +} diff --git a/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts b/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts new file mode 100644 index 0000000000000..13fe7719b0a85 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts @@ -0,0 +1,172 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IndexPattern } from '../../../../../../plugins/data/public'; +import { tabifyAggResponse } from './tabify'; +import { IAggConfig, IAggConfigs, AggGroupNames, Schemas, AggConfigs } from '../aggs'; +import { metricOnly, threeTermBuckets } from 'fixtures/fake_hierarchical_data'; + +jest.mock('ui/new_platform'); + +describe('tabifyAggResponse Integration', () => { + const createAggConfigs = (aggs: IAggConfig[] = []) => { + const field = { + name: '@timestamp', + }; + + const indexPattern = ({ + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as unknown) as IndexPattern; + + return new AggConfigs( + indexPattern, + aggs, + new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + min: 1, + defaults: [{ schema: 'metric', type: 'count' }], + }, + ]).all + ); + }; + + const mockAggConfig = (agg: any): IAggConfig => (agg as unknown) as IAggConfig; + + test('transforms a simple response properly', () => { + const aggConfigs = createAggConfigs(); + + const resp = tabifyAggResponse(aggConfigs, metricOnly, { + metricsAtAllLevels: true, + }); + + expect(resp).toHaveProperty('rows'); + expect(resp).toHaveProperty('columns'); + + expect(resp.rows).toHaveLength(1); + expect(resp.columns).toHaveLength(1); + + expect(resp.rows[0]).toEqual({ 'col-0-1': 1000 }); + expect(resp.columns[0]).toHaveProperty('aggConfig', aggConfigs.aggs[0]); + }); + + describe('transforms a complex response', () => { + let esResp: typeof threeTermBuckets; + let aggConfigs: IAggConfigs; + let avg: IAggConfig; + let ext: IAggConfig; + let src: IAggConfig; + let os: IAggConfig; + + beforeEach(() => { + aggConfigs = createAggConfigs([ + mockAggConfig({ type: 'avg', schema: 'metric', params: { field: '@timestamp' } }), + mockAggConfig({ type: 'terms', schema: 'split', params: { field: '@timestamp' } }), + mockAggConfig({ type: 'terms', schema: 'segment', params: { field: '@timestamp' } }), + mockAggConfig({ type: 'terms', schema: 'segment', params: { field: '@timestamp' } }), + ]); + + [avg, ext, src, os] = aggConfigs.aggs; + + esResp = threeTermBuckets; + esResp.aggregations.agg_2.buckets[1].agg_3.buckets[0].agg_4.buckets = []; + }); + + // check that the columns of a table are formed properly + function expectColumns(table: ReturnType, aggs: IAggConfig[]) { + expect(table.columns).toHaveLength(aggs.length); + + aggs.forEach((agg, i) => { + expect(table.columns[i]).toHaveProperty('aggConfig', agg); + }); + } + + // check that a row has expected values + function expectRow( + row: Record, + asserts: Array<(val: string | number) => void> + ) { + expect(typeof row).toBe('object'); + + asserts.forEach((assert, i: number) => { + if (row[`col-${i}`]) { + assert(row[`col-${i}`]); + } + }); + } + + // check for two character country code + function expectCountry(val: string | number) { + expect(typeof val).toBe('string'); + expect(val).toHaveLength(2); + } + + // check for an OS term + function expectExtension(val: string | number) { + expect(val).toMatch(/^(js|png|html|css|jpg)$/); + } + + // check for an OS term + function expectOS(val: string | number) { + expect(val).toMatch(/^(win|mac|linux)$/); + } + + // check for something like an average bytes result + function expectAvgBytes(val: string | number) { + expect(typeof val).toBe('number'); + expect(val === 0 || val > 1000).toBeDefined(); + } + + test('for non-hierarchical vis', () => { + // the default for a non-hierarchical vis is to display + // only complete rows, and only put the metrics at the end. + + const tabbed = tabifyAggResponse(aggConfigs, esResp, { metricsAtAllLevels: false }); + + expectColumns(tabbed, [ext, src, os, avg]); + + tabbed.rows.forEach(row => { + expectRow(row, [expectExtension, expectCountry, expectOS, expectAvgBytes]); + }); + }); + + test('for hierarchical vis', () => { + const tabbed = tabifyAggResponse(aggConfigs, esResp, { metricsAtAllLevels: true }); + + expectColumns(tabbed, [ext, avg, src, avg, os, avg]); + + tabbed.rows.forEach(row => { + expectRow(row, [ + expectExtension, + expectAvgBytes, + expectCountry, + expectAvgBytes, + expectOS, + expectAvgBytes, + ]); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/search/tabify/tabify.ts b/src/legacy/core_plugins/data/public/search/tabify/tabify.ts new file mode 100644 index 0000000000000..078d3f7f72759 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/tabify.ts @@ -0,0 +1,173 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { get } from 'lodash'; +import { TabbedAggResponseWriter } from './response_writer'; +import { TabifyBuckets } from './buckets'; +import { TabbedResponseWriterOptions, TabbedRangeFilterParams } from './types'; +import { AggResponseBucket } from '../types'; +import { IAggConfigs, AggGroupNames } from '../aggs'; + +/** + * Sets up the ResponseWriter and kicks off bucket collection. + */ +export function tabifyAggResponse( + aggConfigs: IAggConfigs, + esResponse: Record, + respOpts?: Partial +) { + /** + * read an aggregation from a bucket, which *might* be found at key (if + * the response came in object form), and will recurse down the aggregation + * tree and will pass the read values to the ResponseWriter. + */ + function collectBucket( + aggs: IAggConfigs, + write: TabbedAggResponseWriter, + bucket: AggResponseBucket, + key: string, + aggScale: number + ) { + const column = write.columns.shift(); + + if (column) { + const agg = column.aggConfig; + const aggInfo = agg.write(aggs); + aggScale *= aggInfo.metricScale || 1; + + switch (agg.type.type) { + case AggGroupNames.Buckets: + const aggBucket = get(bucket, agg.id); + const tabifyBuckets = new TabifyBuckets(aggBucket, agg.params, timeRange); + + if (tabifyBuckets.length) { + tabifyBuckets.forEach((subBucket, tabifyBucketKey) => { + // if the bucket doesn't have value don't add it to the row + // we don't want rows like: { column1: undefined, column2: 10 } + const bucketValue = agg.getKey(subBucket, tabifyBucketKey); + const hasBucketValue = typeof bucketValue !== 'undefined'; + + if (hasBucketValue) { + write.bucketBuffer.push({ id: column.id, value: bucketValue }); + } + + collectBucket( + aggs, + write, + subBucket, + agg.getKey(subBucket, tabifyBucketKey), + aggScale + ); + + if (hasBucketValue) { + write.bucketBuffer.pop(); + } + }); + } else if (respOpts?.partialRows) { + // we don't have any buckets, but we do have metrics at this + // level, then pass all the empty buckets and jump back in for + // the metrics. + write.columns.unshift(column); + passEmptyBuckets(aggs, write, bucket, key, aggScale); + write.columns.shift(); + } else { + // we don't have any buckets, and we don't have isHierarchical + // data, so no metrics, just try to write the row + write.row(); + } + break; + case AggGroupNames.Metrics: + let value = agg.getValue(bucket); + // since the aggregation could be a non integer (such as a max date) + // only do the scaling calculation if it is needed. + if (aggScale !== 1) { + value *= aggScale; + } + write.metricBuffer.push({ id: column.id, value }); + + if (!write.columns.length) { + // row complete + write.row(); + } else { + // process the next agg at this same level + collectBucket(aggs, write, bucket, key, aggScale); + } + + write.metricBuffer.pop(); + + break; + } + + write.columns.unshift(column); + } + } + + // write empty values for each bucket agg, then write + // the metrics from the initial bucket using collectBucket() + function passEmptyBuckets( + aggs: IAggConfigs, + write: TabbedAggResponseWriter, + bucket: AggResponseBucket, + key: string, + aggScale: number + ) { + const column = write.columns.shift(); + + if (column) { + const agg = column.aggConfig; + + switch (agg.type.type) { + case AggGroupNames.Metrics: + // pass control back to collectBucket() + write.columns.unshift(column); + collectBucket(aggs, write, bucket, key, aggScale); + return; + + case AggGroupNames.Buckets: + passEmptyBuckets(aggs, write, bucket, key, aggScale); + } + + write.columns.unshift(column); + } + } + + const write = new TabbedAggResponseWriter(aggConfigs, respOpts || {}); + const topLevelBucket: AggResponseBucket = { + ...esResponse.aggregations, + doc_count: esResponse.hits.total, + }; + + let timeRange: TabbedRangeFilterParams | undefined; + + // Extract the time range object if provided + if (respOpts && respOpts.timeRange) { + const [timeRangeKey] = Object.keys(respOpts.timeRange); + + if (timeRangeKey) { + timeRange = { + name: timeRangeKey, + ...respOpts.timeRange[timeRangeKey], + }; + } + } + + collectBucket(aggConfigs, write, topLevelBucket, '', 1); + + return write.response(); +} diff --git a/src/legacy/core_plugins/data/public/search/tabify/types.ts b/src/legacy/core_plugins/data/public/search/tabify/types.ts new file mode 100644 index 0000000000000..964a9d2080e7b --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/types.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RangeFilterParams } from '../../../../../../plugins/data/public'; +import { IAggConfig } from '../aggs'; + +/** @internal **/ +export interface TabbedRangeFilterParams extends RangeFilterParams { + name: string; +} + +/** @internal **/ +export interface TabbedResponseWriterOptions { + metricsAtAllLevels: boolean; + partialRows: boolean; + timeRange?: { [key: string]: RangeFilterParams }; +} + +/** @public **/ +export interface TabbedAggColumn { + aggConfig: IAggConfig; + id: string; + name: string; +} + +/** @public **/ +export type TabbedAggRow = Record; + +/** @public **/ +export interface TabbedTable { + columns: TabbedAggColumn[]; + rows: TabbedAggRow[]; +} diff --git a/src/legacy/core_plugins/data/public/search/utils/types.ts b/src/legacy/core_plugins/data/public/search/utils/types.ts index 305f27a86b398..e0afe99aa81fa 100644 --- a/src/legacy/core_plugins/data/public/search/utils/types.ts +++ b/src/legacy/core_plugins/data/public/search/utils/types.ts @@ -31,3 +31,9 @@ export interface RequestInspectorStats { hits?: InspectorStat; requestTime?: InspectorStat; } + +export interface AggResponseBucket { + key_as_string: string; + key: number; + doc_count: number; +} diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx index 3419d773bd09e..d7a62e07b26f3 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx @@ -23,6 +23,7 @@ import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { getDepsMock, getIndexPatternMock } from '../../test_utils'; import { ControlsTab, ControlsTabUiProps } from './controls_tab'; +import { Vis } from 'src/legacy/core_plugins/visualizations/public'; const indexPatternsMock = { get: getIndexPatternMock, @@ -32,7 +33,7 @@ let props: ControlsTabUiProps; beforeEach(() => { props = { deps: getDepsMock(), - vis: { + vis: ({ API: { indexPatterns: indexPatternsMock, }, @@ -46,7 +47,7 @@ beforeEach(() => { requiresSearch: false, hidden: false, }, - }, + } as unknown) as Vis, stateParams: { controls: [ { diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 8e6bae0b588bc..221133a17d59a 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -115,7 +115,7 @@ export default function(kibana) { { id: 'kibana:stack_management', title: i18n.translate('kbn.managementTitle', { - defaultMessage: 'Stack Management', + defaultMessage: 'Management', }), order: 9003, url: `${kbnBaseUrl}#/management`, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index d5198dc557f04..c1f679e9eb7ac 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -24,7 +24,6 @@ * directly where they are needed. */ -export { SavedObjectSaveOpts } from 'ui/saved_objects/types'; export { npSetup, npStart } from 'ui/new_platform'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; export { KbnUrl } from 'ui/url/kbn_url'; @@ -33,7 +32,6 @@ export { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_to // @ts-ignore export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url/index'; export { IInjector } from 'ui/chrome'; -export { SavedObjectLoader } from 'ui/saved_objects'; export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { configureAppAngularModule, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/__snapshots__/dashboard_empty_screen.test.tsx.snap index f7fc3b0891fef..c9f56dc898381 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -14,6 +14,7 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` "get": [Function], "prepend": [Function], "remove": [Function], + "serverBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], @@ -376,6 +377,7 @@ exports[`DashboardEmptyScreen renders correctly with visualize paragraph 1`] = ` "get": [Function], "prepend": [Function], "remove": [Function], + "serverBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], @@ -735,6 +737,7 @@ exports[`DashboardEmptyScreen renders correctly without visualize paragraph 1`] "get": [Function], "prepend": [Function], "remove": [Function], + "serverBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index cc104c1a931d0..7239d8f2258a7 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -38,7 +38,6 @@ import { PrivateProvider, PromiseServiceCreator, RedirectWhenMissingProvider, - SavedObjectLoader, } from '../legacy_imports'; // @ts-ignore import { initDashboardApp } from './legacy_app'; @@ -47,6 +46,7 @@ import { NavigationPublicPluginStart as NavigationStart } from '../../../../../. import { DataPublicPluginStart } from '../../../../../../plugins/data/public'; import { SharePluginStart } from '../../../../../../plugins/share/public'; import { KibanaLegacyStart } from '../../../../../../plugins/kibana_legacy/public'; +import { SavedObjectLoader } from '../../../../../../plugins/saved_objects/public'; export interface RenderDeps { pluginInitializerContext: PluginInitializerContext; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index 465203be0d34c..075516d52bab6 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -26,9 +26,10 @@ import angular from 'angular'; import { Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; import { History } from 'history'; +import { SavedObjectSaveOpts } from 'src/plugins/saved_objects/public'; import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen'; -import { migrateLegacyQuery, SavedObjectSaveOpts, subscribeWithScope } from '../legacy_imports'; +import { migrateLegacyQuery, subscribeWithScope } from '../legacy_imports'; import { esFilters, IndexPattern, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts index fe42e07912799..0820ebd371004 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts @@ -23,6 +23,8 @@ export const DashboardConstants = { CREATE_NEW_DASHBOARD_URL: '/dashboard', ADD_EMBEDDABLE_ID: 'addEmbeddableId', ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', + DASHBOARDS_ID: 'dashboards', + DASHBOARD_ID: 'dashboard', }; export function createDashboardEditUrl(id: string) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts index d80208ce27ffe..db2b1f15247de 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts @@ -18,7 +18,7 @@ */ import { TimefilterContract } from 'src/plugins/data/public'; -import { SavedObjectSaveOpts } from '../../legacy_imports'; +import { SavedObjectSaveOpts } from '../../../../../../../plugins/saved_objects/public'; import { updateSavedDashboard } from './update_saved_dashboard'; import { DashboardStateManager } from '../dashboard_state_manager'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index 7d330676e79ed..7d64ee969212f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -83,7 +83,14 @@ export class DashboardPlugin implements Plugin { ); const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({ baseUrl: core.http.basePath.prepend('/app/kibana'), - defaultSubUrl: '#/dashboards', + defaultSubUrl: `#${DashboardConstants.LANDING_PAGE_PATH}`, + shouldTrackUrlUpdate: pathname => { + const targetAppName = pathname.split('/')[1]; + return ( + targetAppName === DashboardConstants.DASHBOARDS_ID || + targetAppName === DashboardConstants.DASHBOARD_ID + ); + }, storageKey: 'lastUrl:dashboard', navLinkUpdater$: this.appStateUpdater, toastNotifications: core.notifications.toasts, @@ -150,15 +157,15 @@ export class DashboardPlugin implements Plugin { }; kibanaLegacy.registerLegacyApp({ ...app, - id: 'dashboard', + id: DashboardConstants.DASHBOARD_ID, // only register the updater in once app, otherwise all updates would happen twice updater$: this.appStateUpdater.asObservable(), navLinkId: 'kibana:dashboard', }); - kibanaLegacy.registerLegacyApp({ ...app, id: 'dashboards' }); + kibanaLegacy.registerLegacyApp({ ...app, id: DashboardConstants.DASHBOARDS_ID }); home.featureCatalogue.register({ - id: 'dashboard', + id: DashboardConstants.DASHBOARD_ID, title: i18n.translate('kbn.dashboard.featureCatalogue.dashboardTitle', { defaultMessage: 'Dashboard', }), diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.ts index 5babaf8061de9..c5ac05b5a77eb 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.ts @@ -16,8 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { SavedObject, SavedObjectKibanaServices } from 'ui/saved_objects/types'; -import { createSavedObjectClass } from 'ui/saved_objects/saved_object'; +import { + createSavedObjectClass, + SavedObject, + SavedObjectKibanaServices, +} from '../../../../../../plugins/saved_objects/public'; import { extractReferences, injectReferences } from './saved_dashboard_references'; import { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.ts index 4ece5d46358ba..2ff76da9c5ca6 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.ts @@ -17,8 +17,10 @@ * under the License. */ -import { SavedObjectLoader } from 'ui/saved_objects'; -import { SavedObjectKibanaServices } from 'ui/saved_objects/types'; +import { + SavedObjectLoader, + SavedObjectKibanaServices, +} from '../../../../../../plugins/saved_objects/public'; import { createSavedDashboardClass } from './saved_dashboard'; export function createSavedDashboardLoader(services: SavedObjectKibanaServices) { diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index b0bb17ce1ac7f..91b5c7f13dc95 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -58,8 +58,7 @@ export { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; // @ts-ignore export { timezoneProvider } from 'ui/vis/lib/timezone'; -// @ts-ignore -export { tabifyAggResponse } from 'ui/agg_response/tabify'; +export { tabifyAggResponse } from '../../../data/public'; export { unhashUrl } from '../../../../../plugins/kibana_utils/public'; export { migrateLegacyQuery, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/_discover.scss b/src/legacy/core_plugins/kibana/public/discover/np_ready/_discover.scss index 0da28e41579ae..62e7a96ed80cf 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/_discover.scss +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/_discover.scss @@ -206,7 +206,7 @@ discover-app { background-color: transparent; } -.dscField--noResults { +.dscFieldName--noResults { color: $euiColorDarkShade; } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/__snapshots__/field_name.test.tsx.snap b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/__snapshots__/field_name.test.tsx.snap index bdb003771619c..23288fc5feb59 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/__snapshots__/field_name.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/__snapshots__/field_name.test.tsx.snap @@ -1,73 +1,109 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`FieldName renders a geo field, useShortDots is set to true 1`] = ` - - - - t.t.test - - + + + + +
+ + t.t.test + +
+ `; exports[`FieldName renders a number field by providing a field record, useShortDots is set to false 1`] = ` - - - + + + + +
- test.test.test - - + + test.test.test + +
+ `; exports[`FieldName renders a string field by providing fieldType and fieldName 1`] = ` - - - + + + + +
- test - - + + test + +
+ `; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx index 95720bee38df8..54e1c1706a856 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx @@ -18,7 +18,9 @@ */ import React from 'react'; import classNames from 'classnames'; -import { FieldIcon } from '../../../../../../../../../plugins/kibana_react/public'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { FieldIcon, FieldIconProps } from '../../../../../../../../../plugins/kibana_react/public'; import { shortenDottedString } from '../../../../../../../../../plugins/data/common/utils'; import { getFieldTypeName } from './field_type_name'; @@ -35,25 +37,35 @@ interface Props { fieldName?: string; fieldType?: string; useShortDots?: boolean; + fieldIconProps?: Omit; } -export function FieldName({ field, fieldName, fieldType, useShortDots }: Props) { +export function FieldName({ field, fieldName, fieldType, useShortDots, fieldIconProps }: Props) { const type = field ? String(field.type) : String(fieldType); const typeName = getFieldTypeName(type); const name = field ? String(field.name) : String(fieldName); const displayName = useShortDots ? shortenDottedString(name) : name; - const className = classNames({ - 'dscField--noResults': field ? !field.rowCount && !field.scripted : false, - // this is currently not styled, should display an icon - scripted: field ? field.scripted : false, + const noResults = field ? !field.rowCount && !field.scripted : false; + + const className = classNames('dscFieldName', { + 'dscFieldName--noResults': noResults, }); return ( - - - {displayName} - + + + + + + {displayName} + + ); } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/_field_chooser.scss b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/_field_chooser.scss index fe13ac2fafa01..b05775c4ee95c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/_field_chooser.scss +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/_field_chooser.scss @@ -10,7 +10,6 @@ .dscFieldName { color: $euiColorDarkShade; - padding-left: $euiSizeS; } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.test.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.test.tsx index 96b8cc383888e..b6fd5ee60b8e2 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.test.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.test.tsx @@ -31,14 +31,14 @@ const indexPattern1 = { attributes: { title: 'test1 title', }, -} as SavedObject; +} as SavedObject; const indexPattern2 = { id: 'test2', attributes: { title: 'test2 title', }, -} as SavedObject; +} as SavedObject; const defaultProps = { indexPatternList: [indexPattern1, indexPattern2], diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.tsx index a4e8ee2ca3d8a..cca523ee2c1bd 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.tsx @@ -18,6 +18,7 @@ */ import React, { useState } from 'react'; import { SavedObject } from 'kibana/server'; +import { IndexPatternAttributes } from 'src/plugins/data/public'; import { I18nProvider } from '@kbn/i18n/react'; import { IndexPatternRef } from './types'; @@ -26,11 +27,11 @@ export interface DiscoverIndexPatternProps { /** * list of available index patterns, if length > 1, component offers a "change" link */ - indexPatternList: SavedObject[]; + indexPatternList: Array>; /** * currently selected index pattern, due to angular issues it's undefined at first rendering */ - selectedIndexPattern: SavedObject; + selectedIndexPattern: SavedObject; /** * triggered when user selects a new index pattern */ diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table_row.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table_row.tsx index 7a78e89416361..5b13f6b3655c3 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table_row.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table_row.tsx @@ -87,7 +87,12 @@ export function DocViewTableRow({ )} - + {isCollapsible && ( diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts index 113d13287bd12..7bd0eef8c19af 100644 --- a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts @@ -16,8 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { SavedObjectKibanaServices } from 'ui/saved_objects/types'; -import { createSavedObjectClass } from 'ui/saved_objects/saved_object'; + +import { + createSavedObjectClass, + SavedObjectKibanaServices, +} from '../../../../../../plugins/saved_objects/public'; export function createSavedSearchClass(services: SavedObjectKibanaServices) { const SavedObjectClass = createSavedObjectClass(services); diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts b/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts index 0b34652461026..ebd341eba99fd 100644 --- a/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts @@ -16,8 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { SavedObjectLoader } from 'ui/saved_objects'; -import { SavedObjectKibanaServices } from 'ui/saved_objects/types'; + +import { + SavedObjectLoader, + SavedObjectKibanaServices, +} from '../../../../../../plugins/saved_objects/public'; import { createSavedSearchClass } from './_saved_search'; export function createSavedSearchesLoader(services: SavedObjectKibanaServices) { diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js index b0d94711be7b6..db24cb3e3c1b7 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js @@ -131,7 +131,7 @@ describe('home', () => { test('should not render directory entry when showOnHomePage is false', async () => { const directoryEntry = { id: 'stack-management', - title: 'Stack Management', + title: 'Management', description: 'Your center console for managing the Elastic Stack.', icon: 'managementApp', path: 'management_landing_page', diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index d77fc780f4cc2..384c6bd80ec33 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -53,7 +53,7 @@ import './management'; import './dev_tools'; import 'ui/agg_response'; import 'ui/agg_types'; -import { showAppRedirectNotification } from 'ui/notify'; +import { showAppRedirectNotification } from '../../../../plugins/kibana_legacy/public'; import 'leaflet'; import { localApplicationService } from './local_application_service'; @@ -68,4 +68,6 @@ routes.otherwise({ redirectTo: `/${config.defaultAppId || 'discover'}`, }); -uiModules.get('kibana').run(showAppRedirectNotification); +uiModules + .get('kibana') + .run($location => showAppRedirectNotification($location, npSetup.core.notifications.toasts)); diff --git a/src/legacy/core_plugins/kibana/public/management/index.js b/src/legacy/core_plugins/kibana/public/management/index.js index 2cba9fab7be22..6a36391c56b5c 100644 --- a/src/legacy/core_plugins/kibana/public/management/index.js +++ b/src/legacy/core_plugins/kibana/public/management/index.js @@ -69,7 +69,7 @@ export function updateLandingPage(version) {

diff --git a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts index 0a6ac20502669..e0756b2e78e25 100644 --- a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts +++ b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts @@ -20,7 +20,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { npStart } from 'ui/new_platform'; -import { SavedObjectLoader } from 'ui/saved_objects'; +import { SavedObjectLoader } from '../../../../../plugins/saved_objects/public'; import { createSavedDashboardLoader } from '../dashboard'; import { createSavedSearchesLoader } from '../discover'; import { TypesService, createSavedVisLoader } from '../../../visualizations/public'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx index 7cfc7c4dbc81c..bbb6bf26e5b31 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx @@ -24,6 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { indexPatterns, DataPublicPluginStart, + IndexPatternAttributes, } from '../../../../../../../../../../plugins/data/public'; import { SavedObjectsClient, IUiSettingsClient } from '../../../../../../../../../../core/public'; import { MAX_SEARCH_SIZE } from '../../constants'; @@ -96,7 +97,7 @@ export class StepIndexPattern extends Component { - const { savedObjects } = await this.props.savedObjectsClient.find({ + const { savedObjects } = await this.props.savedObjectsClient.find({ type: 'index-pattern', fields: ['title'], perPage: 10000, diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/render.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/render.test.js index af580547b11ed..1b9dafb6daf23 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/render.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/render.test.js @@ -50,6 +50,7 @@ describe('CreateIndexPatternWizardRender', () => { config: {}, changeUrl: () => {}, indexPatternCreationType: {}, + openConfirm: jest.fn(), }); expect(render.mock.calls.length).toBe(1); diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index 096877d5824c4..cfd12b3283459 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -36,6 +36,7 @@ import { VisualizationsStart } from '../../../visualizations/public'; import { SavedVisualizations } from './np_ready/types'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; import { KibanaLegacyStart } from '../../../../../plugins/kibana_legacy/public'; +import { DefaultEditorController } from '../../../vis_default_editor/public'; export interface VisualizeKibanaServices { pluginInitializerContext: PluginInitializerContext; @@ -60,6 +61,7 @@ export interface VisualizeKibanaServices { usageCollection?: UsageCollectionSetup; I18nContext: I18nStart['Context']; setActiveUrl: (newUrl: string) => void; + DefaultVisualizationEditor: typeof DefaultEditorController; } let services: VisualizeKibanaServices | null = null; diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index 92433799ba420..d9565938c838d 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -24,14 +24,11 @@ * directly where they are needed. */ -// @ts-ignore -export { AppState, AppStateProvider } from 'ui/state_management/app_state'; export { State } from 'ui/state_management/state'; // @ts-ignore export { GlobalStateProvider } from 'ui/state_management/global_state'; // @ts-ignore export { StateManagementConfigProvider } from 'ui/state_management/config_provider'; -export { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; export { PersistedState } from 'ui/persisted_state'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; @@ -46,8 +43,7 @@ export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; export { wrapInI18nContext } from 'ui/i18n'; export { DashboardConstants } from '../dashboard/np_ready/dashboard_constants'; -export { VisSavedObject } from '../../../visualizations/public/embeddable/visualize_embeddable'; -export { VISUALIZE_EMBEDDABLE_TYPE } from '../../../visualizations/public/embeddable'; +export { VisSavedObject, VISUALIZE_EMBEDDABLE_TYPE } from '../../../visualizations/public/'; export { configureAppAngularModule, ensureDefaultIndexPattern, diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts index bd7b478f827a6..6a8d9ce106f9d 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts @@ -22,8 +22,6 @@ import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { AppMountContext } from 'kibana/public'; import { - AppStateProvider, - AppState, configureAppAngularModule, createTopNavDirective, createTopNavHelper, @@ -116,12 +114,6 @@ function createLocalStateModule() { 'app/visualize/Promise', 'app/visualize/PersistedState', ]) - .factory('AppState', function(Private: IPrivate) { - return Private(AppStateProvider); - }) - .service('getAppState', function(Private: IPrivate) { - return Private(AppStateProvider).getAppState; - }) .service('globalState', function(Private: IPrivate) { return Private(GlobalStateProvider); }); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html index 6190b92c9be3e..4979d9dc89a0c 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html @@ -2,7 +2,7 @@
@@ -42,9 +42,9 @@ show-filter-bar="showFilterBar() && isVisible" show-date-picker="showQueryBarTimePicker()" show-auto-refresh-only="!showQueryBarTimePicker()" - query="state.query" + query="query" saved-query="savedQuery" - screen-title="state.vis.title" + screen-title="vis.title" on-query-submit="updateQueryAndFetch" index-patterns="[indexPattern]" filters="filters" @@ -97,7 +97,9 @@ ui-state="uiState" time-range="timeRange" filters="filters" - query="query"/> + query="query" + app-state="appState" + />

diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index 409d4b41fbe69..415949f88e9d1 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -24,7 +24,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { migrateAppState } from './lib'; +import { makeStateful, useVisualizeAppState } from './lib'; import { VisualizeConstants } from '../visualize_constants'; import { getEditBreadcrumbs } from '../breadcrumbs'; @@ -45,7 +45,6 @@ import { absoluteToParsedUrl, KibanaParsedUrl, migrateLegacyQuery, - stateMonitorFactory, DashboardConstants, } from '../../legacy_imports'; @@ -68,15 +67,14 @@ function VisualizeAppController( $scope, $element, $route, - AppState, $window, $injector, $timeout, kbnUrl, redirectWhenMissing, Promise, - getAppState, - globalState + globalState, + config ) { const { indexPatterns, @@ -99,7 +97,6 @@ function VisualizeAppController( setActiveUrl, } = getServices(); - const filterStateManager = new FilterStateManager(globalState, getAppState, filterManager); // Retrieve the resolved SavedVis instance. const savedVis = $route.current.locals.savedVis; const _applyVis = () => { @@ -113,9 +110,9 @@ function VisualizeAppController( $scope.vis = vis; - const $appStatus = (this.appStatus = { + const $appStatus = { dirty: !savedVis.id, - }); + }; vis.on('dirtyStateChange', ({ isDirty }) => { vis.dirty = isDirty; @@ -265,53 +262,61 @@ function VisualizeAppController( }, ]; - let stateMonitor; - if (savedVis.id) { chrome.docTitle.change(savedVis.title); } + const defaultQuery = { + query: '', + language: + localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), + }; + // Extract visualization state with filtered aggs. You can see these filtered aggs in the URL. // Consists of things like aggs, params, listeners, title, type, etc. const savedVisState = vis.getState(); const stateDefaults = { uiState: savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : {}, - linked: !!savedVis.savedSearchId, - query: searchSource.getOwnField('query') || { - query: '', - language: - localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), - }, + query: searchSource.getOwnField('query') || defaultQuery, filters: searchSource.getOwnField('filter') || [], vis: savedVisState, + linked: !!savedVis.savedSearchId, }; - // Instance of app_state.js. - const $state = (function initState() { - // This is used to sync visualization state with the url when `appState.save()` is called. - const appState = new AppState(stateDefaults); - - // Initializing appState does two things - first it translates the defaults into AppState, - // second it updates appState based on the url (the url trumps the defaults). This means if - // we update the state format at all and want to handle BWC, we must not only migrate the - // data stored with saved vis, but also any old state in the url. - migrateAppState(appState); - - // The savedVis is pulled from elasticsearch, but the appState is pulled from the url, with the - // defaults applied. If the url was from a previous session which included modifications to the - // appState then they won't be equal. - if (!angular.equals(appState.vis, savedVisState)) { - Promise.try(function() { - vis.setState(appState.vis); - }).catch( - redirectWhenMissing({ - 'index-pattern-field': '/visualize', - }) - ); - } + const useHash = config.get('state:storeInSessionStorage'); + const { stateContainer, stopStateSync } = useVisualizeAppState({ + useHash, + stateDefaults, + }); + + const filterStateManager = new FilterStateManager( + globalState, + () => { + // Temporary AppState replacement + return { + set filters(_filters) { + stateContainer.transitions.set('filters', _filters); + }, + get filters() { + return stateContainer.getState().filters; + }, + }; + }, + filterManager + ); - return appState; - })(); + // The savedVis is pulled from elasticsearch, but the appState is pulled from the url, with the + // defaults applied. If the url was from a previous session which included modifications to the + // appState then they won't be equal. + if (!_.isEqual(stateContainer.getState().vis, stateDefaults.vis)) { + try { + vis.setState(stateContainer.getState().vis); + } catch { + redirectWhenMissing({ + 'index-pattern-field': '/visualize', + }); + } + } $scope.filters = filterManager.getFilters(); @@ -330,8 +335,6 @@ function VisualizeAppController( ); function init() { - // export some objects - $scope.savedVis = savedVis; if (vis.indexPattern) { $scope.indexPattern = vis.indexPattern; } else { @@ -340,14 +343,28 @@ function VisualizeAppController( }); } + const initialState = stateContainer.getState(); + + $scope.appState = { + // mock implementation of the legacy appState.save() + // this could be even replaced by passing only "updateAppState" callback + save() { + stateContainer.transitions.updateVisState(vis.getState()); + }, + }; + + // Create a PersistedState instance for uiState. + const { persistedState, unsubscribePersisted, persistOnChange } = makeStateful( + 'uiState', + stateContainer + ); + $scope.uiState = persistedState; + $scope.savedVis = savedVis; + $scope.query = initialState.query; + $scope.linked = initialState.linked; $scope.searchSource = searchSource; - $scope.state = $state; $scope.refreshInterval = timefilter.getRefreshInterval(); - // Create a PersistedState instance. - $scope.uiState = $state.makeStateful('uiState'); - $scope.appStatus = $appStatus; - const addToDashMode = $route.current.params[DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM]; kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM); @@ -372,22 +389,23 @@ function VisualizeAppController( $scope.timeRange = timefilter.getTime(); $scope.opts = _.pick($scope, 'savedVis', 'isAddToDashMode'); - stateMonitor = stateMonitorFactory.create($state, stateDefaults); - stateMonitor.ignoreProps(['vis.listeners']).onChange(status => { - $appStatus.dirty = status.dirty || !savedVis.id; - }); + const unsubscribeStateUpdates = stateContainer.subscribe(state => { + const newQuery = migrateLegacyQuery(state.query); + if (!_.isEqual(state.query, newQuery)) { + stateContainer.transitions.set('query', newQuery); + } + persistOnChange(state); - $scope.$watch('state.query', (newQuery, oldQuery) => { - if (!_.isEqual(newQuery, oldQuery)) { - const query = migrateLegacyQuery(newQuery); - if (!_.isEqual(query, newQuery)) { - $state.query = query; - } - $scope.fetch(); + // if the browser history was changed manually we need to reflect changes in the editor + if (!_.isEqual(vis.getState(), state.vis)) { + vis.setState(state.vis); + vis.forceReload(); + vis.emit('updateEditor'); } - }); - $state.replace(); + $appStatus.dirty = true; + $scope.fetch(); + }); const updateTimeRange = () => { $scope.timeRange = timefilter.getTime(); @@ -419,10 +437,11 @@ function VisualizeAppController( // update the searchSource when query updates $scope.fetch = function() { - $state.save(); - $scope.query = $state.query; - savedVis.searchSource.setField('query', $state.query); - savedVis.searchSource.setField('filter', $state.filters); + const { query, filters, linked } = stateContainer.getState(); + $scope.query = query; + $scope.linked = linked; + savedVis.searchSource.setField('query', query); + savedVis.searchSource.setField('filter', filters); $scope.$broadcast('render'); }; @@ -446,10 +465,13 @@ function VisualizeAppController( $scope._handler.destroy(); } savedVis.destroy(); - stateMonitor.destroy(); filterStateManager.destroy(); subscriptions.unsubscribe(); $scope.vis.off('apply', _applyVis); + + unsubscribePersisted(); + unsubscribeStateUpdates(); + stopStateSync(); }); $timeout(() => { @@ -459,10 +481,10 @@ function VisualizeAppController( $scope.updateQueryAndFetch = function({ query, dateRange }) { const isUpdate = - (query && !_.isEqual(query, $state.query)) || + (query && !_.isEqual(query, stateContainer.getState().query)) || (dateRange && !_.isEqual(dateRange, $scope.timeRange)); - $state.query = query; + stateContainer.transitions.set('query', query); timefilter.setTime(dateRange); // If nothing has changed, trigger the fetch manually, otherwise it will happen as a result of the changes @@ -488,20 +510,13 @@ function VisualizeAppController( $scope.onClearSavedQuery = () => { delete $scope.savedQuery; - delete $state.savedQuery; - $state.query = { - query: '', - language: - localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), - }; + stateContainer.transitions.removeSavedQuery(defaultQuery); filterManager.setFilters(filterManager.getGlobalFilters()); - $state.save(); $scope.fetch(); }; const updateStateFromSavedQuery = savedQuery => { - $state.query = savedQuery.attributes.query; - $state.save(); + stateContainer.transitions.set('query', savedQuery.attributes.query); const savedQueryFilters = savedQuery.attributes.filters || []; const globalFilters = filterManager.getGlobalFilters(); @@ -520,44 +535,38 @@ function VisualizeAppController( $scope.fetch(); }; + // update the query if savedQuery is stored + if (stateContainer.getState().savedQuery) { + savedQueryService.getSavedQuery(stateContainer.getState().savedQuery).then(savedQuery => { + $scope.$evalAsync(() => { + $scope.savedQuery = savedQuery; + }); + }); + } + $scope.$watch('savedQuery', newSavedQuery => { if (!newSavedQuery) return; - $state.savedQuery = newSavedQuery.id; - $state.save(); + stateContainer.transitions.set('savedQuery', newSavedQuery.id); updateStateFromSavedQuery(newSavedQuery); }); - $scope.$watch('state.savedQuery', newSavedQueryId => { - if (!newSavedQueryId) { - $scope.savedQuery = undefined; - return; - } - if (!$scope.savedQuery || newSavedQueryId !== $scope.savedQuery.id) { - savedQueryService.getSavedQuery(newSavedQueryId).then(savedQuery => { - $scope.$evalAsync(() => { - $scope.savedQuery = savedQuery; - updateStateFromSavedQuery(savedQuery); - }); - }); - } - }); - /** * Called when the user clicks "Save" button. */ function doSave(saveOptions) { // vis.title was not bound and it's needed to reflect title into visState - $state.vis.title = savedVis.title; - $state.vis.type = savedVis.type || $state.vis.type; - savedVis.visState = $state.vis; + stateContainer.transitions.setVis({ + title: savedVis.title, + type: savedVis.type || stateContainer.getState().vis.type, + }); + savedVis.visState = stateContainer.getState().vis; savedVis.uiStateJSON = angular.toJson($scope.uiState.getChanges()); + $appStatus.dirty = false; return savedVis.save(saveOptions).then( function(id) { $scope.$evalAsync(() => { - stateMonitor.setInitialState($state.toJSON()); - if (id) { toastNotifications.addSuccess({ title: i18n.translate( @@ -601,8 +610,6 @@ function VisualizeAppController( chrome.setBreadcrumbs($injector.invoke(getEditBreadcrumbs)); savedVis.vis.title = savedVis.title; savedVis.vis.description = savedVis.description; - // it's needed to save the state to update url string - $state.save(); } else { kbnUrl.change(`${VisualizeConstants.EDIT_PATH}/{{id}}`, { id: savedVis.id }); } @@ -632,9 +639,8 @@ function VisualizeAppController( } $scope.unlink = function() { - if (!$state.linked) return; + if (!$scope.linked) return; - $state.linked = false; const searchSourceParent = searchSource.getParent(); const searchSourceGrandparent = searchSourceParent.getParent(); @@ -645,8 +651,10 @@ function VisualizeAppController( _.union(searchSource.getOwnField('filter'), searchSourceParent.getOwnField('filter')) ); - $state.query = searchSourceParent.getField('query'); - $state.filters = searchSourceParent.getField('filter'); + stateContainer.transitions.unlinkSavedSearch( + searchSourceParent.getField('query'), + searchSourceParent.getField('filter') + ); searchSource.setField('index', searchSourceParent.getField('index')); searchSource.setParent(searchSourceGrandparent); diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/tabify.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts similarity index 84% rename from src/legacy/ui/public/agg_response/tabify/__tests__/tabify.js rename to src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts index 38ed5408b603e..fa5b91b00edaf 100644 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/tabify.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts @@ -17,8 +17,5 @@ * under the License. */ -import './_get_columns'; -import './_buckets'; -import './_response_writer'; -import './_integration'; -describe('Tabify Agg Response', function() {}); +export { useVisualizeAppState } from './visualize_app_state'; +export { makeStateful } from './make_stateful'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts new file mode 100644 index 0000000000000..137d4de1fe9a8 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PersistedState } from '../../../legacy_imports'; +import { ReduxLikeStateContainer } from '../../../../../../../../plugins/kibana_utils/public'; +import { VisualizeAppState, VisualizeAppStateTransitions } from '../../types'; + +/** + * @returns Create a PersistedState instance, initialize state changes subscriber/unsubscriber + */ +export function makeStateful( + prop: keyof VisualizeAppState, + stateContainer: ReduxLikeStateContainer +) { + // set up the persistedState state + const persistedState = new PersistedState(); + + // update the appState when the stateful instance changes + const updateOnChange = function() { + stateContainer.transitions.set(prop, persistedState.getChanges()); + }; + + const handlerOnChange = (method: 'on' | 'off') => + persistedState[method]('change', updateOnChange); + + handlerOnChange('on'); + const unsubscribePersisted = () => handlerOnChange('off'); + + // update the stateful object when the app state changes + const persistOnChange = function(state: VisualizeAppState) { + if (state[prop]) { + persistedState.set(state[prop]); + } + }; + + const appState = stateContainer.getState(); + + // if the thing we're making stateful has an appState value, write to persisted state + if (appState[prop]) persistedState.setSilent(appState[prop]); + + return { persistedState, unsubscribePersisted, persistOnChange }; +} diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.ts similarity index 61% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.js rename to src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.ts index 049ce048239db..7e09aece52e09 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.ts @@ -18,6 +18,7 @@ */ import { get, omit } from 'lodash'; +import { VisualizeAppState } from '../../types'; /** * Creates a new instance of AppState based on the table vis state. @@ -25,37 +26,41 @@ import { get, omit } from 'lodash'; * Dashboards have a similar implementation; see * core_plugins/kibana/public/dashboard/lib/migrate_app_state * - * @param appState {AppState} AppState class to instantiate + * @param appState {VisualizeAppState} */ -export function migrateAppState(appState) { +export function migrateAppState(appState: VisualizeAppState) { // For BWC in pre 7.0 versions where table visualizations could have multiple aggs // with `schema === 'split'`. This ensures that bookmarked URLs with deprecated params // are rewritten to the correct state. See core_plugins/table_vis/migrations. if (appState.vis.type !== 'table') { - return; + return appState; } - const visAggs = get(appState, 'vis.aggs', []); - let splitCount = 0; - const migratedAggs = visAggs.map(agg => { - if (agg.schema !== 'split') { + const visAggs: any = get(appState, 'vis.aggs'); + + if (visAggs) { + let splitCount = 0; + const migratedAggs = visAggs.map((agg: any) => { + if (agg.schema !== 'split') { + return agg; + } + + splitCount++; + if (splitCount === 1) { + return agg; // leave the first split agg unchanged + } + agg.schema = 'bucket'; + // the `row` param is exclusively used by split aggs, so we remove it + agg.params = omit(agg.params, ['row']); return agg; - } + }); - splitCount++; - if (splitCount === 1) { - return agg; // leave the first split agg unchanged + if (splitCount <= 1) { + return appState; // do nothing; we only want to touch tables with multiple split aggs } - agg.schema = 'bucket'; - // the `row` param is exclusively used by split aggs, so we remove it - agg.params = omit(agg.params, ['row']); - return agg; - }); - - if (splitCount <= 1) { - return; // do nothing; we only want to touch tables with multiple split aggs + + appState.vis.aggs = migratedAggs; } - appState.vis.aggs = migratedAggs; - appState.save(); + return appState; } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts new file mode 100644 index 0000000000000..d8de81193d857 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts @@ -0,0 +1,112 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createHashHistory } from 'history'; +import { isFunction, omit } from 'lodash'; + +import { migrateAppState } from './migrate_app_state'; +import { + createKbnUrlStateStorage, + createStateContainer, + syncState, +} from '../../../../../../../../plugins/kibana_utils/public'; +import { PureVisState, VisualizeAppState, VisualizeAppStateTransitions } from '../../types'; + +const STATE_STORAGE_KEY = '_a'; + +interface Arguments { + useHash: boolean; + stateDefaults: VisualizeAppState; +} + +function toObject(state: PureVisState): PureVisState { + return omit(state, (value, key: string) => { + return key.charAt(0) === '$' || key.charAt(0) === '_' || isFunction(value); + }); +} + +export function useVisualizeAppState({ useHash, stateDefaults }: Arguments) { + const history = createHashHistory(); + const kbnUrlStateStorage = createKbnUrlStateStorage({ + useHash, + history, + }); + const urlState = kbnUrlStateStorage.get(STATE_STORAGE_KEY); + const initialState = migrateAppState({ + ...stateDefaults, + ...urlState, + }); + + /* + make sure url ('_a') matches initial state + Initializing appState does two things - first it translates the defaults into AppState, + second it updates appState based on the url (the url trumps the defaults). This means if + we update the state format at all and want to handle BWC, we must not only migrate the + data stored with saved vis, but also any old state in the url. + */ + kbnUrlStateStorage.set(STATE_STORAGE_KEY, initialState, { replace: true }); + + const stateContainer = createStateContainer( + initialState, + { + set: state => (prop, value) => ({ ...state, [prop]: value }), + setVis: state => vis => ({ + ...state, + vis: { + ...state.vis, + ...vis, + }, + }), + removeSavedQuery: state => defaultQuery => { + const { savedQuery, ...rest } = state; + + return { + ...rest, + query: defaultQuery, + }; + }, + unlinkSavedSearch: state => (query, filters) => ({ + ...state, + query, + filters, + linked: false, + }), + updateVisState: state => newVisState => ({ ...state, vis: toObject(newVisState) }), + } + ); + + const { start: startStateSync, stop: stopStateSync } = syncState({ + storageKey: STATE_STORAGE_KEY, + stateContainer: { + ...stateContainer, + set: state => { + if (state) { + // syncState utils requires to handle incoming "null" value + stateContainer.set(state); + } + }, + }, + stateStorage: kbnUrlStateStorage, + }); + + // start syncing the appState with the ('_a') url + startStateSync(); + + return { stateContainer, stopStateSync }; +} diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js index 502bd6e56fb1f..6acdb0abdd0b5 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js @@ -18,7 +18,7 @@ */ export function initVisualizationDirective(app, deps) { - app.directive('visualizationEmbedded', function($timeout, getAppState) { + app.directive('visualizationEmbedded', function($timeout) { return { restrict: 'E', scope: { @@ -27,6 +27,7 @@ export function initVisualizationDirective(app, deps) { timeRange: '=', filters: '=', query: '=', + appState: '=', }, link: function($scope, element) { $scope.renderFunction = async () => { @@ -37,7 +38,7 @@ export function initVisualizationDirective(app, deps) { timeRange: $scope.timeRange, filters: $scope.filters || [], query: $scope.query, - appState: getAppState(), + appState: $scope.appState, uiState: $scope.uiState, }); $scope._handler.render(element[0]); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js index 8032152f88173..65c25b8cf705d 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js @@ -18,7 +18,7 @@ */ export function initVisEditorDirective(app, deps) { - app.directive('visualizationEditor', function($timeout, getAppState) { + app.directive('visualizationEditor', function($timeout) { return { restrict: 'E', scope: { @@ -27,9 +27,10 @@ export function initVisEditorDirective(app, deps) { timeRange: '=', filters: '=', query: '=', + appState: '=', }, link: function($scope, element) { - const Editor = $scope.savedObj.vis.type.editor; + const Editor = $scope.savedObj.vis.type.editor || deps.DefaultVisualizationEditor; const editor = new Editor(element[0], $scope.savedObj); $scope.renderFunction = () => { @@ -41,7 +42,7 @@ export function initVisEditorDirective(app, deps) { timeRange: $scope.timeRange, filters: $scope.filters, query: $scope.query, - appState: getAppState(), + appState: $scope.appState, }); }; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts index 524bc4b3196b7..139c247aa29cc 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts @@ -20,10 +20,37 @@ import { TimeRange, Query, Filter, DataPublicPluginStart } from 'src/plugins/data/public'; import { IEmbeddableStart } from 'src/plugins/embeddable/public'; import { LegacyCoreStart } from 'kibana/public'; -import { VisSavedObject, AppState, PersistedState } from '../legacy_imports'; +import { VisState, Vis } from 'src/legacy/core_plugins/visualizations/public'; +import { VisSavedObject, PersistedState } from '../legacy_imports'; + +export type PureVisState = ReturnType; + +export interface VisualizeAppState { + filters: Filter[]; + uiState: PersistedState; + vis: PureVisState; + query: Query; + savedQuery?: string; + linked: boolean; +} + +export interface VisualizeAppStateTransitions { + set: ( + state: VisualizeAppState + ) => ( + prop: T, + value: VisualizeAppState[T] + ) => VisualizeAppState; + setVis: (state: VisualizeAppState) => (vis: Partial) => VisualizeAppState; + removeSavedQuery: (state: VisualizeAppState) => (defaultQuery: Query) => VisualizeAppState; + unlinkSavedSearch: ( + state: VisualizeAppState + ) => (query: Query, filters: Filter[]) => VisualizeAppState; + updateVisState: (state: VisualizeAppState) => (vis: PureVisState) => VisualizeAppState; +} export interface EditorRenderProps { - appState: AppState; + appState: { save(): void }; core: LegacyCoreStart; data: DataPublicPluginStart; embeddable: IEmbeddableStart; diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index 22804685db3cc..9f2283d29c203 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -50,6 +50,7 @@ import { HomePublicPluginSetup, } from '../../../../../plugins/home/public'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; +import { DefaultEditorController } from '../../../vis_default_editor/public'; export interface VisualizePluginStartDependencies { data: DataPublicPluginStart; @@ -139,7 +140,7 @@ export class VisualizePlugin implements Plugin { localStorage: new Storage(localStorage), navigation, savedObjectsClient, - savedVisualizations: visualizations.getSavedVisualizationsLoader(), + savedVisualizations: visualizations.savedVisualizationsLoader, savedQueryService: dataStart.query.savedQueries, share, toastNotifications: coreStart.notifications.toasts, @@ -150,6 +151,7 @@ export class VisualizePlugin implements Plugin { usageCollection, I18nContext: coreStart.i18n.Context, setActiveUrl, + DefaultVisualizationEditor: DefaultEditorController, }; setServices(deps); diff --git a/src/legacy/core_plugins/kibana/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/ui_setting_defaults.js index 744ede891b84a..c0628b72c2ce7 100644 --- a/src/legacy/core_plugins/kibana/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/ui_setting_defaults.js @@ -690,17 +690,6 @@ export function getUiSettingDefaults() { 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation', }), }, - 'indexPattern:fieldMapping:lookBack': { - name: i18n.translate('kbn.advancedSettings.indexPattern.recentMatchingTitle', { - defaultMessage: 'Recent matching patterns', - }), - value: 5, - description: i18n.translate('kbn.advancedSettings.indexPattern.recentMatchingText', { - defaultMessage: - 'For index patterns containing timestamps in their names, look for this many recent matching ' + - 'patterns from which to query the field mapping', - }), - }, 'format:defaultTypeMap': { name: i18n.translate('kbn.advancedSettings.format.defaultTypeMapTitle', { defaultMessage: 'Field type format name', @@ -1174,24 +1163,5 @@ export function getUiSettingDefaults() { category: ['accessibility'], requiresPageReload: true, }, - pageNavigation: { - name: i18n.translate('kbn.advancedSettings.pageNavigationName', { - defaultMessage: 'Side nav style', - }), - value: 'grouped', - description: i18n.translate('kbn.advancedSettings.pageNavigationDesc', { - defaultMessage: 'Change the style of navigation', - }), - type: 'select', - options: ['grouped', 'individual'], - optionLabels: { - grouped: i18n.translate('kbn.advancedSettings.pageNavigationGrouped', { - defaultMessage: 'Grouped', - }), - individual: i18n.translate('kbn.advancedSettings.pageNavigationIndividual', { - defaultMessage: 'Individual', - }), - }, - }, }; } diff --git a/src/legacy/core_plugins/telemetry/mappings.json b/src/legacy/core_plugins/telemetry/mappings.json index a88372a5578e8..fa9cc93d6363a 100644 --- a/src/legacy/core_plugins/telemetry/mappings.json +++ b/src/legacy/core_plugins/telemetry/mappings.json @@ -17,6 +17,13 @@ }, "userHasSeenNotice": { "type": "boolean" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "ignore_above": 256, + "type": "keyword" } } } diff --git a/src/legacy/core_plugins/telemetry/server/collection_manager.ts b/src/legacy/core_plugins/telemetry/server/collection_manager.ts index 0394dea343adf..715ca56e290a2 100644 --- a/src/legacy/core_plugins/telemetry/server/collection_manager.ts +++ b/src/legacy/core_plugins/telemetry/server/collection_manager.ts @@ -20,6 +20,7 @@ import { encryptTelemetry } from './collectors'; import { CallCluster } from '../../elasticsearch'; import { UsageCollectionSetup } from '../../../../plugins/usage_collection/server'; +import { ESLicense } from './telemetry_collection/get_local_license'; export type EncryptedStatsGetterConfig = { unencrypted: false } & { server: any; @@ -45,22 +46,38 @@ export interface StatsCollectionConfig { end: string | number; } +export interface BasicStatsPayload { + timestamp: string; + cluster_uuid: string; + cluster_name: string; + version: string; + cluster_stats: object; + collection?: string; + stack_stats: object; +} + export type StatsGetterConfig = UnencryptedStatsGetterConfig | EncryptedStatsGetterConfig; export type ClusterDetailsGetter = (config: StatsCollectionConfig) => Promise; -export type StatsGetter = ( +export type StatsGetter = ( + clustersDetails: ClusterDetails[], + config: StatsCollectionConfig +) => Promise; +export type LicenseGetter = ( clustersDetails: ClusterDetails[], config: StatsCollectionConfig -) => Promise; +) => Promise<{ [clusterUuid: string]: ESLicense | undefined }>; -interface CollectionConfig { +interface CollectionConfig { title: string; priority: number; esCluster: string; - statsGetter: StatsGetter; + statsGetter: StatsGetter; clusterDetailsGetter: ClusterDetailsGetter; + licenseGetter: LicenseGetter; } interface Collection { statsGetter: StatsGetter; + licenseGetter: LicenseGetter; clusterDetailsGetter: ClusterDetailsGetter; esCluster: string; title: string; @@ -70,8 +87,15 @@ export class TelemetryCollectionManager { private usageGetterMethodPriority = -1; private collections: Collection[] = []; - public setCollection = (collectionConfig: CollectionConfig) => { - const { title, priority, esCluster, statsGetter, clusterDetailsGetter } = collectionConfig; + public setCollection = (collectionConfig: CollectionConfig) => { + const { + title, + priority, + esCluster, + statsGetter, + clusterDetailsGetter, + licenseGetter, + } = collectionConfig; if (typeof priority !== 'number') { throw new Error('priority must be set.'); @@ -88,10 +112,14 @@ export class TelemetryCollectionManager { throw Error('esCluster name must be set for the getCluster method.'); } if (!clusterDetailsGetter) { - throw Error('Cluser UUIds method is not set.'); + throw Error('Cluster UUIds method is not set.'); + } + if (!licenseGetter) { + throw Error('License getter method not set.'); } this.collections.unshift({ + licenseGetter, statsGetter, clusterDetailsGetter, esCluster, @@ -141,7 +169,19 @@ export class TelemetryCollectionManager { return; } - return await collection.statsGetter(clustersDetails, statsCollectionConfig); + const [stats, licenses] = await Promise.all([ + collection.statsGetter(clustersDetails, statsCollectionConfig), + collection.licenseGetter(clustersDetails, statsCollectionConfig), + ]); + + return stats.map(stat => { + const license = licenses[stat.cluster_uuid]; + return { + ...(license ? { license } : {}), + ...stat, + collectionSource: collection.title, + }; + }); }; public getOptInStats = async (optInStatus: boolean, config: StatsGetterConfig) => { diff --git a/src/legacy/core_plugins/telemetry/server/fetcher.ts b/src/legacy/core_plugins/telemetry/server/fetcher.ts index 6e16328c4abd8..d30ee10066813 100644 --- a/src/legacy/core_plugins/telemetry/server/fetcher.ts +++ b/src/legacy/core_plugins/telemetry/server/fetcher.ts @@ -21,19 +21,26 @@ import moment from 'moment'; // @ts-ignore import fetch from 'node-fetch'; import { telemetryCollectionManager } from './collection_manager'; -import { getTelemetryOptIn, getTelemetrySendUsageFrom } from './telemetry_config'; +import { + getTelemetryOptIn, + getTelemetrySendUsageFrom, + getTelemetryFailureDetails, +} from './telemetry_config'; import { getTelemetrySavedObject, updateTelemetrySavedObject } from './telemetry_repository'; import { REPORT_INTERVAL_MS } from '../common/constants'; export class FetcherTask { - private readonly checkDurationMs = 60 * 1000 * 5; + private readonly initialCheckDelayMs = 60 * 1000 * 5; + private readonly checkIntervalMs = 60 * 1000 * 60 * 12; private intervalId?: NodeJS.Timeout; private lastReported?: number; + private currentVersion: string; private isSending = false; private server: any; constructor(server: any) { this.server = server; + this.currentVersion = this.server.config().get('pkg.version'); } private getInternalRepository = () => { @@ -52,6 +59,9 @@ export class FetcherTask { const allowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus'); const configTelemetryOptIn = config.get('telemetry.optIn'); const telemetryUrl = config.get('telemetry.url') as string; + const { failureCount, failureVersion } = await getTelemetryFailureDetails({ + telemetrySavedObject, + }); return { telemetryOptIn: getTelemetryOptIn({ @@ -65,6 +75,8 @@ export class FetcherTask { configTelemetrySendUsageFrom, }), telemetryUrl, + failureCount, + failureVersion, }; }; @@ -72,11 +84,31 @@ export class FetcherTask { const internalRepository = this.getInternalRepository(); this.lastReported = Date.now(); updateTelemetrySavedObject(internalRepository, { + reportFailureCount: 0, lastReported: this.lastReported, }); }; - private shouldSendReport = ({ telemetryOptIn, telemetrySendUsageFrom }: any) => { + private updateReportFailure = async ({ failureCount }: { failureCount: number }) => { + const internalRepository = this.getInternalRepository(); + + updateTelemetrySavedObject(internalRepository, { + reportFailureCount: failureCount + 1, + reportFailureVersion: this.currentVersion, + }); + }; + + private shouldSendReport = ({ + telemetryOptIn, + telemetrySendUsageFrom, + reportFailureCount, + currentVersion, + reportFailureVersion, + }: any) => { + if (reportFailureCount > 2 && reportFailureVersion === currentVersion) { + return false; + } + if (telemetryOptIn && telemetrySendUsageFrom === 'server') { if (!this.lastReported || Date.now() - this.lastReported > REPORT_INTERVAL_MS) { return true; @@ -98,6 +130,14 @@ export class FetcherTask { private sendTelemetry = async (url: string, cluster: any): Promise => { this.server.log(['debug', 'telemetry', 'fetcher'], `Sending usage stats.`); + /** + * send OPTIONS before sending usage data. + * OPTIONS is less intrusive as it does not contain any payload and is used here to check if the endpoint is reachable. + */ + await fetch(url, { + method: 'options', + }); + await fetch(url, { method: 'post', body: cluster, @@ -108,21 +148,23 @@ export class FetcherTask { if (this.isSending) { return; } - try { - const telemetryConfig = await this.getCurrentConfigs(); - if (!this.shouldSendReport(telemetryConfig)) { - return; - } + const telemetryConfig = await this.getCurrentConfigs(); + if (!this.shouldSendReport(telemetryConfig)) { + return; + } - // mark that we are working so future requests are ignored until we're done + try { this.isSending = true; const clusters = await this.fetchTelemetry(); + const { telemetryUrl } = telemetryConfig; for (const cluster of clusters) { - await this.sendTelemetry(telemetryConfig.telemetryUrl, cluster); + await this.sendTelemetry(telemetryUrl, cluster); } await this.updateLastReported(); } catch (err) { + await this.updateReportFailure(telemetryConfig); + this.server.log( ['warning', 'telemetry', 'fetcher'], `Error sending telemetry usage data: ${err}` @@ -132,8 +174,12 @@ export class FetcherTask { }; public start = () => { - this.intervalId = setInterval(() => this.sendIfDue(), this.checkDurationMs); + setTimeout(() => { + this.sendIfDue(); + this.intervalId = setInterval(() => this.sendIfDue(), this.checkIntervalMs); + }, this.initialCheckDelayMs); }; + public stop = () => { if (this.intervalId) { clearInterval(this.intervalId); diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.ts similarity index 61% rename from src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.js rename to src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.ts index 2e4ed0b36ed26..67812457ed4ec 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.js +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.ts @@ -17,14 +17,32 @@ * under the License. */ +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; + +// This can be removed when the ES client improves the types +export interface ESClusterInfo { + cluster_uuid: string; + cluster_name: string; + version: { + number: string; + build_flavor: string; + build_type: string; + build_hash: string; + build_date: string; + build_snapshot?: boolean; + lucene_version: string; + minimum_wire_compatibility_version: string; + minimum_index_compatibility_version: string; + }; +} + /** * Get the cluster info from the connected cluster. * * This is the equivalent to GET / * * @param {function} callCluster The callWithInternalUser handler (exposed for testing) - * @return {Promise} The response from Elasticsearch. */ -export function getClusterInfo(callCluster) { - return callCluster('info'); +export function getClusterInfo(callCluster: CallCluster) { + return callCluster('info'); } diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_license.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_license.ts new file mode 100644 index 0000000000000..589392ffb6095 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_license.ts @@ -0,0 +1,90 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LicenseGetter } from '../collection_manager'; + +// From https://www.elastic.co/guide/en/elasticsearch/reference/current/get-license.html +export interface ESLicense { + status: string; + uid: string; + type: string; + issue_date: string; + issue_date_in_millis: number; + expiry_date: string; + expirty_date_in_millis: number; + max_nodes: number; + issued_to: string; + issuer: string; + start_date_in_millis: number; +} +let cachedLicense: ESLicense | undefined; + +function fetchLicense(callCluster: CallCluster, local: boolean) { + return callCluster<{ license: ESLicense }>('transport.request', { + method: 'GET', + path: '/_license', + query: { + local, + // For versions >= 7.6 and < 8.0, this flag is needed otherwise 'platinum' is returned for 'enterprise' license. + accept_enterprise: 'true', + }, + }); +} + +/** + * Get the cluster's license from the connected node. + * + * This is the equivalent of GET /_license?local=true . + * + * Like any X-Pack related API, X-Pack must installed for this to work. + */ +async function getLicenseFromLocalOrMaster(callCluster: CallCluster) { + // Fetching the local license is cheaper than getting it from the master and good enough + const { license } = await fetchLicense(callCluster, true).catch(async err => { + if (cachedLicense) { + try { + // Fallback to the master node's license info + const response = await fetchLicense(callCluster, false); + return response; + } catch (masterError) { + if (masterError.statusCode === 404) { + // If the master node does not have a license, we can assume there is no license + cachedLicense = undefined; + } else { + // Any other errors from the master node, throw and do not send any telemetry + throw err; + } + } + } + return { license: void 0 }; + }); + + if (license) { + cachedLicense = license; + } + return license; +} + +export const getLocalLicense: LicenseGetter = async (clustersDetails, { callCluster }) => { + const license = await getLicenseFromLocalOrMaster(callCluster); + + // It should be called only with 1 cluster element in the clustersDetails array, but doing reduce just in case. + return clustersDetails.reduce((acc, { clusterUuid }) => ({ ...acc, [clusterUuid]: license }), {}); +}; diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts index 8adb6d237bee8..d99710deb1cbc 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -17,11 +17,8 @@ * under the License. */ -import { get, omit } from 'lodash'; -// @ts-ignore -import { getClusterInfo } from './get_cluster_info'; +import { getClusterInfo, ESClusterInfo } from './get_cluster_info'; import { getClusterStats } from './get_cluster_stats'; -// @ts-ignore import { getKibana, handleKibanaStats, KibanaUsageStats } from './get_kibana'; import { StatsGetter } from '../collection_manager'; @@ -33,20 +30,19 @@ import { StatsGetter } from '../collection_manager'; * @param {Object} clusterInfo Cluster info (GET /) * @param {Object} clusterStats Cluster stats (GET /_cluster/stats) * @param {Object} kibana The Kibana Usage stats - * @return {Object} A combined object containing the different responses. */ export function handleLocalStats( server: any, - clusterInfo: any, - clusterStats: any, + { cluster_name, cluster_uuid, version }: ESClusterInfo, + { _nodes, cluster_name: clusterName, ...clusterStats }: any, kibana: KibanaUsageStats ) { return { timestamp: new Date().toISOString(), - cluster_uuid: get(clusterInfo, 'cluster_uuid'), - cluster_name: get(clusterInfo, 'cluster_name'), - version: get(clusterInfo, 'version.number'), - cluster_stats: omit(clusterStats, '_nodes', 'cluster_name'), + cluster_uuid, + cluster_name, + version: version.number, + cluster_stats: clusterStats, collection: 'local', stack_stats: { kibana: handleKibanaStats(server, kibana), @@ -54,14 +50,12 @@ export function handleLocalStats( }; } +export type TelemetryLocalStats = ReturnType; + /** * Get statistics for all products joined by Elasticsearch cluster. - * - * @param {Object} server The Kibana server instance used to call ES as the internal user - * @param {function} callCluster The callWithInternalUser handler (exposed for testing) - * @return {Promise} The object containing the current Elasticsearch cluster's telemetry. */ -export const getLocalStats: StatsGetter = async (clustersDetails, config) => { +export const getLocalStats: StatsGetter = async (clustersDetails, config) => { const { server, callCluster, usageCollection } = config; return await Promise.all( diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts index 7f228dbc5e6f6..9ac94216c21bc 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts @@ -17,7 +17,6 @@ * under the License. */ -// @ts-ignore export { getLocalStats } from './get_local_stats'; export { getClusterUuids } from './get_cluster_stats'; export { registerCollection } from './register_collection'; diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/register_collection.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/register_collection.ts index faf8e9de79194..6580b47dba08e 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/register_collection.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/register_collection.ts @@ -39,6 +39,7 @@ import { telemetryCollectionManager } from '../collection_manager'; import { getLocalStats } from './get_local_stats'; import { getClusterUuids } from './get_cluster_stats'; +import { getLocalLicense } from './get_local_license'; export function registerCollection() { telemetryCollectionManager.setCollection({ @@ -47,5 +48,6 @@ export function registerCollection() { priority: 0, statsGetter: getLocalStats, clusterDetailsGetter: getClusterUuids, + licenseGetter: getLocalLicense, }); } diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_failure_details.test.ts b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_failure_details.test.ts new file mode 100644 index 0000000000000..c92696838e8e8 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_failure_details.test.ts @@ -0,0 +1,96 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getTelemetryFailureDetails } from './get_telemetry_failure_details'; + +describe('getTelemetryFailureDetails: get details about server usage fetcher failures', () => { + it('returns `failureCount: 0` and `failureVersion: undefined` when telemetry does not have any custom configs in saved Object', () => { + const telemetrySavedObject = null; + const failureDetails = getTelemetryFailureDetails({ telemetrySavedObject }); + expect(failureDetails).toStrictEqual({ + failureVersion: undefined, + failureCount: 0, + }); + }); + + it('returns telemetryFailureCount and reportFailureVersion from telemetry saved Object', () => { + const telemetrySavedObject = { + reportFailureCount: 12, + reportFailureVersion: '8.0.0', + }; + const failureDetails = getTelemetryFailureDetails({ telemetrySavedObject }); + expect(failureDetails).toStrictEqual({ + failureVersion: '8.0.0', + failureCount: 12, + }); + }); + + it('returns `failureCount: 0` on malformed reportFailureCount telemetry saved Object', () => { + const failureVersion = '8.0.0'; + expect( + getTelemetryFailureDetails({ + telemetrySavedObject: { + reportFailureCount: null, + reportFailureVersion: failureVersion, + } as any, + }) + ).toStrictEqual({ failureVersion, failureCount: 0 }); + expect( + getTelemetryFailureDetails({ + telemetrySavedObject: { + reportFailureCount: undefined, + reportFailureVersion: failureVersion, + }, + }) + ).toStrictEqual({ failureVersion, failureCount: 0 }); + expect( + getTelemetryFailureDetails({ + telemetrySavedObject: { + reportFailureCount: 'not_a_number', + reportFailureVersion: failureVersion, + } as any, + }) + ).toStrictEqual({ failureVersion, failureCount: 0 }); + }); + + it('returns `failureVersion: undefined` on malformed reportFailureCount telemetry saved Object', () => { + const failureCount = 0; + expect( + getTelemetryFailureDetails({ + telemetrySavedObject: { + reportFailureVersion: null, + reportFailureCount: failureCount, + } as any, + }) + ).toStrictEqual({ failureCount, failureVersion: undefined }); + expect( + getTelemetryFailureDetails({ + telemetrySavedObject: { reportFailureVersion: undefined, reportFailureCount: failureCount }, + }) + ).toStrictEqual({ failureCount, failureVersion: undefined }); + expect( + getTelemetryFailureDetails({ + telemetrySavedObject: { + reportFailureVersion: 123, + reportFailureCount: failureCount, + } as any, + }) + ).toStrictEqual({ failureCount, failureVersion: undefined }); + }); +}); diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_failure_details.ts b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_failure_details.ts new file mode 100644 index 0000000000000..2952fa96a5cf3 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_failure_details.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object'; + +interface GetTelemetryFailureDetailsConfig { + telemetrySavedObject: TelemetrySavedObject; +} + +export interface TelemetryFailureDetails { + failureCount: number; + failureVersion?: string; +} + +export function getTelemetryFailureDetails({ + telemetrySavedObject, +}: GetTelemetryFailureDetailsConfig): TelemetryFailureDetails { + if (!telemetrySavedObject) { + return { + failureVersion: undefined, + failureCount: 0, + }; + } + const { reportFailureCount, reportFailureVersion } = telemetrySavedObject; + + return { + failureCount: typeof reportFailureCount === 'number' ? reportFailureCount : 0, + failureVersion: typeof reportFailureVersion === 'string' ? reportFailureVersion : undefined, + }; +} diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_config/index.ts b/src/legacy/core_plugins/telemetry/server/telemetry_config/index.ts index ab30dac1c3666..bf9855ce7538e 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_config/index.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_config/index.ts @@ -21,3 +21,7 @@ export { replaceTelemetryInjectedVars } from './replace_injected_vars'; export { getTelemetryOptIn } from './get_telemetry_opt_in'; export { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from'; export { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status'; +export { + getTelemetryFailureDetails, + TelemetryFailureDetails, +} from './get_telemetry_failure_details'; diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_repository/index.ts b/src/legacy/core_plugins/telemetry/server/telemetry_repository/index.ts index b9ba2ce5573c3..f1735d1bb2866 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_repository/index.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_repository/index.ts @@ -27,4 +27,6 @@ export interface TelemetrySavedObjectAttributes { lastReported?: number; telemetryAllowChangingOptInStatus?: boolean; userHasSeenNotice?: boolean; + reportFailureCount?: number; + reportFailureVersion?: string; } diff --git a/src/legacy/core_plugins/timelion/public/directives/saved_object_save_as_checkbox.html b/src/legacy/core_plugins/timelion/public/directives/saved_object_save_as_checkbox.html index 3e4a1526113c3..5adce4286010a 100644 --- a/src/legacy/core_plugins/timelion/public/directives/saved_object_save_as_checkbox.html +++ b/src/legacy/core_plugins/timelion/public/directives/saved_object_save_as_checkbox.html @@ -5,7 +5,7 @@ i18n-id="timelion.savedObjects.howToSaveAsNewDescription" i18n-default-message="In previous versions of Kibana, changing the name of a {savedObjectName} would make a copy with the new name. Use the 'Save as a new {savedObjectName}' checkbox to do this now." i18n-values="{ savedObjectName: savedObject.getDisplayName() }" - i18n-description="'Save as a new {savedObjectName}' refers to common.ui.savedObjects.saveAsNewLabel and should be the same text." + i18n-description="'Save as a new {savedObjectName}' refers to timelion.savedObjects.saveAsNewLabel and should be the same text." >