diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index e40cc584dc376c..fa1e141be93eaa 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 56db8d3793f57c..51433f598ac16e 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 @@ -184,3 +186,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/development/core/public/kibana-plugin-public.ibasepath.md b/docs/development/core/public/kibana-plugin-public.ibasepath.md index ca4c4b7ad3be71..7f2070eb1fd6db 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 00000000000000..0c2b5451767c77 --- /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 00000000000000..ba986b75503eda --- /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/server/kibana-plugin-server.elasticsearchconfig._constructor_.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig._constructor_.md new file mode 100644 index 00000000000000..55e5cf74fc512c --- /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 00000000000000..097654f1fb090a --- /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 00000000000000..0b3998e59c5df2 --- /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 00000000000000..b5589727d80aa4 --- /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 00000000000000..29770ba5e0795f --- /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 00000000000000..42e32f920c1dbf --- /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 00000000000000..64de7f65044508 --- /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 00000000000000..e478dc7b966a25 --- /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 00000000000000..ffe6f75e9874de --- /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 00000000000000..09123f0969b601 --- /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 00000000000000..eeced56e3103f0 --- /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 00000000000000..dbd5ecb9396731 --- /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 00000000000000..aa923042bf64fc --- /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 00000000000000..37fd2a7439535a --- /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 00000000000000..c703be548d34bc --- /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 00000000000000..26a7d9cc11a80d --- /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 00000000000000..4d23c410f59fa2 --- /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 00000000000000..d0098d656befbf --- /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 482f014b226b9f..9ec443d6482e89 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/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index ec626677d09025..80c9053dc5ae6f 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -70,9 +70,6 @@ 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/package.json b/package.json index b11234b6312e98..cb87c48ab7f2a7 100644 --- a/package.json +++ b/package.json @@ -205,7 +205,7 @@ "leaflet.heat": "0.2.0", "less": "^2.7.3", "less-loader": "5.0.0", - "lodash": "npm:@elastic/lodash@3.10.1-kibana3", + "lodash": "npm:@elastic/lodash@3.10.1-kibana4", "lodash.clonedeep": "^4.5.0", "lru-cache": "4.1.5", "markdown-it": "^10.0.0", diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json index d2f0b0c3582845..5dede7fbf1aaa9 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 44234acd897dc5..2337017f54ed8a 100644 --- a/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts +++ b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts @@ -17,14 +17,35 @@ * 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'); + jest.spyOn(realFs, 'readFile'); + return realFs; +}); + expect.addSnapshotSerializer(createAbsolutePathSerializer()); jest.requireMock('execa').mockImplementation(async (cmd: string, args: string[], opts: object) => { @@ -46,28 +67,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 3529ffa587f16c..af6a8a648d29cb 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-storybook/index.js b/packages/kbn-storybook/index.js index 78e2cf7f5073b3..b595de8ea1c07c 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 bef08a5120a36b..16754ad550da0a 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 fc245ca3fe9217..0402a83d3d274c 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 9a5fb479276f70..5028c6efdb40eb 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 5f5ac3f1c9c2ff..c7c004bd55794d 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 00000000000000..570aca86c484c5 --- /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 6795d363a9f1d2..acb2b48e12278d 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -12,6 +12,7 @@ "@elastic/charts": "^17.0.2", "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 c5c3cba147fcfb..5d981c73f1d211 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 87cca2cc897f84..dc6e7ae33dbecd 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 23f33940283c0d..594c2efc8adc92 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 00000000000000..33dd4787efad92 --- /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 00000000000000..f5148899ff8542 --- /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 82d514877aff64..00000000000000 --- 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 00000000000000..2def3569828d34 --- /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 9e57fc4c368762..d33fd9bcce7a01 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 6ab9fe158742a6..2b0b115ce068ee 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 cf3b48f237286b..00000000000000 --- 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 c3cefd180b16f6..c9a583f39b30cb 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 7272935b93a520..00000000000000 --- 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 dbb68d5dd3901e..c57faec1e428d7 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 63b7fa61cee846..6468e674d5e788 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 6352327c41625b..67464a6196b027 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 8965747ba68374..44fc9d65565d4e 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 5909572c7e545e..6370ae165282b0 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 3c06f40d976db6..5caa9830a643db 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 64a8b8a855fb46..75abdd6d87d5ab 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 f0289cc2b83550..ca2f6789bebeee 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 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 f2fc9bfe232e2a..99676f6b78d42d 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/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index 50866e5550d8ec..b2f4e388b337d3 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 5d64fadfaa1842..cfd72a6fd5e47b 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 cc838ddd1351df..52827b72ee0cc0 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 3fbec7a45148da..ed64e7c4ce0b1c 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 5e6e977663bc4c..3b11313367d9cf 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 11d1fb271c81d5..dbafd5806bd74a 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 3f9f6ff2949097..cfaa23d4911396 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/server.api.md b/src/core/server/server.api.md index ad1907df571fbe..053a60028fc5ff 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) @@ -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/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 1dce53b6c2a843..fb91b865097faf 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 0f7dc40ceef0b3..efb618a48cd6e5 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 b4ae02a487049f..4480caae39664a 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 50120292a627a6..ce46f534141f4f 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -81,4 +81,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 7e7e4944b00da9..8e091ed5f21ae0 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 34727ff4614b95..551cb81529a0a3 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 0fef7f38aae742..0bdb92b8de65e8 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 8f7953c408a971..e85e9deff6ddf9 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 9aee7124c95211..302527e4ed549f 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 90e191b769a8df..96d2825559da26 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 b85b45d3c5820a..ef2748102623ab 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 00000000000000..8078136299f8c6 --- /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 00000000000000..cfd4cd7de640b2 --- /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 4144d5be16012a..8bffca65b4ae2f 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/plugins/embeddable/public/components/embeddable_panel/__examples__/embeddable_panel.examples.tsx b/src/legacy/core_plugins/data/public/search/tabify/index.ts similarity index 79% rename from src/plugins/embeddable/public/components/embeddable_panel/__examples__/embeddable_panel.examples.tsx rename to src/legacy/core_plugins/data/public/search/tabify/index.ts index 7ec8848b8cebdd..90ac3f2fb730b9 100644 --- a/src/plugins/embeddable/public/components/embeddable_panel/__examples__/embeddable_panel.examples.tsx +++ b/src/legacy/core_plugins/data/public/search/tabify/index.ts @@ -17,8 +17,7 @@ * under the License. */ -import * as React from 'react'; -import { storiesOf } from '@storybook/react'; -import { EmbeddablePanel } from '..'; +export { tabifyAggResponse } from './tabify'; +export { tabifyGetColumns } from './get_columns'; -storiesOf('components/EmbeddablePanel', module).add('default', () => ); +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 00000000000000..f5df0a683ca00c --- /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 00000000000000..c910eda0245404 --- /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 00000000000000..13fe7719b0a856 --- /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 00000000000000..078d3f7f72759b --- /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 00000000000000..964a9d2080e7bd --- /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 305f27a86b398c..e0afe99aa81fac 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/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 8e6bae0b588bc0..221133a17d59ab 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 d5198dc557f04f..c1f679e9eb7ac5 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 f7fc3b0891feff..c9f56dc898381d 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 cc104c1a931d00..7239d8f2258a74 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 465203be0d34c1..075516d52bab63 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/lib/save_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts index d80208ce27ffe4..db2b1f15247de5 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/saved_dashboard/saved_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.ts index 5babaf8061de9a..c5ac05b5a77ebc 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 4ece5d46358ba5..2ff76da9c5ca65 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 b0bb17ce1ac7f7..91b5c7f13dc954 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/saved_searches/_saved_search.ts b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts index 113d13287bd122..7bd0eef8c19afd 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 0b346524610262..ebd341eba99fde 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 b0d94711be7b62..db24cb3e3c1b7e 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/management/index.js b/src/legacy/core_plugins/kibana/public/management/index.js index 2cba9fab7be222..6a36391c56b5c3 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 0a6ac205026693..e0756b2e78e259 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/render.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/render.test.js index af580547b11ed8..1b9dafb6daf237 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/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/ui_setting_defaults.js index 744ede891b84a3..f92694eabe58d1 100644 --- a/src/legacy/core_plugins/kibana/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/ui_setting_defaults.js @@ -1174,24 +1174,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/timelion/public/directives/saved_object_save_as_checkbox.html b/src/legacy/core_plugins/timelion/public/directives/saved_object_save_as_checkbox.html index 3e4a1526113c3e..5adce4286010a6 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." >