diff --git a/.ci/packer_cache_for_branch.sh b/.ci/packer_cache_for_branch.sh index bc427bf927f11..bbdf5484faf65 100755 --- a/.ci/packer_cache_for_branch.sh +++ b/.ci/packer_cache_for_branch.sh @@ -5,6 +5,17 @@ set -e branch="$1" checkoutDir="$(pwd)" +function cleanup() +{ + if [[ "$branch" != "master" ]]; then + rm --preserve-root -rf "$checkoutDir" + fi + + exit 0 +} + +trap 'cleanup' 0 + if [[ "$branch" != "master" ]]; then checkoutDir="/tmp/kibana-$branch" git clone https://github.com/elastic/kibana.git --branch "$branch" --depth 1 "$checkoutDir" @@ -56,6 +67,3 @@ echo "created $HOME/.kibana/bootstrap_cache/$branch.tar" .ci/build_docker.sh -if [[ "$branch" != "master" ]]; then - rm --preserve-root -rf "$checkoutDir" -fi diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cbdf292964472..5c768dccaf274 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -236,6 +236,7 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Security /src/core/server/csp/ @elastic/kibana-security @elastic/kibana-core /src/plugins/security_oss/ @elastic/kibana-security +/src/plugins/spaces_oss/ @elastic/kibana-security /test/security_functional/ @elastic/kibana-security /x-pack/plugins/spaces/ @elastic/kibana-security /x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security diff --git a/docs/apm/images/all-instances.png b/docs/apm/images/all-instances.png new file mode 100644 index 0000000000000..e77c8af2c46f6 Binary files /dev/null and b/docs/apm/images/all-instances.png differ diff --git a/docs/apm/images/error-rate.png b/docs/apm/images/error-rate.png new file mode 100644 index 0000000000000..7e5e32c50f13e Binary files /dev/null and b/docs/apm/images/error-rate.png differ diff --git a/docs/apm/images/latency.png b/docs/apm/images/latency.png new file mode 100644 index 0000000000000..4c970d8c582e6 Binary files /dev/null and b/docs/apm/images/latency.png differ diff --git a/docs/apm/images/metadata-icons.png b/docs/apm/images/metadata-icons.png new file mode 100644 index 0000000000000..dcdac41a7d01a Binary files /dev/null and b/docs/apm/images/metadata-icons.png differ diff --git a/docs/apm/images/spans-dependencies.png b/docs/apm/images/spans-dependencies.png new file mode 100644 index 0000000000000..a827083b5ddcd Binary files /dev/null and b/docs/apm/images/spans-dependencies.png differ diff --git a/docs/apm/images/traffic-transactions.png b/docs/apm/images/traffic-transactions.png new file mode 100644 index 0000000000000..134bc0e6bcb42 Binary files /dev/null and b/docs/apm/images/traffic-transactions.png differ diff --git a/docs/apm/service-overview.asciidoc b/docs/apm/service-overview.asciidoc index 970d4c61ed92e..1afe00806474f 100644 --- a/docs/apm/service-overview.asciidoc +++ b/docs/apm/service-overview.asciidoc @@ -2,4 +2,107 @@ [[service-overview]] === Service overview -Selecting a <> brings you to the *Service overview*. \ No newline at end of file +Selecting a <> brings you to the *Service overview*. +The *Service overview* contains a wide variety of charts and tables that provide +visibility into how a service performs across your infrastructure. + +[discrete] +[[service-latency]] +=== Latency + +Response times for the service. You can filter the *Latency* chart to display the average, +95th, or 99th percentile latency times for the service. + +[role="screenshot"] +image::apm/images/latency.png[Service latency] + +[discrete] +[[service-traffic-transactions]] +=== Traffic and transactions + +The *Traffic* chart visualizes the average number of transactions per minute for the selected service. + +The *Transactions* table displays a list of _transaction groups_ for the +selected service and includes the latency, traffic, error rate, and the impact for each transaction. +Transactions that share the same name are grouped, and only one entry is displayed for each group. + +By default, transaction groups are sorted by _Impact_ to show the most used and slowest endpoints in your +service. If there is a particular endpoint you are interested in, click *View transactions* to view a +list of similar transactions on the <> page. + +[role="screenshot"] +image::apm/images/traffic-transactions.png[Traffic and transactions] + +[discrete] +[[service-error-rates]] +=== Error rate and errors + +The *Error rate* chart displays the average error rates relating to the service, within a specific time range. + +The *Errors* table provides a high-level view of each error message when it first and last occurred, +along with the total number of occurrences. This makes it very easy to quickly see which errors affect +your services and take actions to rectify them. To do so, click *View errors*. + +[role="screenshot"] +image::apm/images/error-rate.png[Error rate and errors] + +[discrete] +[[service-span-duration]] +=== Span types average duration and dependencies + +The *Average duration by span type* chart visualizes each span type's average duration and helps you determine +which spans could be slowing down transactions. The "app" label displayed under the +chart indicates that something was happening within the application. This could signal that the +agent does not have auto-instrumentation for whatever was happening during that time or that the time was spent in the +application code and not in database or external requests. + +The *Dependencies* table displays a list of downstream services or external connections relevant +to the service at the selected time range. The table displays latency, traffic, error rate, and the impact of +each dependency. By default, dependencies are sorted by _Impact_ to show the most used and the slowest dependency. +If there is a particular dependency you are interested in, click *View service map* to view the related +<>. + +[role="screenshot"] +image::apm/images/spans-dependencies.png[Span type duration and dependencies] + +[discrete] +[[service-instances]] +=== All instances + +The *All instances* table displays a list of all the available service instances within the selected time range. +Depending on how the service runs, the instance could be a host or a container. The table displays latency, traffic, +errors, CPU usage, and memory usage for each instance. By default, instances are sorted by _Traffic_. + +[role="screenshot"] +image::apm/images/all-instances.png[All instances] + +[discrete] +[[service-metadata]] +=== Service metadata + +To view metadata relating to the service agent, and if relevant, the container and cloud provider, +click on each icon located at the top of the page beside the service name. + +[role="screenshot"] +image::apm/images/metadata-icons.png[Service metadata] + +*Service information* + +* Service version +* Runtime name and version +* Framework name +* Agent name and version + +*Container information* + +* Operating system +* Containerized - Yes or no. +* Total number of instances +* Orchestration + +*Cloud provider information* + +* Cloud provider +* Availability zones +* Machine types +* Project ID diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index 3f624980e3937..83ca9e5a10a9b 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -24,6 +24,7 @@ Like in the Transaction duration graph, you can zoom in on anomalies to further *Error rate*:: Visualize the total number of transactions with errors divided by the total number of transactions. +The error rate value is based on the `event.outcome` field and is the relative number of failed transactions. Any unexpected increases, decreases, or irregular patterns can be investigated further with the <>. diff --git a/docs/developer/advanced/upgrading-nodejs.asciidoc b/docs/developer/advanced/upgrading-nodejs.asciidoc index a6fa57581772a..c1e727b1eac65 100644 --- a/docs/developer/advanced/upgrading-nodejs.asciidoc +++ b/docs/developer/advanced/upgrading-nodejs.asciidoc @@ -9,7 +9,7 @@ Theses files must be updated when upgrading Node.js: - {kib-repo}blob/{branch}/.ci/Dockerfile[`.ci/Dockerfile`] - The version is specified in the `NODE_VERSION` constant. This is used to pull the relevant image from https://hub.docker.com/_/node[Docker Hub]. - Note that Docker Hub can take 24+ hours to be updated with the new images after a new release of Node.js, so if you're upgrading Node.js in Kibana _just_ after the official Node.js release, you have to check if the new images are present on Docker Hub. + Note that Docker Hub can take 24+ hours to be updated with the new images after a new release of Node.js, so if you're upgrading Node.js in Kibana just after the official Node.js release, you have to check if the new images are present on Docker Hub. If they are not, and the update is urgent, you can skip this file and update it later once Docker Hub has been updated. - {kib-repo}blob/{branch}/.node-version[`.node-version`] - {kib-repo}blob/{branch}/.nvmrc[`.nvmrc`] @@ -29,7 +29,7 @@ Hence, upgrades to either Node.js 14 or Node.js 10 shold be done as separate PRs ==== Node.js patch upgrades -Typically, you want to backport Node.js *patch* upgrades to all supported release branches that run the same _major_ Node.js version: +Typically, you want to backport Node.js *patch* upgrades to all supported release branches that run the same *major* Node.js version: - If upgrading Node.js 14, and the current release is 7.11.1, the main PR should target `master` and be backported to `7.x` and `7.11`. - If upgrading Node.js 10, the main PR should target `6.8` only. diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index c79e46c1d9173..ba2ea98cad5e6 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -194,6 +194,10 @@ so they can properly protect the data within their clusters. |Replaces the legacy ui/share module for registering share context menus. +|{kib-repo}blob/{branch}/src/plugins/spaces_oss/README.md[spacesOss] +|Bridge plugin for consumption of the Spaces feature from OSS plugins. + + |{kib-repo}blob/{branch}/src/plugins/telemetry/README.md[telemetry] |Telemetry allows Kibana features to have usage tracked in the wild. The general term "telemetry" refers to multiple things: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md index 7bae0bca701bf..c7810b18c55a9 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md @@ -20,6 +20,7 @@ export interface DataPublicPluginStart | [autocomplete](./kibana-plugin-plugins-data-public.datapublicpluginstart.autocomplete.md) | AutocompleteStart | autocomplete service [AutocompleteStart](./kibana-plugin-plugins-data-public.autocompletestart.md) | | [fieldFormats](./kibana-plugin-plugins-data-public.datapublicpluginstart.fieldformats.md) | FieldFormatsStart | field formats service [FieldFormatsStart](./kibana-plugin-plugins-data-public.fieldformatsstart.md) | | [indexPatterns](./kibana-plugin-plugins-data-public.datapublicpluginstart.indexpatterns.md) | IndexPatternsContract | index patterns service [IndexPatternsContract](./kibana-plugin-plugins-data-public.indexpatternscontract.md) | +| [nowProvider](./kibana-plugin-plugins-data-public.datapublicpluginstart.nowprovider.md) | NowProviderPublicContract | | | [query](./kibana-plugin-plugins-data-public.datapublicpluginstart.query.md) | QueryStart | query service [QueryStart](./kibana-plugin-plugins-data-public.querystart.md) | | [search](./kibana-plugin-plugins-data-public.datapublicpluginstart.search.md) | ISearchStart | search service [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) | | [ui](./kibana-plugin-plugins-data-public.datapublicpluginstart.ui.md) | DataPublicPluginStartUi | prewired UI components [DataPublicPluginStartUi](./kibana-plugin-plugins-data-public.datapublicpluginstartui.md) | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.nowprovider.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.nowprovider.md new file mode 100644 index 0000000000000..4a93c25e28815 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.nowprovider.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DataPublicPluginStart](./kibana-plugin-plugins-data-public.datapublicpluginstart.md) > [nowProvider](./kibana-plugin-plugins-data-public.datapublicpluginstart.nowprovider.md) + +## DataPublicPluginStart.nowProvider property + +Signature: + +```typescript +nowProvider: NowProviderPublicContract; +``` diff --git a/docs/user/reporting/response-codes.asciidoc b/docs/user/reporting/response-codes.asciidoc index 5ec9bf5300124..50a27766f37fb 100644 --- a/docs/user/reporting/response-codes.asciidoc +++ b/docs/user/reporting/response-codes.asciidoc @@ -9,7 +9,7 @@ the POST URL. This is true even if the job somehow fails later, since report generation happens asynchronously from queuing. - **`400` (Bad Request)**: When sending requests to the POST URL, if you don't use - `POST` as the HTTP method, or if your request is missing the `kbn-version` header, + `POST` as the HTTP method, or if your request is missing the `kbn-xsrf` header, Kibana will return a code `400` status response for the request. - **`503` (Service Unavailable)**: When using the `path` to request the download, you diff --git a/docs/user/reporting/script-example.asciidoc b/docs/user/reporting/script-example.asciidoc index 94301fc6fb448..56721d20ea3c7 100644 --- a/docs/user/reporting/script-example.asciidoc +++ b/docs/user/reporting/script-example.asciidoc @@ -3,7 +3,7 @@ The response from this request will be JSON, and will contain a `path` property URL to use to download the generated report. Use the `GET` method in the HTTP request to download the report. -The request method must be `POST` and it must include a `kbn-version` header for Kibana +The request method must be `POST` and it must include a `kbn-xsrf` header for Kibana to allow the request. The following example queues CSV report generation using the `POST` URL with cURL: @@ -13,7 +13,7 @@ The following example queues CSV report generation using the `POST` URL with cUR curl \ -XPOST \ <1> -u elastic \ <2> --H 'kbn-version: {version}' \ <3> +-H 'kbn-xsrf: true' \ <3> 'http://0.0.0.0:5601/api/reporting/generate/csv?jobParams=...' <4> --------------------------------------------------------- // CONSOLE @@ -21,8 +21,8 @@ curl \ <1> `POST` method is required. <2> Provide user credentials for a user with permission to access Kibana and {report-features}. -<3> The `kbn-version` header is required for all `POST` requests to Kibana. -**The value must match the dotted-numeral version of the Kibana instance.** +<3> The `kbn-xsrf` header is required for all `POST` requests to Kibana. For more information, see <>. <4> The POST URL. You can copy and paste the URL for any report from the Kibana UI. Here is an example response for a successfully queued report: diff --git a/package.json b/package.json index 08ddb36523d8f..edeeebdddabf1 100644 --- a/package.json +++ b/package.json @@ -195,7 +195,7 @@ "dedent": "^0.7.0", "deep-freeze-strict": "^1.1.1", "del": "^5.1.0", - "elastic-apm-node": "^3.7.0", + "elastic-apm-node": "^3.10.0", "elasticsearch": "^16.7.0", "execa": "^4.0.2", "exit-hook": "^2.2.0", @@ -440,6 +440,7 @@ "@types/enzyme": "^3.10.5", "@types/eslint": "^6.1.3", "@types/extract-zip": "^1.6.2", + "@types/faker": "^5.1.5", "@types/fancy-log": "^1.3.1", "@types/fetch-mock": "^7.3.1", "@types/file-saver": "^2.0.0", @@ -647,7 +648,7 @@ "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-perf": "^3.2.3", "expose-loader": "^0.7.5", - "faker": "1.1.0", + "faker": "^5.1.0", "fancy-log": "^1.3.2", "fast-glob": "2.2.7", "fetch-mock": "^7.3.9", @@ -829,10 +830,10 @@ "url-loader": "^2.2.0", "use-resize-observer": "^6.0.0", "val-loader": "^1.1.1", - "vega": "^5.17.3", + "vega": "^5.18.0", "vega-lite": "^4.17.0", "vega-schema-url-parser": "^2.1.0", - "vega-tooltip": "^0.24.2", + "vega-tooltip": "^0.25.0", "venn.js": "0.2.20", "vinyl-fs": "^3.0.3", "wait-on": "^5.0.1", diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 67287089489e1..485481e2a7f14 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -106,3 +106,4 @@ pageLoadAssetSize: stackAlerts: 29684 presentationUtil: 28545 runtimeFieldEditor: 46986 + spacesOss: 18817 diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 12266ec8de2e4..43960ce7db467 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -201,10 +201,13 @@ export class DocLinksService { apiKeyServiceSettings: `${ELASTICSEARCH_DOCS}security-settings.html#api-key-service-settings`, clusterPrivileges: `${ELASTICSEARCH_DOCS}security-privileges.html#privileges-list-cluster`, elasticsearchSettings: `${ELASTICSEARCH_DOCS}security-settings.html`, + elasticsearchEnableSecurity: `${ELASTICSEARCH_DOCS}get-started-enable-security.html`, indicesPrivileges: `${ELASTICSEARCH_DOCS}security-privileges.html#privileges-list-indices`, kibanaTLS: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/configuring-tls.html`, kibanaPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-privileges.html`, mappingRoles: `${ELASTICSEARCH_DOCS}mapping-roles.html`, + mappingRolesFieldRules: `${ELASTICSEARCH_DOCS}role-mapping-resources.html#mapping-roles-rule-field`, + runAsPrivilege: `${ELASTICSEARCH_DOCS}security-privileges.html#_run_as_privilege`, }, watcher: { jiraAction: `${ELASTICSEARCH_DOCS}actions-jira.html`, @@ -219,6 +222,7 @@ export class DocLinksService { createIndex: `${ELASTICSEARCH_DOCS}indices-create-index.html`, createSnapshotLifecylePolicy: `${ELASTICSEARCH_DOCS}slm-api-put-policy.html`, createRoleMapping: `${ELASTICSEARCH_DOCS}security-api-put-role-mapping.html`, + createRoleMappingTemplates: `${ELASTICSEARCH_DOCS}security-api-put-role-mapping.html#_role_templates`, createApiKey: `${ELASTICSEARCH_DOCS}security-api-create-api-key.html`, createPipeline: `${ELASTICSEARCH_DOCS}put-pipeline-api.html`, createTransformRequest: `${ELASTICSEARCH_DOCS}put-transform.html#put-transform-request-body`, diff --git a/src/core/server/http/base_path_proxy_server.test.ts b/src/core/server/http/base_path_proxy_server.test.ts new file mode 100644 index 0000000000000..9f4ffdcf8e081 --- /dev/null +++ b/src/core/server/http/base_path_proxy_server.test.ts @@ -0,0 +1,1052 @@ +/* + * 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 { BasePathProxyServer, BasePathProxyServerOptions } from './base_path_proxy_server'; +import { loggingSystemMock } from '../logging/logging_system.mock'; +import { DevConfig } from '../dev/dev_config'; +import { EMPTY } from 'rxjs'; +import { HttpConfig } from './http_config'; +import { ByteSizeValue, schema } from '@kbn/config-schema'; +import { + KibanaRequest, + KibanaResponseFactory, + Router, + RouteValidationFunction, + RouteValidationResultFactory, +} from './router'; +import { HttpServer } from './http_server'; +import supertest from 'supertest'; +import { RequestHandlerContext } from 'kibana/server'; +import { readFileSync } from 'fs'; +import { KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; +import { omit } from 'lodash'; +import { Readable } from 'stream'; + +/** + * Most of these tests are inspired by: + * src/core/server/http/http_server.test.ts + * and copied for completeness from that file. The modifications are that these tests use the developer proxy. + */ +describe('BasePathProxyServer', () => { + let server: HttpServer; + let proxyServer: BasePathProxyServer; + let config: HttpConfig; + let configWithSSL: HttpConfig; + let basePath: string; + let certificate: string; + let key: string; + let proxySupertest: supertest.SuperTest; + const logger = loggingSystemMock.createLogger(); + const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {}); + + beforeAll(() => { + certificate = readFileSync(KBN_CERT_PATH, 'utf8'); + key = readFileSync(KBN_KEY_PATH, 'utf8'); + }); + + beforeEach(async () => { + // setup the server but don't start it until each individual test so that routes can be dynamically configured per unit test. + server = new HttpServer(logger, 'tests'); + config = ({ + name: 'kibana', + host: '127.0.0.1', + port: 10012, + compression: { enabled: true }, + requestId: { + allowFromAnyIp: true, + ipAllowlist: [], + }, + autoListen: true, + keepaliveTimeout: 1000, + socketTimeout: 1000, + cors: { + enabled: false, + allowCredentials: false, + allowOrigin: [], + }, + ssl: { enabled: false }, + customResponseHeaders: {}, + maxPayload: new ByteSizeValue(1024), + rewriteBasePath: true, + } as unknown) as HttpConfig; + + configWithSSL = { + ...config, + ssl: { + enabled: true, + certificate, + cipherSuites: ['TLS_AES_256_GCM_SHA384'], + getSecureOptions: () => 0, + key, + redirectHttpFromPort: config.port + 1, + }, + } as HttpConfig; + + // setup and start the proxy server + const proxyConfig: HttpConfig = { ...config, port: 10013 }; + const devConfig = new DevConfig({ basePathProxyTarget: config.port }); + proxyServer = new BasePathProxyServer(logger, proxyConfig, devConfig); + const options: Readonly = { + shouldRedirectFromOldBasePath: () => true, + delayUntil: () => EMPTY, + }; + await proxyServer.start(options); + + // set the base path or throw if for some unknown reason it is not setup + if (proxyServer.basePath == null) { + throw new Error('Invalid null base path, all tests will fail'); + } else { + basePath = proxyServer.basePath; + } + proxySupertest = supertest(`http://127.0.0.1:${proxyConfig.port}`); + }); + + afterEach(async () => { + await server.stop(); + await proxyServer.stop(); + jest.clearAllMocks(); + }); + + test('root URL will return a 302 redirect', async () => { + await proxySupertest.get('/').expect(302); + }); + + test('root URL will return a redirect location with exactly 3 characters that are a-z', async () => { + const res = await proxySupertest.get('/'); + const location = res.header.location; + expect(location).toMatch(/[a-z]{3}/); + }); + + test('valid params', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + router.get( + { + path: '/{test}', + validate: { + params: schema.object({ + test: schema.string(), + }), + }, + }, + (_, req, res) => { + return res.ok({ body: req.params.test }); + } + ); + const { registerRouter } = await server.setup(config); + registerRouter(router); + await server.start(); + + await proxySupertest + .get(`${basePath}/foo/some-string`) + .expect(200) + .then((res) => { + expect(res.text).toBe('some-string'); + }); + }); + + test('invalid params', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + router.get( + { + path: '/{test}', + validate: { + params: schema.object({ + test: schema.number(), + }), + }, + }, + (_, req, res) => { + return res.ok({ body: String(req.params.test) }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .get(`${basePath}/foo/some-string`) + .expect(400) + .then((res) => { + expect(res.body).toEqual({ + error: 'Bad Request', + statusCode: 400, + message: '[request params.test]: expected value of type [number] but got [string]', + }); + }); + }); + + test('valid query', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + router.get( + { + path: '/', + validate: { + query: schema.object({ + bar: schema.string(), + quux: schema.number(), + }), + }, + }, + (_, req, res) => { + return res.ok({ body: req.query }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .get(`${basePath}/foo/?bar=test&quux=123`) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ bar: 'test', quux: 123 }); + }); + }); + + test('invalid query', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + router.get( + { + path: '/', + validate: { + query: schema.object({ + bar: schema.number(), + }), + }, + }, + (_, req, res) => { + return res.ok({ body: req.query }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .get(`${basePath}/foo/?bar=test`) + .expect(400) + .then((res) => { + expect(res.body).toEqual({ + error: 'Bad Request', + statusCode: 400, + message: '[request query.bar]: expected value of type [number] but got [string]', + }); + }); + }); + + test('valid body', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + router.post( + { + path: '/', + validate: { + body: schema.object({ + bar: schema.string(), + baz: schema.number(), + }), + }, + }, + (_, req, res) => { + return res.ok({ body: req.body }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .post(`${basePath}/foo/`) + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ bar: 'test', baz: 123 }); + }); + }); + + test('valid body with validate function', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + router.post( + { + path: '/', + validate: { + body: ({ bar, baz } = {}, { ok, badRequest }) => { + if (typeof bar === 'string' && typeof baz === 'number') { + return ok({ bar, baz }); + } else { + return badRequest('Wrong payload', ['body']); + } + }, + }, + }, + (_, req, res) => { + return res.ok({ body: req.body }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .post(`${basePath}/foo/`) + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ bar: 'test', baz: 123 }); + }); + }); + + test('not inline validation - specifying params', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + const bodyValidation = ( + { bar, baz }: any = {}, + { ok, badRequest }: RouteValidationResultFactory + ) => { + if (typeof bar === 'string' && typeof baz === 'number') { + return ok({ bar, baz }); + } else { + return badRequest('Wrong payload', ['body']); + } + }; + + router.post( + { + path: '/', + validate: { + body: bodyValidation, + }, + }, + (_, req, res) => { + return res.ok({ body: req.body }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .post(`${basePath}/foo/`) + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ bar: 'test', baz: 123 }); + }); + }); + + test('not inline validation - specifying validation handler', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + const bodyValidation: RouteValidationFunction<{ bar: string; baz: number }> = ( + { bar, baz } = {}, + { ok, badRequest } + ) => { + if (typeof bar === 'string' && typeof baz === 'number') { + return ok({ bar, baz }); + } else { + return badRequest('Wrong payload', ['body']); + } + }; + + router.post( + { + path: '/', + validate: { + body: bodyValidation, + }, + }, + (_, req, res) => { + return res.ok({ body: req.body }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .post(`${basePath}/foo/`) + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ bar: 'test', baz: 123 }); + }); + }); + + test('not inline handler - KibanaRequest', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + const handler = ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ) => { + const body = { + bar: req.body.bar.toUpperCase(), + baz: req.body.baz.toString(), + }; + + return res.ok({ body }); + }; + + router.post( + { + path: '/', + validate: { + body: ({ bar, baz } = {}, { ok, badRequest }) => { + if (typeof bar === 'string' && typeof baz === 'number') { + return ok({ bar, baz }); + } else { + return badRequest('Wrong payload', ['body']); + } + }, + }, + }, + handler + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .post(`${basePath}/foo/`) + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ bar: 'TEST', baz: '123' }); + }); + }); + + test('invalid body', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + router.post( + { + path: '/', + validate: { + body: schema.object({ + bar: schema.number(), + }), + }, + }, + (_, req, res) => { + return res.ok({ body: req.body }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .post(`${basePath}/foo/`) + .send({ bar: 'test' }) + .expect(400) + .then((res) => { + expect(res.body).toEqual({ + error: 'Bad Request', + statusCode: 400, + message: '[request body.bar]: expected value of type [number] but got [string]', + }); + }); + }); + + test('handles putting', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + router.put( + { + path: '/', + validate: { + body: schema.object({ + key: schema.string(), + }), + }, + }, + (_, req, res) => { + return res.ok({ body: req.body }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .put(`${basePath}/foo/`) + .send({ key: 'new value' }) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ key: 'new value' }); + }); + }); + + test('handles deleting', async () => { + const router = new Router(`${basePath}/foo`, logger, enhanceWithContext); + + router.delete( + { + path: '/{id}', + validate: { + params: schema.object({ + id: schema.number(), + }), + }, + }, + (_, req, res) => { + return res.ok({ body: { key: req.params.id } }); + } + ); + + const { registerRouter } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await proxySupertest + .delete(`${basePath}/foo/3`) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ key: 3 }); + }); + }); + + describe('with `basepath: /bar` and `rewriteBasePath: false`', () => { + let configWithBasePath: HttpConfig; + + beforeEach(async () => { + configWithBasePath = { + ...config, + basePath: '/bar', + rewriteBasePath: false, + } as HttpConfig; + + const router = new Router(`${basePath}/`, logger, enhanceWithContext); + router.get({ path: '/', validate: false }, (_, __, res) => res.ok({ body: 'value:/' })); + router.get({ path: '/foo', validate: false }, (_, __, res) => res.ok({ body: 'value:/foo' })); + + const { registerRouter } = await server.setup(configWithBasePath); + registerRouter(router); + + await server.start(); + }); + + test('/bar => 404', async () => { + await proxySupertest.get(`${basePath}/bar`).expect(404); + }); + + test('/bar/ => 404', async () => { + await proxySupertest.get(`${basePath}/bar/`).expect(404); + }); + + test('/bar/foo => 404', async () => { + await proxySupertest.get(`${basePath}/bar/foo`).expect(404); + }); + + test('/ => /', async () => { + await proxySupertest + .get(`${basePath}/`) + .expect(200) + .then((res) => { + expect(res.text).toBe('value:/'); + }); + }); + + test('/foo => /foo', async () => { + await proxySupertest + .get(`${basePath}/foo`) + .expect(200) + .then((res) => { + expect(res.text).toBe('value:/foo'); + }); + }); + }); + + test('with defined `redirectHttpFromPort`', async () => { + const router = new Router(`${basePath}/`, logger, enhanceWithContext); + router.get({ path: '/', validate: false }, (_, __, res) => res.ok({ body: 'value:/' })); + + const { registerRouter } = await server.setup(configWithSSL); + registerRouter(router); + + await server.start(); + }); + + test('allows attaching metadata to attach meta-data tag strings to a route', async () => { + const tags = ['my:tag']; + const { registerRouter } = await server.setup(config); + + const router = new Router(basePath, logger, enhanceWithContext); + router.get({ path: '/with-tags', validate: false, options: { tags } }, (_, req, res) => + res.ok({ body: { tags: req.route.options.tags } }) + ); + router.get({ path: '/without-tags', validate: false }, (_, req, res) => + res.ok({ body: { tags: req.route.options.tags } }) + ); + registerRouter(router); + + await server.start(); + await proxySupertest.get(`${basePath}/with-tags`).expect(200, { tags }); + + await proxySupertest.get(`${basePath}/without-tags`).expect(200, { tags: [] }); + }); + + describe('response headers', () => { + test('default headers', async () => { + const { registerRouter } = await server.setup(config); + + const router = new Router(basePath, logger, enhanceWithContext); + router.get({ path: '/', validate: false }, (_, req, res) => res.ok({ body: req.route })); + registerRouter(router); + + await server.start(); + const response = await proxySupertest.get(`${basePath}/`).expect(200); + + const restHeaders = omit(response.header, ['date', 'content-length']); + expect(restHeaders).toMatchInlineSnapshot(` + Object { + "accept-ranges": "bytes", + "cache-control": "private, no-cache, no-store, must-revalidate", + "connection": "close", + "content-type": "application/json; charset=utf-8", + } + `); + }); + }); + + test('exposes route details of incoming request to a route handler (POST + payload options)', async () => { + const { registerRouter } = await server.setup(config); + + const router = new Router(basePath, logger, enhanceWithContext); + router.post( + { + path: '/', + validate: { body: schema.object({ test: schema.number() }) }, + options: { body: { accepts: 'application/json' } }, + }, + (_, req, res) => res.ok({ body: req.route }) + ); + registerRouter(router); + + await server.start(); + await proxySupertest + .post(`${basePath}/`) + .send({ test: 1 }) + .expect(200, { + method: 'post', + path: `${basePath}/`, + options: { + authRequired: true, + xsrfRequired: true, + tags: [], + timeout: { + payload: 10000, + idleSocket: 1000, + }, + body: { + parse: true, // hapi populates the default + maxBytes: 1024, // hapi populates the default + accepts: ['application/json'], + output: 'data', + }, + }, + }); + }); + + test('should return a stream in the body', async () => { + const { registerRouter } = await server.setup(config); + + const router = new Router(basePath, logger, enhanceWithContext); + router.put( + { + path: '/', + validate: { body: schema.stream() }, + options: { body: { output: 'stream' } }, + }, + (_, req, res) => { + try { + expect(req.body).toBeInstanceOf(Readable); + return res.ok({ body: req.route.options.body }); + } catch (err) { + return res.internalError({ body: err.message }); + } + } + ); + registerRouter(router); + + await server.start(); + await proxySupertest.put(`${basePath}/`).send({ test: 1 }).expect(200, { + parse: true, + maxBytes: 1024, // hapi populates the default + output: 'stream', + }); + }); + + describe('timeout options', () => { + describe('payload timeout', () => { + test('POST routes set the payload timeout', async () => { + const { registerRouter } = await server.setup(config); + + const router = new Router(basePath, logger, enhanceWithContext); + router.post( + { + path: '/', + validate: false, + options: { + timeout: { + payload: 300000, + }, + }, + }, + (_, req, res) => { + try { + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); + } catch (err) { + return res.internalError({ body: err.message }); + } + } + ); + registerRouter(router); + await server.start(); + await proxySupertest + .post(`${basePath}/`) + .send({ test: 1 }) + .expect(200, { + timeout: { + payload: 300000, + idleSocket: 1000, // This is an extra option added by the proxy + }, + }); + }); + + test('DELETE routes set the payload timeout', async () => { + const { registerRouter } = await server.setup(config); + + const router = new Router(basePath, logger, enhanceWithContext); + router.delete( + { + path: '/', + validate: false, + options: { + timeout: { + payload: 300000, + }, + }, + }, + (context, req, res) => { + try { + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); + } catch (err) { + return res.internalError({ body: err.message }); + } + } + ); + registerRouter(router); + await server.start(); + await proxySupertest.delete(`${basePath}/`).expect(200, { + timeout: { + payload: 300000, + idleSocket: 1000, // This is an extra option added by the proxy + }, + }); + }); + + test('PUT routes set the payload timeout and automatically adjusts the idle socket timeout', async () => { + const { registerRouter } = await server.setup(config); + + const router = new Router(basePath, logger, enhanceWithContext); + router.put( + { + path: '/', + validate: false, + options: { + timeout: { + payload: 300000, + }, + }, + }, + (_, req, res) => { + try { + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); + } catch (err) { + return res.internalError({ body: err.message }); + } + } + ); + registerRouter(router); + await server.start(); + await proxySupertest.put(`${basePath}/`).expect(200, { + timeout: { + payload: 300000, + idleSocket: 1000, // This is an extra option added by the proxy + }, + }); + }); + + test('PATCH routes set the payload timeout and automatically adjusts the idle socket timeout', async () => { + const { registerRouter } = await server.setup(config); + + const router = new Router(basePath, logger, enhanceWithContext); + router.patch( + { + path: '/', + validate: false, + options: { + timeout: { + payload: 300000, + }, + }, + }, + (_, req, res) => { + try { + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); + } catch (err) { + return res.internalError({ body: err.message }); + } + } + ); + registerRouter(router); + await server.start(); + await proxySupertest.patch(`${basePath}/`).expect(200, { + timeout: { + payload: 300000, + idleSocket: 1000, // This is an extra option added by the proxy + }, + }); + }); + }); + + describe('idleSocket timeout', () => { + test('uses server socket timeout when not specified in the route', async () => { + const { registerRouter } = await server.setup({ + ...config, + socketTimeout: 11000, + }); + + const router = new Router(basePath, logger, enhanceWithContext); + router.get( + { + path: '/', + validate: { body: schema.maybe(schema.any()) }, + }, + (_, req, res) => { + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); + } + ); + registerRouter(router); + + await server.start(); + await proxySupertest + .get(`${basePath}/`) + .send() + .expect(200, { + timeout: { + idleSocket: 11000, + }, + }); + }); + + test('sets the socket timeout when specified in the route', async () => { + const { registerRouter } = await server.setup({ + ...config, + socketTimeout: 11000, + }); + + const router = new Router(basePath, logger, enhanceWithContext); + router.get( + { + path: '/', + validate: { body: schema.maybe(schema.any()) }, + options: { timeout: { idleSocket: 12000 } }, + }, + (context, req, res) => { + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); + } + ); + registerRouter(router); + + await server.start(); + await proxySupertest + .get(`${basePath}/`) + .send() + .expect(200, { + timeout: { + idleSocket: 12000, + }, + }); + }); + + test('idleSocket timeout can be smaller than the payload timeout', async () => { + const { registerRouter } = await server.setup(config); + + const router = new Router(basePath, logger, enhanceWithContext); + router.post( + { + path: `${basePath}/`, + validate: { body: schema.any() }, + options: { + timeout: { + payload: 1000, + idleSocket: 10, + }, + }, + }, + (_, req, res) => { + return res.ok({ body: { timeout: req.route.options.timeout } }); + } + ); + + registerRouter(router); + + await server.start(); + }); + }); + }); + + describe('shouldRedirect', () => { + let proxyServerWithoutShouldRedirect: BasePathProxyServer; + let proxyWithoutShouldRedirectSupertest: supertest.SuperTest; + + beforeEach(async () => { + // setup and start a proxy server which does not use "shouldRedirectFromOldBasePath" + const proxyConfig: HttpConfig = { ...config, port: 10004 }; + const devConfig = new DevConfig({ basePathProxyTarget: config.port }); + proxyServerWithoutShouldRedirect = new BasePathProxyServer(logger, proxyConfig, devConfig); + const options: Readonly = { + shouldRedirectFromOldBasePath: () => false, // Return false to not redirect + delayUntil: () => EMPTY, + }; + await proxyServerWithoutShouldRedirect.start(options); + proxyWithoutShouldRedirectSupertest = supertest(`http://127.0.0.1:${proxyConfig.port}`); + }); + + afterEach(async () => { + await proxyServerWithoutShouldRedirect.stop(); + }); + + test('it will do a redirect if it detects what looks like a stale or previously used base path', async () => { + const fakeBasePath = basePath !== 'abc' ? 'abc' : 'efg'; + const res = await proxySupertest.get(`/${fakeBasePath}`).expect(302); + const location = res.header.location; + expect(location).toEqual(`${basePath}/`); + }); + + test('it will NOT do a redirect if it detects what looks like a stale or previously used base path if we intentionally turn it off', async () => { + const fakeBasePath = basePath !== 'abc' ? 'abc' : 'efg'; + await proxyWithoutShouldRedirectSupertest.get(`/${fakeBasePath}`).expect(404); + }); + + test('it will NOT redirect if it detects a larger path than 3 characters', async () => { + await proxySupertest.get('/abcde').expect(404); + }); + + test('it will NOT redirect if it is not a GET verb', async () => { + const fakeBasePath = basePath !== 'abc' ? 'abc' : 'efg'; + await proxySupertest.put(`/${fakeBasePath}`).expect(404); + }); + }); + + describe('constructor option for sending in a custom basePath', () => { + let proxyServerWithFooBasePath: BasePathProxyServer; + let proxyWithFooBasePath: supertest.SuperTest; + + beforeEach(async () => { + // setup and start a proxy server which uses a basePath of "foo" + const proxyConfig: HttpConfig = { ...config, port: 10004, basePath: '/foo' }; // <-- "foo" here in basePath + const devConfig = new DevConfig({ basePathProxyTarget: config.port }); + proxyServerWithFooBasePath = new BasePathProxyServer(logger, proxyConfig, devConfig); + const options: Readonly = { + shouldRedirectFromOldBasePath: () => true, + delayUntil: () => EMPTY, + }; + await proxyServerWithFooBasePath.start(options); + proxyWithFooBasePath = supertest(`http://127.0.0.1:${proxyConfig.port}`); + }); + + afterEach(async () => { + await proxyServerWithFooBasePath.stop(); + }); + + test('it will do a redirect to foo which is our passed in value for the configuration', async () => { + const res = await proxyWithFooBasePath.get('/bar').expect(302); + const location = res.header.location; + expect(location).toEqual('/foo/'); + }); + }); +}); diff --git a/src/core/server/http/base_path_proxy_server.ts b/src/core/server/http/base_path_proxy_server.ts index d461abe54ccbd..dfcd0757c2d1e 100644 --- a/src/core/server/http/base_path_proxy_server.ts +++ b/src/core/server/http/base_path_proxy_server.ts @@ -143,12 +143,25 @@ export class BasePathProxyServer { handler: { proxy: { agent: this.httpsAgent, - host: this.server.info.host, passThrough: true, - port: this.devConfig.basePathProxyTargetPort, - // typings mismatch. h2o2 doesn't support "socket" - protocol: this.server.info.protocol as HapiProxy.ProxyHandlerOptions['protocol'], xforward: true, + mapUri: async (request) => { + return { + // Passing in this header to merge it is a workaround until this is fixed: + // https://github.com/hapijs/h2o2/issues/124 + headers: + request.headers['content-length'] != null + ? { 'content-length': request.headers['content-length'] } + : undefined, + uri: Url.format({ + hostname: request.server.info.host, + port: this.devConfig.basePathProxyTargetPort, + protocol: request.server.info.protocol, + pathname: request.path, + query: request.query, + }), + }; + }, }, }, method: '*', diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index cbb60480c4cf1..70c346a5333cc 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -888,52 +888,48 @@ describe('conditional compression', () => { expect(response.header).not.toHaveProperty('content-encoding'); }); }); +}); - describe('response headers', () => { - it('allows to configure "keep-alive" header', async () => { - const { registerRouter, server: innerServer } = await server.setup({ - ...config, - keepaliveTimeout: 100_000, - }); +describe('response headers', () => { + test('allows to configure "keep-alive" header', async () => { + const { registerRouter, server: innerServer } = await server.setup({ + ...config, + keepaliveTimeout: 100_000, + }); - const router = new Router('', logger, enhanceWithContext); - router.get({ path: '/', validate: false }, (context, req, res) => - res.ok({ body: req.route }) - ); - registerRouter(router); + const router = new Router('', logger, enhanceWithContext); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: req.route })); + registerRouter(router); - await server.start(); - const response = await supertest(innerServer.listener) - .get('/') - .set('Connection', 'keep-alive') - .expect(200); + await server.start(); + const response = await supertest(innerServer.listener) + .get('/') + .set('Connection', 'keep-alive') + .expect(200); - expect(response.header.connection).toBe('keep-alive'); - expect(response.header['keep-alive']).toBe('timeout=100'); - }); + expect(response.header.connection).toBe('keep-alive'); + expect(response.header['keep-alive']).toBe('timeout=100'); + }); - it('default headers', async () => { - const { registerRouter, server: innerServer } = await server.setup(config); + test('default headers', async () => { + const { registerRouter, server: innerServer } = await server.setup(config); - const router = new Router('', logger, enhanceWithContext); - router.get({ path: '/', validate: false }, (context, req, res) => - res.ok({ body: req.route }) - ); - registerRouter(router); + const router = new Router('', logger, enhanceWithContext); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: req.route })); + registerRouter(router); - await server.start(); - const response = await supertest(innerServer.listener).get('/').expect(200); - - const restHeaders = omit(response.header, ['date', 'content-length']); - expect(restHeaders).toMatchInlineSnapshot(` - Object { - "accept-ranges": "bytes", - "cache-control": "private, no-cache, no-store, must-revalidate", - "connection": "close", - "content-type": "application/json; charset=utf-8", - } - `); - }); + await server.start(); + const response = await supertest(innerServer.listener).get('/').expect(200); + + const restHeaders = omit(response.header, ['date', 'content-length']); + expect(restHeaders).toMatchInlineSnapshot(` + Object { + "accept-ranges": "bytes", + "cache-control": "private, no-cache, no-store, must-revalidate", + "connection": "close", + "content-type": "application/json; charset=utf-8", + } + `); }); }); @@ -1270,31 +1266,31 @@ describe('timeout options', () => { }, }); }); - }); - test(`idleSocket timeout can be smaller than the payload timeout`, async () => { - const { registerRouter } = await server.setup(config); + test('idleSocket timeout can be smaller than the payload timeout', async () => { + const { registerRouter } = await server.setup(config); - const router = new Router('', logger, enhanceWithContext); - router.post( - { - path: '/', - validate: { body: schema.any() }, - options: { - timeout: { - payload: 1000, - idleSocket: 10, + const router = new Router('', logger, enhanceWithContext); + router.post( + { + path: '/', + validate: { body: schema.any() }, + options: { + timeout: { + payload: 1000, + idleSocket: 10, + }, }, }, - }, - (context, req, res) => { - return res.ok({ body: { timeout: req.route.options.timeout } }); - } - ); + (context, req, res) => { + return res.ok({ body: { timeout: req.route.options.timeout } }); + } + ); - registerRouter(router); + registerRouter(router); - await server.start(); + await server.start(); + }); }); }); @@ -1329,13 +1325,14 @@ test('should return a stream in the body', async () => { describe('setup contract', () => { describe('#createSessionStorage', () => { - it('creates session storage factory', async () => { + test('creates session storage factory', async () => { const { createCookieSessionStorageFactory } = await server.setup(config); const sessionStorageFactory = await createCookieSessionStorageFactory(cookieOptions); expect(sessionStorageFactory.asScoped).toBeDefined(); }); - it('creates session storage factory only once', async () => { + + test('creates session storage factory only once', async () => { const { createCookieSessionStorageFactory } = await server.setup(config); const create = async () => await createCookieSessionStorageFactory(cookieOptions); @@ -1343,7 +1340,7 @@ describe('setup contract', () => { expect(create()).rejects.toThrowError('A cookieSessionStorageFactory was already created'); }); - it('does not throw if called after stop', async () => { + test('does not throw if called after stop', async () => { const { createCookieSessionStorageFactory } = await server.setup(config); await server.stop(); expect(() => { @@ -1353,7 +1350,7 @@ describe('setup contract', () => { }); describe('#getServerInfo', () => { - it('returns correct information', async () => { + test('returns correct information', async () => { let { getServerInfo } = await server.setup(config); expect(getServerInfo()).toEqual({ @@ -1378,7 +1375,7 @@ describe('setup contract', () => { }); }); - it('returns correct protocol when ssl is enabled', async () => { + test('returns correct protocol when ssl is enabled', async () => { const { getServerInfo } = await server.setup(configWithSSL); expect(getServerInfo().protocol).toEqual('https'); @@ -1386,7 +1383,7 @@ describe('setup contract', () => { }); describe('#registerStaticDir', () => { - it('does not throw if called after stop', async () => { + test('does not throw if called after stop', async () => { const { registerStaticDir } = await server.setup(config); await server.stop(); expect(() => { diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index abda7cf82b121..1fd2e7352d8f7 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -908,8 +908,7 @@ describe('migration actions', () => { }); }); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/87160 - describe.skip('createIndex', () => { + describe('createIndex', () => { afterAll(async () => { await client.indices.delete({ index: 'yellow_then_green_index' }); }); @@ -936,6 +935,7 @@ describe('migration actions', () => { setTimeout(() => { client.indices.putSettings({ + index: 'yellow_then_green_index', body: { index: { number_of_replicas: 0, diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts index 942021fd1918d..723f4c02db3ce 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts @@ -127,7 +127,8 @@ describe('migration v2', () => { await new Promise((resolve) => setTimeout(resolve, 10000)); }; - describe('migrating from 7.3.0-xpack version', () => { + // FLAKY: https://github.com/elastic/kibana/issues/87968 + describe.skip('migrating from 7.3.0-xpack version', () => { const migratedIndex = `.kibana_${kibanaVersion}_001`; beforeAll(async () => { diff --git a/src/dev/code_coverage/shell_scripts/copy_mocha_reports.sh b/src/dev/code_coverage/shell_scripts/copy_mocha_reports.sh deleted file mode 100644 index 579276aac990f..0000000000000 --- a/src/dev/code_coverage/shell_scripts/copy_mocha_reports.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -EXTRACT_START_DIR=tmp/extracted_coverage -EXTRACT_END_DIR=target/kibana-coverage -COMBINED_EXRACT_DIR=/${EXTRACT_START_DIR}/${EXTRACT_END_DIR} - - -echo "### Copy mocha reports" -mkdir -p $EXTRACT_END_DIR/mocha-combined -cp -r $COMBINED_EXRACT_DIR/mocha/. $EXTRACT_END_DIR/mocha-combined/ diff --git a/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh index 62b81929ae79b..caa1f1a761367 100644 --- a/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh +++ b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh @@ -40,11 +40,5 @@ for x in jest functional; do node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH done -# Need to override COVERAGE_INGESTION_KIBANA_ROOT since mocha json file has original intake worker path -COVERAGE_SUMMARY_FILE=target/kibana-coverage/mocha-combined/coverage-summary.json -export COVERAGE_INGESTION_KIBANA_ROOT=/dev/shm/workspace/kibana - -node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH - echo "### Ingesting Code Coverage - Complete" echo "" diff --git a/src/dev/run_find_plugins_with_circular_deps.ts b/src/dev/run_find_plugins_with_circular_deps.ts index 75faf3d8c17a7..fc599ad738dfa 100644 --- a/src/dev/run_find_plugins_with_circular_deps.ts +++ b/src/dev/run_find_plugins_with_circular_deps.ts @@ -31,7 +31,6 @@ interface Options { type CircularDepList = Set; const allowedList: CircularDepList = new Set([ - 'src/plugins/visualizations -> src/plugins/visualize', 'x-pack/plugins/actions -> x-pack/plugins/case', 'x-pack/plugins/case -> x-pack/plugins/security_solution', 'x-pack/plugins/apm -> x-pack/plugins/infra', diff --git a/src/plugins/advanced_settings/tsconfig.json b/src/plugins/advanced_settings/tsconfig.json new file mode 100644 index 0000000000000..4d62e410326b6 --- /dev/null +++ b/src/plugins/advanced_settings/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + "server/**/*" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../management/tsconfig.json" }, + { "path": "../home/tsconfig.json" }, + { "path": "../usage_collection/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + ] +} diff --git a/src/plugins/charts/tsconfig.json b/src/plugins/charts/tsconfig.json new file mode 100644 index 0000000000000..a4f65d5937204 --- /dev/null +++ b/src/plugins/charts/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["common/**/*", "public/**/*", "server/**/*"], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../expressions/tsconfig.json" }, + { "path": "../embeddable/tsconfig.json" } + ] +} diff --git a/src/plugins/dashboard/.storybook/storyshots.test.tsx b/src/plugins/dashboard/.storybook/storyshots.test.tsx index af8e71c77231a..3b4d5df350bf2 100644 --- a/src/plugins/dashboard/.storybook/storyshots.test.tsx +++ b/src/plugins/dashboard/.storybook/storyshots.test.tsx @@ -12,6 +12,7 @@ import 'moment-timezone'; import ReactDOM from 'react-dom'; import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots'; +// @ts-ignore import styleSheetSerializer from 'jest-styled-components/src/styleSheetSerializer'; import { addSerializer } from 'jest-specific-snapshot'; @@ -52,6 +53,7 @@ jest.mock('@elastic/eui/lib/components/overlay_mask/overlay_mask', () => { }; }); +// @ts-ignore import { EuiObserver } from '@elastic/eui/test-env/components/observer/observer'; jest.mock('@elastic/eui/test-env/components/observer/observer'); EuiObserver.mockImplementation(() => 'EuiObserver'); diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index b5451203e2365..e074d529917d2 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -6,14 +6,21 @@ "embeddable", "inspector", "kibanaLegacy", - "urlForwarding", "navigation", - "uiActions", "savedObjects", - "share" + "share", + "uiActions", + "urlForwarding" ], - "optionalPlugins": ["home", "usageCollection", "savedObjectsTaggingOss"], + "optionalPlugins": [ + "home", + "savedObjectsTaggingOss", + "usageCollection"], "server": true, "ui": true, - "requiredBundles": ["kibanaUtils", "kibanaReact", "home"] + "requiredBundles": [ + "home", + "kibanaReact", + "kibanaUtils" + ] } diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index f33383427342b..8f4bc8bc6ef1a 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -173,10 +173,14 @@ export function DashboardApp({ ).subscribe(() => refreshDashboardContainer()) ); subscriptions.add( - data.search.session.onRefresh$.subscribe(() => { + merge( + data.search.session.onRefresh$, + data.query.timefilter.timefilter.getAutoRefreshFetch$() + ).subscribe(() => { setLastReloadTime(() => new Date().getTime()); }) ); + dashboardStateManager.registerChangeListener(() => { // we aren't checking dirty state because there are changes the container needs to know about // that won't make the dashboard "dirty" - like a view mode change. diff --git a/src/plugins/dashboard/public/application/lib/session_restoration.ts b/src/plugins/dashboard/public/application/lib/session_restoration.ts index 5f05fa122e161..890b81b5418be 100644 --- a/src/plugins/dashboard/public/application/lib/session_restoration.ts +++ b/src/plugins/dashboard/public/application/lib/session_restoration.ts @@ -43,17 +43,22 @@ function getUrlGeneratorState({ data, getAppState, getDashboardId, - forceAbsoluteTime, // TODO: not implemented + forceAbsoluteTime, }: { data: DataPublicPluginStart; getAppState: () => DashboardAppState; getDashboardId: () => string; + /** + * Can force time range from time filter to convert from relative to absolute time range + */ forceAbsoluteTime: boolean; }): DashboardUrlGeneratorState { const appState = getAppState(); return { dashboardId: getDashboardId(), - timeRange: data.query.timefilter.timefilter.getTime(), + timeRange: forceAbsoluteTime + ? data.query.timefilter.timefilter.getAbsoluteTime() + : data.query.timefilter.timefilter.getTime(), filters: data.query.filterManager.getFilters(), query: data.query.queryString.formatQuery(appState.query), savedQuery: appState.savedQuery, diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json new file mode 100644 index 0000000000000..c70f2bad7e701 --- /dev/null +++ b/src/plugins/dashboard/tsconfig.json @@ -0,0 +1,34 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "*.ts", + ".storybook/**/*", + "common/**/*", + "public/**/*", + "server/**/*", + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../inspector/tsconfig.json" }, + { "path": "../kibana_legacy/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../share/tsconfig.json" }, + { "path": "../url_forwarding/tsconfig.json" }, + { "path": "../usage_collection/tsconfig.json" }, + { "path": "../data/tsconfig.json"}, + { "path": "../embeddable/tsconfig.json" }, + { "path": "../home/tsconfig.json" }, + { "path": "../navigation/tsconfig.json" }, + { "path": "../saved_objects_tagging_oss/tsconfig.json" }, + { "path": "../saved_objects/tsconfig.json" }, + { "path": "../ui_actions/tsconfig.json" }, + ] +} diff --git a/src/plugins/data/common/query/timefilter/get_time.test.ts b/src/plugins/data/common/query/timefilter/get_time.test.ts index 4dba157a6f554..5b77153136761 100644 --- a/src/plugins/data/common/query/timefilter/get_time.test.ts +++ b/src/plugins/data/common/query/timefilter/get_time.test.ts @@ -19,7 +19,7 @@ import moment from 'moment'; import sinon from 'sinon'; -import { getTime } from './get_time'; +import { getTime, getAbsoluteTimeRange } from './get_time'; describe('get_time', () => { describe('getTime', () => { @@ -90,4 +90,19 @@ describe('get_time', () => { clock.restore(); }); }); + describe('getAbsoluteTimeRange', () => { + test('should forward absolute timerange as is', () => { + const from = '2000-01-01T00:00:00.000Z'; + const to = '2000-02-01T00:00:00.000Z'; + expect(getAbsoluteTimeRange({ from, to })).toEqual({ from, to }); + }); + + test('should convert relative to absolute', () => { + const clock = sinon.useFakeTimers(moment.utc([2000, 1, 0, 0, 0, 0, 0]).valueOf()); + const from = '2000-01-01T00:00:00.000Z'; + const to = moment.utc(clock.now).toISOString(); + expect(getAbsoluteTimeRange({ from, to: 'now' })).toEqual({ from, to }); + clock.restore(); + }); + }); }); diff --git a/src/plugins/data/common/query/timefilter/get_time.ts b/src/plugins/data/common/query/timefilter/get_time.ts index 6e4eda95accc7..bb7b5760240b7 100644 --- a/src/plugins/data/common/query/timefilter/get_time.ts +++ b/src/plugins/data/common/query/timefilter/get_time.ts @@ -34,6 +34,17 @@ export function calculateBounds( }; } +export function getAbsoluteTimeRange( + timeRange: TimeRange, + { forceNow }: { forceNow?: Date } = {} +): TimeRange { + const { min, max } = calculateBounds(timeRange, { forceNow }); + return { + from: min ? min.toISOString() : timeRange.from, + to: max ? max.toISOString() : timeRange.to, + }; +} + export function getTime( indexPattern: IIndexPattern | undefined, timeRange: TimeRange, diff --git a/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts b/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts index 2274fcfd6b8d5..e9bf03af192ba 100644 --- a/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts +++ b/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts @@ -62,6 +62,7 @@ export interface EsaggsStartDependencies { deserializeFieldFormat: FormatFactory; indexPatterns: IndexPatternsContract; searchSource: ISearchStartSearchSource; + getNow?: () => Date; } /** @internal */ @@ -118,7 +119,8 @@ export async function handleEsaggsRequest( args: Arguments, params: RequestHandlerParams ): Promise { - const resolvedTimeRange = input?.timeRange && calculateBounds(input.timeRange); + const resolvedTimeRange = + input?.timeRange && calculateBounds(input.timeRange, { forceNow: params.getNow?.() }); const response = await handleRequest(params); diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts index b773aad67c3f8..29a841b18706c 100644 --- a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts +++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts @@ -51,6 +51,7 @@ export interface RequestHandlerParams { searchSourceService: ISearchStartSearchSource; timeFields?: string[]; timeRange?: TimeRange; + getNow?: () => Date; } export const handleRequest = async ({ @@ -67,7 +68,9 @@ export const handleRequest = async ({ searchSourceService, timeFields, timeRange, + getNow, }: RequestHandlerParams) => { + const forceNow = getNow?.(); const searchSource = await searchSourceService.create(); searchSource.setField('index', indexPattern); @@ -115,7 +118,7 @@ export const handleRequest = async ({ if (timeRange && allTimeFields.length > 0) { timeFilterSearchSource.setField('filter', () => { return allTimeFields - .map((fieldName) => getTime(indexPattern, timeRange, { fieldName })) + .map((fieldName) => getTime(indexPattern, timeRange, { fieldName, forceNow })) .filter(isRangeFilter); }); } @@ -183,7 +186,7 @@ export const handleRequest = async ({ } } - const parsedTimeRange = timeRange ? calculateBounds(timeRange) : null; + const parsedTimeRange = timeRange ? calculateBounds(timeRange, { forceNow }) : null; const tabifyParams = { metricsAtAllLevels, partialRows, diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 67c1ff7e09dd7..c34139caa553e 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -22,6 +22,7 @@ import { fieldFormatsServiceMock } from './field_formats/mocks'; import { searchServiceMock } from './search/mocks'; import { queryServiceMock } from './query/mocks'; import { AutocompleteStart, AutocompleteSetup } from './autocomplete'; +import { createNowProviderMock } from './now_provider/mocks'; export type Setup = jest.Mocked>; export type Start = jest.Mocked>; @@ -76,6 +77,7 @@ const createStartContract = (): Start => { get: jest.fn().mockReturnValue(Promise.resolve({})), clearCache: jest.fn(), } as unknown) as IndexPatternsContract, + nowProvider: createNowProviderMock(), }; }; diff --git a/src/plugins/vis_type_timeseries/common/model_options.js b/src/plugins/data/public/now_provider/index.ts similarity index 79% rename from src/plugins/vis_type_timeseries/common/model_options.js rename to src/plugins/data/public/now_provider/index.ts index 22fe7a0abc842..662ab87dd534b 100644 --- a/src/plugins/vis_type_timeseries/common/model_options.js +++ b/src/plugins/data/public/now_provider/index.ts @@ -17,10 +17,8 @@ * under the License. */ -export const MODEL_TYPES = { - UNWEIGHTED: 'simple', - WEIGHTED_EXPONENTIAL: 'ewma', - WEIGHTED_EXPONENTIAL_DOUBLE: 'holt', - WEIGHTED_EXPONENTIAL_TRIPLE: 'holt_winters', - WEIGHTED_LINEAR: 'linear', -}; +export { + NowProvider, + NowProviderInternalContract, + NowProviderPublicContract, +} from './now_provider'; diff --git a/src/plugins/data/public/now_provider/lib/get_force_now_from_url.test.ts b/src/plugins/data/public/now_provider/lib/get_force_now_from_url.test.ts new file mode 100644 index 0000000000000..4e3233474b040 --- /dev/null +++ b/src/plugins/data/public/now_provider/lib/get_force_now_from_url.test.ts @@ -0,0 +1,50 @@ +/* + * 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 { getForceNowFromUrl } from './get_force_now_from_url'; +const originalLocation = window.location; +afterAll(() => { + window.location = originalLocation; +}); + +function mockLocation(url: string) { + // @ts-ignore + delete window.location; + // @ts-ignore + window.location = new URL(url); +} + +test('should get force now from URL', () => { + const dateString = '1999-01-01T00:00:00.000Z'; + mockLocation(`https://elastic.co/?forceNow=${dateString}`); + + expect(getForceNowFromUrl()).toEqual(new Date(dateString)); +}); + +test('should throw if force now is invalid', () => { + const dateString = 'invalid-date'; + mockLocation(`https://elastic.co/?forceNow=${dateString}`); + + expect(() => getForceNowFromUrl()).toThrowError(); +}); + +test('should return undefined if no forceNow', () => { + mockLocation(`https://elastic.co/`); + expect(getForceNowFromUrl()).toBe(undefined); +}); diff --git a/src/plugins/data/public/query/timefilter/lib/parse_querystring.ts b/src/plugins/data/public/now_provider/lib/get_force_now_from_url.ts similarity index 74% rename from src/plugins/data/public/query/timefilter/lib/parse_querystring.ts rename to src/plugins/data/public/now_provider/lib/get_force_now_from_url.ts index 5982bfd0bd276..906eec1ab143e 100644 --- a/src/plugins/data/public/query/timefilter/lib/parse_querystring.ts +++ b/src/plugins/data/public/now_provider/lib/get_force_now_from_url.ts @@ -16,10 +16,25 @@ * specific language governing permissions and limitations * under the License. */ + import { parse } from 'query-string'; /** @internal */ -export function parseQueryString() { +export function getForceNowFromUrl(): Date | undefined { + const forceNow = parseQueryString().forceNow as string; + if (!forceNow) { + return; + } + + const ts = Date.parse(forceNow); + if (isNaN(ts)) { + throw new Error(`forceNow query parameter, ${forceNow}, can't be parsed by Date.parse`); + } + return new Date(ts); +} + +/** @internal */ +function parseQueryString() { // window.location.search is an empty string // get search from href const hrefSplit = window.location.href.split('?'); diff --git a/src/plugins/data/public/now_provider/lib/index.ts b/src/plugins/data/public/now_provider/lib/index.ts new file mode 100644 index 0000000000000..990db5ae3c77c --- /dev/null +++ b/src/plugins/data/public/now_provider/lib/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { getForceNowFromUrl } from './get_force_now_from_url'; diff --git a/src/plugins/data/public/now_provider/mocks.ts b/src/plugins/data/public/now_provider/mocks.ts new file mode 100644 index 0000000000000..1021d2b48cfe5 --- /dev/null +++ b/src/plugins/data/public/now_provider/mocks.ts @@ -0,0 +1,28 @@ +/* + * 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 { NowProviderInternalContract } from './now_provider'; + +export const createNowProviderMock = (): jest.Mocked => { + return { + get: jest.fn(() => new Date()), + set: jest.fn(), + reset: jest.fn(), + }; +}; diff --git a/src/plugins/data/public/now_provider/now_provider.test.ts b/src/plugins/data/public/now_provider/now_provider.test.ts new file mode 100644 index 0000000000000..e557065ff2b67 --- /dev/null +++ b/src/plugins/data/public/now_provider/now_provider.test.ts @@ -0,0 +1,55 @@ +/* + * 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 { NowProvider, NowProviderInternalContract } from './now_provider'; + +let mockDateFromUrl: undefined | Date; +let nowProvider: NowProviderInternalContract; + +jest.mock('./lib', () => ({ + // @ts-ignore + ...jest.requireActual('./lib'), + getForceNowFromUrl: () => mockDateFromUrl, +})); + +beforeEach(() => { + nowProvider = new NowProvider(); +}); +afterEach(() => { + mockDateFromUrl = undefined; +}); + +test('should return Date.now() by default', async () => { + const now = Date.now(); + await new Promise((r) => setTimeout(r, 10)); + expect(nowProvider.get().getTime()).toBeGreaterThan(now); +}); + +test('should forceNow from URL', async () => { + mockDateFromUrl = new Date('1999-01-01T00:00:00.000Z'); + nowProvider = new NowProvider(); + expect(nowProvider.get()).toEqual(mockDateFromUrl); +}); + +test('should forceNow from URL if custom now is set', async () => { + mockDateFromUrl = new Date('1999-01-01T00:00:00.000Z'); + nowProvider = new NowProvider(); + nowProvider.set(new Date()); + expect(nowProvider.get()).toEqual(mockDateFromUrl); +}); diff --git a/src/plugins/data/public/now_provider/now_provider.ts b/src/plugins/data/public/now_provider/now_provider.ts new file mode 100644 index 0000000000000..a55fa691ef1da --- /dev/null +++ b/src/plugins/data/public/now_provider/now_provider.ts @@ -0,0 +1,50 @@ +/* + * 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 { PublicMethodsOf } from '@kbn/utility-types'; +import { getForceNowFromUrl } from './lib'; + +export type NowProviderInternalContract = PublicMethodsOf; +export type NowProviderPublicContract = Pick; + +/** + * Used to synchronize time between parallel searches with relative time range that rely on `now`. + */ +export class NowProvider { + // TODO: service shouldn't access params in the URL + // instead it should be handled by apps + private readonly nowFromUrl = getForceNowFromUrl(); + private now?: Date; + + constructor() {} + + get(): Date { + if (this.nowFromUrl) return this.nowFromUrl; // now forced from URL always takes precedence + if (this.now) return this.now; + return new Date(); + } + + set(now: Date) { + this.now = now; + } + + reset() { + this.now = undefined; + } +} diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 43abe84950fdb..aabf3136e6159 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -61,6 +61,7 @@ import { SavedObjectsClientPublicToCommon } from './index_patterns'; import { getIndexPatternLoad } from './index_patterns/expressions'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { getTableViewDescription } from './utils/table_inspector_view'; +import { NowProvider, NowProviderInternalContract } from './now_provider'; export class DataPublicPlugin implements @@ -76,6 +77,7 @@ export class DataPublicPlugin private readonly queryService: QueryService; private readonly storage: IStorageWrapper; private usageCollection: UsageCollectionSetup | undefined; + private readonly nowProvider: NowProviderInternalContract; constructor(initializerContext: PluginInitializerContext) { this.searchService = new SearchService(initializerContext); @@ -83,6 +85,7 @@ export class DataPublicPlugin this.fieldFormatsService = new FieldFormatsService(); this.autocomplete = new AutocompleteService(initializerContext); this.storage = new Storage(window.localStorage); + this.nowProvider = new NowProvider(); } public setup( @@ -95,9 +98,17 @@ export class DataPublicPlugin this.usageCollection = usageCollection; + const searchService = this.searchService.setup(core, { + bfetch, + usageCollection, + expressions, + nowProvider: this.nowProvider, + }); + const queryService = this.queryService.setup({ uiSettings: core.uiSettings, storage: this.storage, + nowProvider: this.nowProvider, }); uiActions.registerTrigger(applyFilterTrigger); @@ -120,12 +131,6 @@ export class DataPublicPlugin })) ); - const searchService = this.searchService.setup(core, { - bfetch, - usageCollection, - expressions, - }); - inspector.registerView( getTableViewDescription(() => ({ uiActions: startServices().plugins.uiActions, @@ -195,6 +200,7 @@ export class DataPublicPlugin indexPatterns, query, search, + nowProvider: this.nowProvider, }; const SearchBar = createSearchBar({ diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 27a40b4e5ffcb..935cb945678de 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -580,6 +580,10 @@ export interface DataPublicPluginStart { autocomplete: AutocompleteStart; fieldFormats: FieldFormatsStart; indexPatterns: IndexPatternsContract; + // Warning: (ae-forgotten-export) The symbol "NowProviderPublicContract" needs to be exported by the entry point index.d.ts + // + // (undocumented) + nowProvider: NowProviderPublicContract; query: QueryStart; search: ISearchStart; ui: DataPublicPluginStartUi; @@ -2620,7 +2624,7 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:433:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:436:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/search/session/session_service.ts:50:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/search/session/session_service.ts:51:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts index fe7fdcbb1d113..e5f681549b06e 100644 --- a/src/plugins/data/public/query/query_service.ts +++ b/src/plugins/data/public/query/query_service.ts @@ -28,6 +28,7 @@ import { createQueryStateObservable } from './state_sync/create_global_query_obs import { QueryStringManager, QueryStringContract } from './query_string'; import { buildEsQuery, getEsQueryConfig } from '../../common'; import { getUiSettings } from '../services'; +import { NowProviderInternalContract } from '../now_provider'; import { IndexPattern } from '..'; /** @@ -38,6 +39,7 @@ import { IndexPattern } from '..'; interface QueryServiceSetupDependencies { storage: IStorageWrapper; uiSettings: IUiSettingsClient; + nowProvider: NowProviderInternalContract; } interface QueryServiceStartDependencies { @@ -53,10 +55,10 @@ export class QueryService { state$!: ReturnType; - public setup({ storage, uiSettings }: QueryServiceSetupDependencies) { + public setup({ storage, uiSettings, nowProvider }: QueryServiceSetupDependencies) { this.filterManager = new FilterManager(uiSettings); - const timefilterService = new TimefilterService(); + const timefilterService = new TimefilterService(nowProvider); this.timefilter = timefilterService.setup({ uiSettings, storage, diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts index c970dd521c142..3ad1f3daa992b 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts @@ -28,6 +28,7 @@ import { StubBrowserStorage } from '@kbn/test/jest'; import { connectToQueryState } from './connect_to_query_state'; import { TimefilterContract } from '../timefilter'; import { QueryState } from './types'; +import { createNowProviderMock } from '../../now_provider/mocks'; const connectToQueryGlobalState = (query: QueryStart, state: BaseStateContainer) => connectToQueryState(query, state, { @@ -79,6 +80,7 @@ describe('connect_to_global_state', () => { queryService.setup({ uiSettings: setupMock.uiSettings, storage: new Storage(new StubBrowserStorage()), + nowProvider: createNowProviderMock(), }); queryServiceStart = queryService.start({ uiSettings: setupMock.uiSettings, @@ -312,6 +314,7 @@ describe('connect_to_app_state', () => { queryService.setup({ uiSettings: setupMock.uiSettings, storage: new Storage(new StubBrowserStorage()), + nowProvider: createNowProviderMock(), }); queryServiceStart = queryService.start({ uiSettings: setupMock.uiSettings, @@ -490,6 +493,7 @@ describe('filters with different state', () => { queryService.setup({ uiSettings: setupMock.uiSettings, storage: new Storage(new StubBrowserStorage()), + nowProvider: createNowProviderMock(), }); queryServiceStart = queryService.start({ uiSettings: setupMock.uiSettings, diff --git a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts index 47af09bfc7c0e..129a145ee0693 100644 --- a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts +++ b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts @@ -33,6 +33,7 @@ import { StubBrowserStorage } from '@kbn/test/jest'; import { TimefilterContract } from '../timefilter'; import { syncQueryStateWithUrl } from './sync_state_with_url'; import { QueryState } from './types'; +import { createNowProviderMock } from '../../now_provider/mocks'; const setupMock = coreMock.createSetup(); const startMock = coreMock.createStart(); @@ -73,6 +74,7 @@ describe('sync_query_state_with_url', () => { queryService.setup({ uiSettings: setupMock.uiSettings, storage: new Storage(new StubBrowserStorage()), + nowProvider: createNowProviderMock(), }); queryServiceStart = queryService.start({ uiSettings: startMock.uiSettings, diff --git a/src/plugins/data/public/query/timefilter/timefilter.test.ts b/src/plugins/data/public/query/timefilter/timefilter.test.ts index 6c1a4eff786f6..d8dfed002ea80 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.test.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.test.ts @@ -19,21 +19,12 @@ jest.useFakeTimers(); -jest.mock('./lib/parse_querystring', () => ({ - parseQueryString: () => { - return { - // Can not access local variable from within a mock - // @ts-ignore - forceNow: global.nowTime, - }; - }, -})); - import sinon from 'sinon'; import moment from 'moment'; import { Timefilter } from './timefilter'; import { Subscription } from 'rxjs'; import { TimeRange, RefreshInterval } from '../../../common'; +import { createNowProviderMock } from '../../now_provider/mocks'; import { timefilterServiceMock } from './timefilter_service.mock'; const timefilterSetupMock = timefilterServiceMock.createSetupContract(); @@ -42,16 +33,16 @@ const timefilterConfig = { timeDefaults: { from: 'now-15m', to: 'now' }, refreshIntervalDefaults: { pause: false, value: 0 }, }; -const timefilter = new Timefilter(timefilterConfig, timefilterSetupMock.history); + +const nowProviderMock = createNowProviderMock(); +const timefilter = new Timefilter(timefilterConfig, timefilterSetupMock.history, nowProviderMock); function stubNowTime(nowTime: any) { - // @ts-ignore - global.nowTime = nowTime; + nowProviderMock.get.mockImplementation(() => (nowTime ? new Date(nowTime) : new Date())); } function clearNowTimeStub() { - // @ts-ignore - delete global.nowTime; + nowProviderMock.get.mockReset(); } test('isTimeTouched is initially set to false', () => { diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts index 7278ceaaddcce..3ee2a3962e8ff 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.ts @@ -22,10 +22,11 @@ import { Subject, BehaviorSubject } from 'rxjs'; import moment from 'moment'; import { PublicMethodsOf } from '@kbn/utility-types'; import { areRefreshIntervalsDifferent, areTimeRangesDifferent } from './lib/diff_time_picker_vals'; -import { getForceNow } from './lib/get_force_now'; import { TimefilterConfig, InputTimeRange, TimeRangeBounds } from './types'; +import { NowProviderInternalContract } from '../../now_provider'; import { calculateBounds, + getAbsoluteTimeRange, getTime, IIndexPattern, RefreshInterval, @@ -60,7 +61,11 @@ export class Timefilter { private readonly timeDefaults: TimeRange; private readonly refreshIntervalDefaults: RefreshInterval; - constructor(config: TimefilterConfig, timeHistory: TimeHistoryContract) { + constructor( + config: TimefilterConfig, + timeHistory: TimeHistoryContract, + private readonly nowProvider: NowProviderInternalContract + ) { this._history = timeHistory; this.timeDefaults = config.timeDefaults; this.refreshIntervalDefaults = config.refreshIntervalDefaults; @@ -109,6 +114,13 @@ export class Timefilter { }; }; + /** + * Same as {@link getTime}, but also converts relative time range to absolute time range + */ + public getAbsoluteTime() { + return getAbsoluteTimeRange(this._time, { forceNow: this.nowProvider.get() }); + } + /** * Updates timefilter time. * Emits 'timeUpdate' and 'fetch' events when time changes @@ -177,7 +189,7 @@ export class Timefilter { public createFilter = (indexPattern: IIndexPattern, timeRange?: TimeRange) => { return getTime(indexPattern, timeRange ? timeRange : this._time, { - forceNow: this.getForceNow(), + forceNow: this.nowProvider.get(), }); }; @@ -186,7 +198,7 @@ export class Timefilter { } public calculateBounds(timeRange: TimeRange): TimeRangeBounds { - return calculateBounds(timeRange, { forceNow: this.getForceNow() }); + return calculateBounds(timeRange, { forceNow: this.nowProvider.get() }); } public getActiveBounds(): TimeRangeBounds | undefined { @@ -234,10 +246,6 @@ export class Timefilter { public getRefreshIntervalDefaults(): RefreshInterval { return _.cloneDeep(this.refreshIntervalDefaults); } - - private getForceNow = () => { - return getForceNow(); - }; } export type TimefilterContract = PublicMethodsOf; diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts index 9f1c64a1739a5..72ff49b9178fc 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts @@ -46,6 +46,7 @@ const createSetupContractMock = () => { createFilter: jest.fn(), getRefreshIntervalDefaults: jest.fn(), getTimeDefaults: jest.fn(), + getAbsoluteTime: jest.fn(), }; const historyMock: jest.Mocked = { diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.ts b/src/plugins/data/public/query/timefilter/timefilter_service.ts index 35b46de5f21b2..1f0c0345a0eae 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.ts @@ -21,6 +21,7 @@ import { IUiSettingsClient } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { TimeHistory, Timefilter, TimeHistoryContract, TimefilterContract } from './index'; import { UI_SETTINGS } from '../../../common'; +import { NowProviderInternalContract } from '../../now_provider'; /** * Filter Service @@ -33,13 +34,15 @@ export interface TimeFilterServiceDependencies { } export class TimefilterService { + constructor(private readonly nowProvider: NowProviderInternalContract) {} + public setup({ uiSettings, storage }: TimeFilterServiceDependencies): TimefilterSetup { const timefilterConfig = { timeDefaults: uiSettings.get(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS), refreshIntervalDefaults: uiSettings.get(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS), }; const history = new TimeHistory(storage); - const timefilter = new Timefilter(timefilterConfig, history); + const timefilter = new Timefilter(timefilterConfig, history, this.nowProvider); return { timefilter, diff --git a/src/plugins/data/public/search/aggs/aggs_service.test.ts b/src/plugins/data/public/search/aggs/aggs_service.test.ts index bc4992384b0c2..86a466b699710 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.test.ts @@ -31,6 +31,7 @@ import { AggsStartDependencies, createGetConfig, } from './aggs_service'; +import { createNowProviderMock } from '../../now_provider/mocks'; const { uiSettings } = coreMock.createSetup(); @@ -44,6 +45,7 @@ describe('AggsService - public', () => { setupDeps = { registerFunction: expressionsPluginMock.createSetupContract().registerFunction, uiSettings, + nowProvider: createNowProviderMock(), }; startDeps = { fieldFormats: fieldFormatsServiceMock.createStartContract(), diff --git a/src/plugins/data/public/search/aggs/aggs_service.ts b/src/plugins/data/public/search/aggs/aggs_service.ts index 7b5edac0280d9..59032a8608632 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.ts @@ -22,7 +22,6 @@ import { Subscription } from 'rxjs'; import { IUiSettingsClient } from 'src/core/public'; import { ExpressionsServiceSetup } from 'src/plugins/expressions/common'; import { FieldFormatsStart } from '../../field_formats'; -import { getForceNow } from '../../query/timefilter/lib/get_force_now'; import { calculateBounds, TimeRange } from '../../../common'; import { aggsRequiredUiSettings, @@ -33,6 +32,7 @@ import { } from '../../../common/search/aggs'; import { AggsSetup, AggsStart } from './types'; import { IndexPatternsContract } from '../../index_patterns'; +import { NowProviderInternalContract } from '../../now_provider'; /** * Aggs needs synchronous access to specific uiSettings. Since settings can change @@ -63,6 +63,7 @@ export function createGetConfig( export interface AggsSetupDependencies { registerFunction: ExpressionsServiceSetup['registerFunction']; uiSettings: IUiSettingsClient; + nowProvider: NowProviderInternalContract; } /** @internal */ @@ -82,15 +83,17 @@ export class AggsService { private readonly initializedAggTypes = new Map(); private getConfig?: AggsCommonStartDependencies['getConfig']; private subscriptions: Subscription[] = []; + private nowProvider!: NowProviderInternalContract; /** - * getForceNow uses window.location, so we must have a separate implementation + * NowGetter uses window.location, so we must have a separate implementation * of calculateBounds on the client and the server. */ private calculateBounds = (timeRange: TimeRange) => - calculateBounds(timeRange, { forceNow: getForceNow() }); + calculateBounds(timeRange, { forceNow: this.nowProvider.get() }); - public setup({ registerFunction, uiSettings }: AggsSetupDependencies): AggsSetup { + public setup({ registerFunction, uiSettings, nowProvider }: AggsSetupDependencies): AggsSetup { + this.nowProvider = nowProvider; this.getConfig = createGetConfig(uiSettings, aggsRequiredUiSettings, this.subscriptions); return this.aggsCommonService.setup({ registerFunction }); diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index d8d90ea464a73..63138ee1b0454 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -53,6 +53,7 @@ export function getFunctionDefinition({ deserializeFieldFormat, indexPatterns, searchSource, + getNow, } = await getStartDependencies(); const indexPattern = await indexPatterns.create(args.index.value, true); @@ -75,6 +76,7 @@ export function getFunctionDefinition({ searchSourceService: searchSource, timeFields: args.timeFields, timeRange: get(input, 'timeRange', undefined), + getNow, }); }, }); @@ -102,12 +104,13 @@ export function getEsaggs({ return getFunctionDefinition({ getStartDependencies: async () => { const [, , self] = await getStartServices(); - const { fieldFormats, indexPatterns, search } = self; + const { fieldFormats, indexPatterns, search, nowProvider } = self; return { aggs: search.aggs, deserializeFieldFormat: fieldFormats.deserialize.bind(fieldFormats), indexPatterns, searchSource: search.searchSource, + getNow: () => nowProvider.get(), }; }, }); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 1c49de8f0ff4b..eef6190fe78c5 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -53,12 +53,14 @@ import { } from '../../common/search/aggs/buckets/shard_delay'; import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; import { DataPublicPluginStart, DataStartDependencies } from '../types'; +import { NowProviderInternalContract } from '../now_provider'; /** @internal */ export interface SearchServiceSetupDependencies { bfetch: BfetchPublicSetup; expressions: ExpressionsSetup; usageCollection?: UsageCollectionSetup; + nowProvider: NowProviderInternalContract; } /** @internal */ @@ -79,7 +81,7 @@ export class SearchService implements Plugin { public setup( { http, getStartServices, notifications, uiSettings }: CoreSetup, - { bfetch, expressions, usageCollection }: SearchServiceSetupDependencies + { bfetch, expressions, usageCollection, nowProvider }: SearchServiceSetupDependencies ): ISearchSetup { this.usageCollector = createUsageCollector(getStartServices, usageCollection); @@ -87,7 +89,8 @@ export class SearchService implements Plugin { this.sessionService = new SessionService( this.initializerContext, getStartServices, - this.sessionsClient + this.sessionsClient, + nowProvider ); /** * A global object that intercepts all searches and provides convenience methods for cancelling @@ -118,6 +121,7 @@ export class SearchService implements Plugin { const aggs = this.aggsService.setup({ registerFunction: expressions.registerFunction, uiSettings, + nowProvider, }); if (this.initializerContext.config.get().search.aggs.shardDelay.enabled) { diff --git a/src/plugins/data/public/search/session/search_session_state.test.ts b/src/plugins/data/public/search/session/search_session_state.test.ts index 539fc8252b2a5..c2cb75b9a7493 100644 --- a/src/plugins/data/public/search/session/search_session_state.test.ts +++ b/src/plugins/data/public/search/session/search_session_state.test.ts @@ -31,6 +31,7 @@ describe('Session state container', () => { state.transitions.start(); expect(state.selectors.getState()).toBe(SearchSessionState.None); expect(state.get().sessionId).not.toBeUndefined(); + expect(state.get().startTime).not.toBeUndefined(); }); test('track', () => { @@ -56,6 +57,7 @@ describe('Session state container', () => { state.transitions.clear(); expect(state.selectors.getState()).toBe(SearchSessionState.None); expect(state.get().sessionId).toBeUndefined(); + expect(state.get().startTime).toBeUndefined(); }); test('cancel', () => { diff --git a/src/plugins/data/public/search/session/search_session_state.ts b/src/plugins/data/public/search/session/search_session_state.ts index 7a35a65a600d7..0894de13d53a9 100644 --- a/src/plugins/data/public/search/session/search_session_state.ts +++ b/src/plugins/data/public/search/session/search_session_state.ts @@ -105,6 +105,11 @@ export interface SessionStateInternal { * If user has explicitly canceled search requests */ isCanceled: boolean; + + /** + * Start time of current session + */ + startTime?: Date; } const createSessionDefaultState: < @@ -132,7 +137,11 @@ export interface SessionPureTransitions< } export const sessionPureTransitions: SessionPureTransitions = { - start: (state) => () => ({ ...createSessionDefaultState(), sessionId: uuid.v4() }), + start: (state) => () => ({ + ...createSessionDefaultState(), + sessionId: uuid.v4(), + startTime: new Date(), + }), restore: (state) => (sessionId: string) => ({ ...createSessionDefaultState(), sessionId, @@ -216,6 +225,7 @@ export const createSessionStateContainer = ( ): { stateContainer: SessionStateContainer; sessionState$: Observable; + sessionStartTime$: Observable; } => { const stateContainer = createStateContainer( createSessionDefaultState(), @@ -229,8 +239,16 @@ export const createSessionStateContainer = ( distinctUntilChanged(), shareReplay(1) ); + + const sessionStartTime$: Observable = stateContainer.state$.pipe( + map(() => stateContainer.get().startTime), + distinctUntilChanged(), + shareReplay(1) + ); + return { stateContainer, sessionState$, + sessionStartTime$, }; }; diff --git a/src/plugins/data/public/search/session/session_service.test.ts b/src/plugins/data/public/search/session/session_service.test.ts index cf083239b1571..aeca7b4d63da7 100644 --- a/src/plugins/data/public/search/session/session_service.test.ts +++ b/src/plugins/data/public/search/session/session_service.test.ts @@ -23,17 +23,22 @@ import { take, toArray } from 'rxjs/operators'; import { getSessionsClientMock } from './mocks'; import { BehaviorSubject } from 'rxjs'; import { SearchSessionState } from './search_session_state'; +import { createNowProviderMock } from '../../now_provider/mocks'; +import { NowProviderInternalContract } from '../../now_provider'; describe('Session service', () => { let sessionService: ISessionService; let state$: BehaviorSubject; + let nowProvider: jest.Mocked; beforeEach(() => { const initializerContext = coreMock.createPluginInitializerContext(); + nowProvider = createNowProviderMock(); sessionService = new SessionService( initializerContext, coreMock.createSetup().getStartServices, getSessionsClientMock(), + nowProvider, { freezeState: false } // needed to use mocks inside state container ); state$ = new BehaviorSubject(SearchSessionState.None); @@ -44,8 +49,10 @@ describe('Session service', () => { it('Creates and clears a session', async () => { sessionService.start(); expect(sessionService.getSessionId()).not.toBeUndefined(); + expect(nowProvider.set).toHaveBeenCalled(); sessionService.clear(); expect(sessionService.getSessionId()).toBeUndefined(); + expect(nowProvider.reset).toHaveBeenCalled(); }); it('Restores a session', async () => { diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts index 2bbb762fcfe9f..e2185d8148708 100644 --- a/src/plugins/data/public/search/session/session_service.ts +++ b/src/plugins/data/public/search/session/session_service.ts @@ -29,6 +29,7 @@ import { SessionStateContainer, } from './search_session_state'; import { ISessionsClient } from './sessions_client'; +import { NowProviderInternalContract } from '../../now_provider'; export type ISessionService = PublicContract; @@ -60,40 +61,54 @@ export class SessionService { private readonly state: SessionStateContainer; private searchSessionInfoProvider?: SearchSessionInfoProvider; - private appChangeSubscription$?: Subscription; + private subscription = new Subscription(); private curApp?: string; constructor( initializerContext: PluginInitializerContext, getStartServices: StartServicesAccessor, private readonly sessionsClient: ISessionsClient, + private readonly nowProvider: NowProviderInternalContract, { freezeState = true }: { freezeState: boolean } = { freezeState: true } ) { - const { stateContainer, sessionState$ } = createSessionStateContainer({ + const { + stateContainer, + sessionState$, + sessionStartTime$, + } = createSessionStateContainer({ freeze: freezeState, }); this.state$ = sessionState$; this.state = stateContainer; + this.subscription.add( + sessionStartTime$.subscribe((startTime) => { + if (startTime) this.nowProvider.set(startTime); + else this.nowProvider.reset(); + }) + ); + getStartServices().then(([coreStart]) => { // Apps required to clean up their sessions before unmounting // Make sure that apps don't leave sessions open. - this.appChangeSubscription$ = coreStart.application.currentAppId$.subscribe((appName) => { - if (this.state.get().sessionId) { - const message = `Application '${this.curApp}' had an open session while navigating`; - if (initializerContext.env.mode.dev) { - // TODO: This setTimeout is necessary due to a race condition while navigating. - setTimeout(() => { - coreStart.fatalErrors.add(message); - }, 100); - } else { - // eslint-disable-next-line no-console - console.warn(message); - this.clear(); + this.subscription.add( + coreStart.application.currentAppId$.subscribe((appName) => { + if (this.state.get().sessionId) { + const message = `Application '${this.curApp}' had an open session while navigating`; + if (initializerContext.env.mode.dev) { + // TODO: This setTimeout is necessary due to a race condition while navigating. + setTimeout(() => { + coreStart.fatalErrors.add(message); + }, 100); + } else { + // eslint-disable-next-line no-console + console.warn(message); + this.clear(); + } } - } - this.curApp = appName; - }); + this.curApp = appName; + }) + ); }); } @@ -122,9 +137,7 @@ export class SessionService { } public destroy() { - if (this.appChangeSubscription$) { - this.appChangeSubscription$.unsubscribe(); - } + this.subscription.unsubscribe(); this.clear(); } diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index c7b66acfc6c7a..e38cabe313b6c 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -32,6 +32,7 @@ import { IndexPatternsContract } from './index_patterns'; import { IndexPatternSelectProps, StatefulSearchBarProps } from './ui'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { Setup as InspectorSetup } from '../../inspector/public'; +import { NowProviderPublicContract } from './now_provider'; export interface DataPublicPluginEnhancements { search: SearchEnhancements; @@ -118,6 +119,8 @@ export interface DataPublicPluginStart { * {@link DataPublicPluginStartUi} */ ui: DataPublicPluginStartUi; + + nowProvider: NowProviderPublicContract; } export interface IDataPluginServices extends Partial { diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 4c5cb864b5111..839add1aeb22d 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -295,7 +295,7 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab createSearchSessionRestorationDataProvider({ appStateContainer, data, - getSavedSearchId: () => savedSearch.id, + getSavedSearch: () => savedSearch, }) ); diff --git a/src/plugins/discover/public/application/angular/discover_state.test.ts b/src/plugins/discover/public/application/angular/discover_state.test.ts index 2914ce8f17a09..e05a3028c5ca9 100644 --- a/src/plugins/discover/public/application/angular/discover_state.test.ts +++ b/src/plugins/discover/public/application/angular/discover_state.test.ts @@ -17,8 +17,14 @@ * under the License. */ -import { getState, GetStateReturn } from './discover_state'; +import { + getState, + GetStateReturn, + createSearchSessionRestorationDataProvider, +} from './discover_state'; import { createBrowserHistory, History } from 'history'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { SavedSearch } from '../../saved_searches'; let history: History; let state: GetStateReturn; @@ -103,3 +109,30 @@ describe('Test discover state with legacy migration', () => { `); }); }); + +describe('createSearchSessionRestorationDataProvider', () => { + let mockSavedSearch: SavedSearch = ({} as unknown) as SavedSearch; + const searchSessionInfoProvider = createSearchSessionRestorationDataProvider({ + data: dataPluginMock.createStartContract(), + appStateContainer: getState({ + history: createBrowserHistory(), + }).appStateContainer, + getSavedSearch: () => mockSavedSearch, + }); + + describe('session name', () => { + test('No saved search returns default name', async () => { + expect(await searchSessionInfoProvider.getName()).toBe('Discover'); + }); + + test('Saved Search with a title returns saved search title', async () => { + mockSavedSearch = ({ id: 'id', title: 'Name' } as unknown) as SavedSearch; + expect(await searchSessionInfoProvider.getName()).toBe('Name'); + }); + + test('Saved Search without a title returns default name', async () => { + mockSavedSearch = ({ id: 'id', title: undefined } as unknown) as SavedSearch; + expect(await searchSessionInfoProvider.getName()).toBe('Discover'); + }); + }); +}); diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index f52bc8b49ba42..69291872fa855 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/angular/discover_state.ts @@ -17,6 +17,7 @@ * under the License. */ import { isEqual } from 'lodash'; +import { i18n } from '@kbn/i18n'; import { History } from 'history'; import { NotificationsStart } from 'kibana/public'; import { @@ -38,6 +39,7 @@ import { import { migrateLegacyQuery } from '../helpers/migrate_legacy_query'; import { DiscoverGridSettings } from '../components/discover_grid/types'; import { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from '../../url_generator'; +import { SavedSearch } from '../../saved_searches'; export interface AppState { /** @@ -264,15 +266,32 @@ export function isEqualState(stateA: AppState, stateB: AppState) { export function createSearchSessionRestorationDataProvider(deps: { appStateContainer: StateContainer; data: DataPublicPluginStart; - getSavedSearchId: () => string | undefined; + getSavedSearch: () => SavedSearch; }): SearchSessionInfoProvider { + const getSavedSearchId = () => deps.getSavedSearch().id; return { - getName: async () => 'Discover', + getName: async () => { + const savedSearch = deps.getSavedSearch(); + return ( + (savedSearch.id && savedSearch.title) || + i18n.translate('discover.discoverDefaultSearchSessionName', { + defaultMessage: 'Discover', + }) + ); + }, getUrlGeneratorData: async () => { return { urlGeneratorId: DISCOVER_APP_URL_GENERATOR, - initialState: createUrlGeneratorState({ ...deps, forceAbsoluteTime: false }), - restoreState: createUrlGeneratorState({ ...deps, forceAbsoluteTime: true }), + initialState: createUrlGeneratorState({ + ...deps, + getSavedSearchId, + forceAbsoluteTime: false, + }), + restoreState: createUrlGeneratorState({ + ...deps, + getSavedSearchId, + forceAbsoluteTime: true, + }), }; }, }; @@ -282,11 +301,14 @@ function createUrlGeneratorState({ appStateContainer, data, getSavedSearchId, - forceAbsoluteTime, // TODO: not implemented + forceAbsoluteTime, }: { appStateContainer: StateContainer; data: DataPublicPluginStart; getSavedSearchId: () => string | undefined; + /** + * Can force time range from time filter to convert from relative to absolute time range + */ forceAbsoluteTime: boolean; }): DiscoverUrlGeneratorState { const appState = appStateContainer.get(); @@ -295,7 +317,9 @@ function createUrlGeneratorState({ indexPatternId: appState.index, query: appState.query, savedSearchId: getSavedSearchId(), - timeRange: data.query.timefilter.timefilter.getTime(), // TODO: handle relative time range + timeRange: forceAbsoluteTime + ? data.query.timefilter.timefilter.getAbsoluteTime() + : data.query.timefilter.timefilter.getTime(), searchSessionId: data.search.session.getSessionId(), columns: appState.columns, sort: appState.sort, diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index e4a8ab7bc67ff..6919dc1bef286 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -29,7 +29,6 @@ import { Filter, TimeRange, FilterManager, - getTime, Query, IFieldType, } from '../../../../data/public'; @@ -98,7 +97,6 @@ export class SearchEmbeddable private panelTitle: string = ''; private filtersSearchSource?: ISearchSource; private searchInstance?: JQLite; - private autoRefreshFetchSubscription?: Subscription; private subscription?: Subscription; public readonly type = SEARCH_EMBEDDABLE_TYPE; private filterManager: FilterManager; @@ -148,10 +146,6 @@ export class SearchEmbeddable }; this.initializeSearchScope(); - this.autoRefreshFetchSubscription = this.services.timefilter - .getAutoRefreshFetch$() - .subscribe(this.fetch); - this.subscription = this.getUpdated$().subscribe(() => { this.panelTitle = this.output.title || ''; @@ -199,9 +193,7 @@ export class SearchEmbeddable if (this.subscription) { this.subscription.unsubscribe(); } - if (this.autoRefreshFetchSubscription) { - this.autoRefreshFetchSubscription.unsubscribe(); - } + if (this.abortController) this.abortController.abort(); } @@ -224,7 +216,7 @@ export class SearchEmbeddable const timeRangeSearchSource = searchSource.create(); timeRangeSearchSource.setField('filter', () => { if (!this.searchScope || !this.input.timeRange) return; - return getTime(indexPattern, this.input.timeRange); + return this.services.timefilter.createFilter(indexPattern, this.input.timeRange); }); this.filtersSearchSource = searchSource.create(); diff --git a/src/plugins/discover/public/url_generator.test.ts b/src/plugins/discover/public/url_generator.test.ts index 95bff6b1fdc9c..f87ef4f91108a 100644 --- a/src/plugins/discover/public/url_generator.test.ts +++ b/src/plugins/discover/public/url_generator.test.ts @@ -62,7 +62,7 @@ describe('Discover url generator', () => { const url = await generator.createUrl({ savedSearchId }); const { _a, _g } = getStatesFromKbnUrl(url, ['_a', '_g']); - expect(url.startsWith(`${appBasePath}#/${savedSearchId}`)).toBe(true); + expect(url.startsWith(`${appBasePath}#/view/${savedSearchId}`)).toBe(true); expect(_a).toEqual({}); expect(_g).toEqual({}); }); diff --git a/src/plugins/discover/public/url_generator.ts b/src/plugins/discover/public/url_generator.ts index 6d86818910b11..d1b574c360d88 100644 --- a/src/plugins/discover/public/url_generator.ts +++ b/src/plugins/discover/public/url_generator.ts @@ -119,7 +119,7 @@ export class DiscoverUrlGenerator sort, interval, }: DiscoverUrlGeneratorState): Promise => { - const savedSearchPath = savedSearchId ? encodeURIComponent(savedSearchId) : ''; + const savedSearchPath = savedSearchId ? `view/${encodeURIComponent(savedSearchId)}` : ''; const appState: { query?: Query; filters?: Filter[]; diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json new file mode 100644 index 0000000000000..ec98199c3423e --- /dev/null +++ b/src/plugins/discover/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["common/**/*", "public/**/*", "server/**/*", "../../../typings/**/*"], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../charts/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../embeddable/tsconfig.json" }, + { "path": "../inspector/tsconfig.json" }, + { "path": "../url_forwarding/tsconfig.json" }, + { "path": "../saved_objects/tsconfig.json" }, + { "path": "../navigation/tsconfig.json" }, + { "path": "../ui_actions/tsconfig.json" }, + { "path": "../home/tsconfig.json" }, + { "path": "../share/tsconfig.json" }, + { "path": "../usage_collection/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../kibana_legacy/tsconfig.json" } + ] +} diff --git a/src/plugins/embeddable/README.asciidoc b/src/plugins/embeddable/README.asciidoc index 5e3c5066f46c7..daa6040eab7eb 100644 --- a/src/plugins/embeddable/README.asciidoc +++ b/src/plugins/embeddable/README.asciidoc @@ -22,7 +22,7 @@ There is also an example of rendering dashboard container outside of dashboard a === Docs -(./docs/README.md)[Embeddable docs, guides & caveats] +link:./docs/README.md[Embeddable docs, guides & caveats] === API docs diff --git a/src/plugins/input_control_vis/kibana.json b/src/plugins/input_control_vis/kibana.json index c6d1157f5ff25..5acb79a8ab038 100644 --- a/src/plugins/input_control_vis/kibana.json +++ b/src/plugins/input_control_vis/kibana.json @@ -4,6 +4,13 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["data", "expressions", "visualizations", "visDefaultEditor"], - "requiredBundles": ["kibanaReact"] + "requiredPlugins": [ + "data", + "expressions", + "visDefaultEditor", + "visualizations" + ], + "requiredBundles": [ + "kibanaReact" + ] } diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json new file mode 100644 index 0000000000000..1e3756f45e953 --- /dev/null +++ b/src/plugins/presentation_util/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["common/**/*", "public/**/*"], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../dashboard/tsconfig.json" }, + { "path": "../saved_objects/tsconfig.json" }, + ] +} diff --git a/src/plugins/spaces_oss/README.md b/src/plugins/spaces_oss/README.md new file mode 100644 index 0000000000000..73de736d6fb4e --- /dev/null +++ b/src/plugins/spaces_oss/README.md @@ -0,0 +1,3 @@ +# SpacesOss + +Bridge plugin for consumption of the Spaces feature from OSS plugins. diff --git a/src/plugins/spaces_oss/common/index.ts b/src/plugins/spaces_oss/common/index.ts new file mode 100644 index 0000000000000..8a04df861b5e5 --- /dev/null +++ b/src/plugins/spaces_oss/common/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { Space } from './types'; diff --git a/src/plugins/spaces_oss/common/types.ts b/src/plugins/spaces_oss/common/types.ts new file mode 100644 index 0000000000000..6eff95a8d3d9f --- /dev/null +++ b/src/plugins/spaces_oss/common/types.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface Space { + id: string; + name: string; + description?: string; + color?: string; + initials?: string; + disabledFeatures: string[]; + _reserved?: boolean; + imageUrl?: string; +} diff --git a/src/plugins/vis_type_timeseries/common/field_types.js b/src/plugins/spaces_oss/jest.config.js similarity index 86% rename from src/plugins/vis_type_timeseries/common/field_types.js rename to src/plugins/spaces_oss/jest.config.js index f5323f1542a31..d60652d15bfad 100644 --- a/src/plugins/vis_type_timeseries/common/field_types.js +++ b/src/plugins/spaces_oss/jest.config.js @@ -17,10 +17,8 @@ * under the License. */ -export const FIELD_TYPES = { - BOOLEAN: 'boolean', - DATE: 'date', - GEO: 'geo_point', - NUMBER: 'number', - STRING: 'string', +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/spaces_oss'], }; diff --git a/src/plugins/spaces_oss/kibana.json b/src/plugins/spaces_oss/kibana.json new file mode 100644 index 0000000000000..e048fb7ffb79c --- /dev/null +++ b/src/plugins/spaces_oss/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "spacesOss", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": [], + "optionalPlugins": [] +} diff --git a/src/plugins/spaces_oss/public/api.mock.ts b/src/plugins/spaces_oss/public/api.mock.ts new file mode 100644 index 0000000000000..21d991c33e382 --- /dev/null +++ b/src/plugins/spaces_oss/public/api.mock.ts @@ -0,0 +1,30 @@ +/* + * 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 { of } from 'rxjs'; +import { SpacesApi } from './api'; + +const createApiMock = (): jest.Mocked => ({ + activeSpace$: of(), + getActiveSpace: jest.fn(), +}); + +export const spacesApiMock = { + create: createApiMock, +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.js b/src/plugins/spaces_oss/public/api.ts similarity index 78% rename from src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.js rename to src/plugins/spaces_oss/public/api.ts index ff6c2cc0767f5..7725175a5b873 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.js +++ b/src/plugins/spaces_oss/public/api.ts @@ -17,12 +17,13 @@ * under the License. */ -import _ from 'lodash'; +import { Observable } from 'rxjs'; +import { Space } from '../common'; -export const createSelectHandler = (handleChange) => { - return (name) => (selectedOptions) => { - return handleChange?.({ - [name]: _.get(selectedOptions, '[0].value', null), - }); - }; -}; +/** + * @public + */ +export interface SpacesApi { + readonly activeSpace$: Observable; + getActiveSpace(): Promise; +} diff --git a/src/plugins/spaces_oss/public/index.ts b/src/plugins/spaces_oss/public/index.ts new file mode 100644 index 0000000000000..f6e551053ae6c --- /dev/null +++ b/src/plugins/spaces_oss/public/index.ts @@ -0,0 +1,26 @@ +/* + * 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 { SpacesOssPlugin } from './plugin'; + +export { SpacesOssPluginSetup, SpacesOssPluginStart } from './types'; + +export { SpacesApi } from './api'; + +export const plugin = () => new SpacesOssPlugin(); diff --git a/src/plugins/spaces_oss/public/mocks/index.ts b/src/plugins/spaces_oss/public/mocks/index.ts new file mode 100644 index 0000000000000..f201ed9334804 --- /dev/null +++ b/src/plugins/spaces_oss/public/mocks/index.ts @@ -0,0 +1,35 @@ +/* + * 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 { spacesApiMock } from '../api.mock'; +import { SpacesOssPluginSetup, SpacesOssPluginStart } from '..'; + +const createSetupContract = (): jest.Mocked => ({ + registerSpacesApi: jest.fn(), +}); + +const createStartContract = (): jest.Mocked => ({ + isSpacesAvailable: true, + ...spacesApiMock.create(), +}); + +export const spacesOssPluginMock = { + createSetupContract, + createStartContract, +}; diff --git a/src/plugins/spaces_oss/public/plugin.test.ts b/src/plugins/spaces_oss/public/plugin.test.ts new file mode 100644 index 0000000000000..4099c9a7eb4c0 --- /dev/null +++ b/src/plugins/spaces_oss/public/plugin.test.ts @@ -0,0 +1,65 @@ +/* + * 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 { spacesApiMock } from './api.mock'; +import { SpacesOssPlugin } from './plugin'; + +describe('SpacesOssPlugin', () => { + let plugin: SpacesOssPlugin; + + beforeEach(() => { + plugin = new SpacesOssPlugin(); + }); + + describe('#setup', () => { + it('only allows the API to be registered once', async () => { + const spacesApi = spacesApiMock.create(); + const { registerSpacesApi } = plugin.setup(); + + expect(() => registerSpacesApi(spacesApi)).not.toThrow(); + + expect(() => registerSpacesApi(spacesApi)).toThrowErrorMatchingInlineSnapshot( + `"Spaces API can only be registered once"` + ); + }); + }); + + describe('#start', () => { + it('returns the spaces API if registered', async () => { + const spacesApi = spacesApiMock.create(); + const { registerSpacesApi } = plugin.setup(); + + registerSpacesApi(spacesApi); + + const { isSpacesAvailable, ...api } = plugin.start(); + + expect(isSpacesAvailable).toBe(true); + expect(api).toStrictEqual(spacesApi); + }); + + it('does not return the spaces API if not registered', async () => { + plugin.setup(); + + const { isSpacesAvailable, ...api } = plugin.start(); + + expect(isSpacesAvailable).toBe(false); + expect(Object.keys(api)).toHaveLength(0); + }); + }); +}); diff --git a/src/plugins/spaces_oss/public/plugin.ts b/src/plugins/spaces_oss/public/plugin.ts new file mode 100644 index 0000000000000..93b1c7696f0fb --- /dev/null +++ b/src/plugins/spaces_oss/public/plugin.ts @@ -0,0 +1,52 @@ +/* + * 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 { Plugin } from 'src/core/public'; +import { SpacesOssPluginSetup, SpacesOssPluginStart } from './types'; +import { SpacesApi } from './api'; + +export class SpacesOssPlugin implements Plugin { + private api?: SpacesApi; + + constructor() {} + + public setup() { + return { + registerSpacesApi: (provider: SpacesApi) => { + if (this.api) { + throw new Error('Spaces API can only be registered once'); + } + this.api = provider; + }, + }; + } + + public start() { + if (this.api) { + return { + isSpacesAvailable: true as true, + ...this.api!, + }; + } else { + return { + isSpacesAvailable: false as false, + }; + } + } +} diff --git a/src/plugins/data/public/query/timefilter/lib/get_force_now.ts b/src/plugins/spaces_oss/public/types.ts similarity index 60% rename from src/plugins/data/public/query/timefilter/lib/get_force_now.ts rename to src/plugins/spaces_oss/public/types.ts index fe68656f0c3aa..e089231513e23 100644 --- a/src/plugins/data/public/query/timefilter/lib/get_force_now.ts +++ b/src/plugins/spaces_oss/public/types.ts @@ -17,18 +17,23 @@ * under the License. */ -import { parseQueryString } from './parse_querystring'; +import { SpacesApi } from './api'; -/** @internal */ -export function getForceNow() { - const forceNow = parseQueryString().forceNow as string; - if (!forceNow) { - return; - } +interface SpacesAvailableStartContract extends SpacesApi { + isSpacesAvailable: true; +} + +interface SpacesUnavailableStartContract { + isSpacesAvailable: false; +} - const ticks = Date.parse(forceNow); - if (isNaN(ticks)) { - throw new Error(`forceNow query parameter, ${forceNow}, can't be parsed by Date.parse`); - } - return new Date(ticks); +export interface SpacesOssPluginSetup { + /** + * Register a provider for the Spaces API. + * + * Only one provider can be registered, subsequent calls to this method will fail. + */ + registerSpacesApi(provider: SpacesApi): void; } + +export type SpacesOssPluginStart = SpacesAvailableStartContract | SpacesUnavailableStartContract; diff --git a/src/plugins/spaces_oss/tsconfig.json b/src/plugins/spaces_oss/tsconfig.json new file mode 100644 index 0000000000000..0cc82d7e5d124 --- /dev/null +++ b/src/plugins/spaces_oss/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + ] +} diff --git a/src/plugins/vis_default_editor/public/default_editor.tsx b/src/plugins/vis_default_editor/public/default_editor.tsx index a7251acfdf75d..719766544d4f2 100644 --- a/src/plugins/vis_default_editor/public/default_editor.tsx +++ b/src/plugins/vis_default_editor/public/default_editor.tsx @@ -23,8 +23,11 @@ import 'brace/mode/json'; import React, { useEffect, useRef, useState, useCallback } from 'react'; import { EventEmitter } from 'events'; -import { EditorRenderProps } from 'src/plugins/visualize/public'; -import { Vis, VisualizeEmbeddableContract } from 'src/plugins/visualizations/public'; +import { + Vis, + VisualizeEmbeddableContract, + EditorRenderProps, +} from 'src/plugins/visualizations/public'; import { KibanaContextProvider, PanelsContainer, Panel } from '../../kibana_react/public'; import { Storage } from '../../kibana_utils/public'; diff --git a/src/plugins/vis_default_editor/public/default_editor_controller.tsx b/src/plugins/vis_default_editor/public/default_editor_controller.tsx index f44ea3e203b05..ebeb056cc89c1 100644 --- a/src/plugins/vis_default_editor/public/default_editor_controller.tsx +++ b/src/plugins/vis_default_editor/public/default_editor_controller.tsx @@ -22,8 +22,12 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { EventEmitter } from 'events'; import { EuiErrorBoundary, EuiLoadingChart } from '@elastic/eui'; -import { EditorRenderProps, IEditorController } from 'src/plugins/visualize/public'; -import { Vis, VisualizeEmbeddableContract } from 'src/plugins/visualizations/public'; +import { + Vis, + IEditorController, + EditorRenderProps, + VisualizeEmbeddableContract, +} from 'src/plugins/visualizations/public'; // @ts-ignore const DefaultEditor = lazy(() => import('./default_editor')); diff --git a/src/plugins/vis_type_markdown/kibana.json b/src/plugins/vis_type_markdown/kibana.json index 6cfedf60687ef..e14b4fe7bce96 100644 --- a/src/plugins/vis_type_markdown/kibana.json +++ b/src/plugins/vis_type_markdown/kibana.json @@ -3,6 +3,14 @@ "version": "kibana", "ui": true, "server": true, - "requiredPlugins": ["expressions", "visualizations"], - "requiredBundles": ["kibanaReact", "visualizations", "expressions", "visDefaultEditor"] + "requiredPlugins": [ + "expressions", + "visualizations" + ], + "requiredBundles": [ + "expressions", + "kibanaReact", + "visDefaultEditor", + "visualizations" + ] } diff --git a/src/plugins/vis_type_timeseries/common/__snapshots__/model_options.test.js.snap b/src/plugins/vis_type_timeseries/common/__snapshots__/model_options.test.js.snap deleted file mode 100644 index 0fca2a017b911..0000000000000 --- a/src/plugins/vis_type_timeseries/common/__snapshots__/model_options.test.js.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`src/legacy/core_plugins/metrics/common/model_options.js MODEL_TYPES should match a snapshot of constants 1`] = ` -Object { - "UNWEIGHTED": "simple", - "WEIGHTED_EXPONENTIAL": "ewma", - "WEIGHTED_EXPONENTIAL_DOUBLE": "holt", - "WEIGHTED_EXPONENTIAL_TRIPLE": "holt_winters", - "WEIGHTED_LINEAR": "linear", -} -`; diff --git a/src/plugins/vis_type_timeseries/common/agg_lookup.test.js b/src/plugins/vis_type_timeseries/common/agg_lookup.test.ts similarity index 83% rename from src/plugins/vis_type_timeseries/common/agg_lookup.test.js rename to src/plugins/vis_type_timeseries/common/agg_lookup.test.ts index a7c5d362e669c..d61cab6229305 100644 --- a/src/plugins/vis_type_timeseries/common/agg_lookup.test.js +++ b/src/plugins/vis_type_timeseries/common/agg_lookup.test.ts @@ -18,14 +18,15 @@ */ import { isBasicAgg } from './agg_lookup'; +import { MetricsItemsSchema } from './types'; describe('aggLookup', () => { describe('isBasicAgg(metric)', () => { test('returns true for a basic metric (count)', () => { - expect(isBasicAgg({ type: 'count' })).toEqual(true); + expect(isBasicAgg({ type: 'count' } as MetricsItemsSchema)).toEqual(true); }); test('returns false for a pipeline metric (derivative)', () => { - expect(isBasicAgg({ type: 'derivative' })).toEqual(false); + expect(isBasicAgg({ type: 'derivative' } as MetricsItemsSchema)).toEqual(false); }); }); }); diff --git a/src/plugins/vis_type_timeseries/common/agg_lookup.js b/src/plugins/vis_type_timeseries/common/agg_lookup.ts similarity index 92% rename from src/plugins/vis_type_timeseries/common/agg_lookup.js rename to src/plugins/vis_type_timeseries/common/agg_lookup.ts index 0a71ab34082f8..7cdae3f55978a 100644 --- a/src/plugins/vis_type_timeseries/common/agg_lookup.js +++ b/src/plugins/vis_type_timeseries/common/agg_lookup.ts @@ -17,10 +17,11 @@ * under the License. */ -import _ from 'lodash'; +import { omit, pick, includes } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { MetricsItemsSchema } from './types'; -export const lookup = { +export const lookup: Record = { count: i18n.translate('visTypeTimeseries.aggLookup.countLabel', { defaultMessage: 'Count' }), calculation: i18n.translate('visTypeTimeseries.aggLookup.calculationLabel', { defaultMessage: 'Calculation', @@ -122,11 +123,11 @@ const pipeline = [ const byType = { _all: lookup, - pipeline: pipeline, - basic: _.omit(lookup, pipeline), - metrics: _.pick(lookup, ['count', 'avg', 'min', 'max', 'sum', 'cardinality', 'value_count']), + pipeline, + basic: omit(lookup, pipeline), + metrics: pick(lookup, ['count', 'avg', 'min', 'max', 'sum', 'cardinality', 'value_count']), }; -export function isBasicAgg(item) { - return _.includes(Object.keys(byType.basic), item.type); +export function isBasicAgg(item: MetricsItemsSchema) { + return includes(Object.keys(byType.basic), item.type); } diff --git a/src/plugins/vis_type_timeseries/common/calculate_label.test.js b/src/plugins/vis_type_timeseries/common/calculate_label.test.ts similarity index 56% rename from src/plugins/vis_type_timeseries/common/calculate_label.test.js rename to src/plugins/vis_type_timeseries/common/calculate_label.test.ts index a5af6d114c894..0f201d405ee0d 100644 --- a/src/plugins/vis_type_timeseries/common/calculate_label.test.js +++ b/src/plugins/vis_type_timeseries/common/calculate_label.test.ts @@ -18,66 +18,79 @@ */ import { calculateLabel } from './calculate_label'; +import type { MetricsItemsSchema } from './types'; describe('calculateLabel(metric, metrics)', () => { - test('returns "Unknown" for empty metric', () => { - expect(calculateLabel()).toEqual('Unknown'); - }); - test('returns the metric.alias if set', () => { - expect(calculateLabel({ alias: 'Example' })).toEqual('Example'); + expect(calculateLabel({ alias: 'Example' } as MetricsItemsSchema)).toEqual('Example'); }); test('returns "Count" for a count metric', () => { - expect(calculateLabel({ type: 'count' })).toEqual('Count'); + expect(calculateLabel({ type: 'count' } as MetricsItemsSchema)).toEqual('Count'); }); test('returns "Calculation" for a bucket script metric', () => { - expect(calculateLabel({ type: 'calculation' })).toEqual('Bucket Script'); + expect(calculateLabel({ type: 'calculation' } as MetricsItemsSchema)).toEqual('Bucket Script'); }); - test('returns formated label for series_agg', () => { - const label = calculateLabel({ type: 'series_agg', function: 'max' }); + test('returns formatted label for series_agg', () => { + const label = calculateLabel({ type: 'series_agg', function: 'max' } as MetricsItemsSchema); + expect(label).toEqual('Series Agg (max)'); }); - test('returns formated label for basic aggs', () => { - const label = calculateLabel({ type: 'avg', field: 'memory' }); + test('returns formatted label for basic aggs', () => { + const label = calculateLabel({ type: 'avg', field: 'memory' } as MetricsItemsSchema); + expect(label).toEqual('Average of memory'); }); - test('returns formated label for pipeline aggs', () => { - const metric = { id: 2, type: 'derivative', field: 1 }; - const metrics = [{ id: 1, type: 'max', field: 'network.out.bytes' }, metric]; + test('returns formatted label for pipeline aggs', () => { + const metric = ({ id: 2, type: 'derivative', field: 1 } as unknown) as MetricsItemsSchema; + const metrics = ([ + { id: 1, type: 'max', field: 'network.out.bytes' }, + metric, + ] as unknown) as MetricsItemsSchema[]; const label = calculateLabel(metric, metrics); + expect(label).toEqual('Derivative of Max of network.out.bytes'); }); - test('returns formated label for derivative of percentile', () => { - const metric = { id: 2, type: 'derivative', field: '1[50.0]' }; - const metrics = [{ id: 1, type: 'percentile', field: 'network.out.bytes' }, metric]; + test('returns formatted label for derivative of percentile', () => { + const metric = ({ + id: 2, + type: 'derivative', + field: '1[50.0]', + } as unknown) as MetricsItemsSchema; + const metrics = ([ + { id: 1, type: 'percentile', field: 'network.out.bytes' }, + metric, + ] as unknown) as MetricsItemsSchema[]; const label = calculateLabel(metric, metrics); + expect(label).toEqual('Derivative of Percentile of network.out.bytes (50.0)'); }); - test('returns formated label for pipeline aggs (deep)', () => { - const metric = { id: 3, type: 'derivative', field: 2 }; - const metrics = [ + test('returns formatted label for pipeline aggs (deep)', () => { + const metric = ({ id: 3, type: 'derivative', field: 2 } as unknown) as MetricsItemsSchema; + const metrics = ([ { id: 1, type: 'max', field: 'network.out.bytes' }, { id: 2, type: 'moving_average', field: 1 }, metric, - ]; + ] as unknown) as MetricsItemsSchema[]; const label = calculateLabel(metric, metrics); + expect(label).toEqual('Derivative of Moving Average of Max of network.out.bytes'); }); - test('returns formated label for pipeline aggs uses alias for field metric', () => { - const metric = { id: 2, type: 'derivative', field: 1 }; - const metrics = [ + test('returns formatted label for pipeline aggs uses alias for field metric', () => { + const metric = ({ id: 2, type: 'derivative', field: 1 } as unknown) as MetricsItemsSchema; + const metrics = ([ { id: 1, type: 'max', field: 'network.out.bytes', alias: 'Outbound Traffic' }, metric, - ]; + ] as unknown) as MetricsItemsSchema[]; const label = calculateLabel(metric, metrics); + expect(label).toEqual('Derivative of Outbound Traffic'); }); }); diff --git a/src/plugins/vis_type_timeseries/common/calculate_label.js b/src/plugins/vis_type_timeseries/common/calculate_label.ts similarity index 84% rename from src/plugins/vis_type_timeseries/common/calculate_label.js rename to src/plugins/vis_type_timeseries/common/calculate_label.ts index 96e9fa0825b25..33a1fbe6879ae 100644 --- a/src/plugins/vis_type_timeseries/common/calculate_label.js +++ b/src/plugins/vis_type_timeseries/common/calculate_label.ts @@ -18,8 +18,9 @@ */ import { includes, startsWith } from 'lodash'; -import { lookup } from './agg_lookup'; import { i18n } from '@kbn/i18n'; +import { lookup } from './agg_lookup'; +import { MetricsItemsSchema, SanitizedFieldType } from './types'; const paths = [ 'cumulative_sum', @@ -36,7 +37,15 @@ const paths = [ 'positive_only', ]; -export function calculateLabel(metric, metrics) { +export const extractFieldLabel = (fields: SanitizedFieldType[], name: string) => { + return fields.find((f) => f.name === name)?.label ?? name; +}; + +export const calculateLabel = ( + metric: MetricsItemsSchema, + metrics: MetricsItemsSchema[] = [], + fields: SanitizedFieldType[] = [] +): string => { if (!metric) { return i18n.translate('visTypeTimeseries.calculateLabel.unknownLabel', { defaultMessage: 'Unknown', @@ -73,7 +82,7 @@ export function calculateLabel(metric, metrics) { if (metric.type === 'positive_rate') { return i18n.translate('visTypeTimeseries.calculateLabel.positiveRateLabel', { defaultMessage: 'Counter Rate of {field}', - values: { field: metric.field }, + values: { field: extractFieldLabel(fields, metric.field!) }, }); } if (metric.type === 'static') { @@ -84,15 +93,15 @@ export function calculateLabel(metric, metrics) { } if (includes(paths, metric.type)) { - const targetMetric = metrics.find((m) => startsWith(metric.field, m.id)); - const targetLabel = calculateLabel(targetMetric, metrics); + const targetMetric = metrics.find((m) => startsWith(metric.field!, m.id)); + const targetLabel = calculateLabel(targetMetric!, metrics, fields); // For percentiles we need to parse the field id to extract the percentile // the user configured in the percentile aggregation and specified in the // submetric they selected. This applies only to pipeline aggs. if (targetMetric && targetMetric.type === 'percentile') { const percentileValueMatch = /\[([0-9\.]+)\]$/; - const matches = metric.field.match(percentileValueMatch); + const matches = metric.field!.match(percentileValueMatch); if (matches) { return i18n.translate( 'visTypeTimeseries.calculateLabel.lookupMetricTypeOfTargetWithAdditionalLabel', @@ -115,6 +124,9 @@ export function calculateLabel(metric, metrics) { return i18n.translate('visTypeTimeseries.calculateLabel.lookupMetricTypeOfMetricFieldRankLabel', { defaultMessage: '{lookupMetricType} of {metricField}', - values: { lookupMetricType: lookup[metric.type], metricField: metric.field }, + values: { + lookupMetricType: lookup[metric.type], + metricField: extractFieldLabel(fields, metric.field!), + }, }); -} +}; diff --git a/src/plugins/vis_type_timeseries/common/extract_index_patterns.test.js b/src/plugins/vis_type_timeseries/common/extract_index_patterns.test.ts similarity index 73% rename from src/plugins/vis_type_timeseries/common/extract_index_patterns.test.js rename to src/plugins/vis_type_timeseries/common/extract_index_patterns.test.ts index 385c0b58d12fd..69cbd9c3fe0b6 100644 --- a/src/plugins/vis_type_timeseries/common/extract_index_patterns.test.js +++ b/src/plugins/vis_type_timeseries/common/extract_index_patterns.test.ts @@ -18,16 +18,13 @@ */ import { extractIndexPatterns } from './extract_index_patterns'; +import { PanelSchema } from './types'; describe('extractIndexPatterns(vis)', () => { - let visParams; - let visFields; + let panel: PanelSchema; beforeEach(() => { - visFields = { - '*': [], - }; - visParams = { + panel = { index_pattern: '*', series: [ { @@ -40,25 +37,10 @@ describe('extractIndexPatterns(vis)', () => { }, ], annotations: [{ index_pattern: 'notes-*' }, { index_pattern: 'example-1-*' }], - }; + } as PanelSchema; }); test('should return index patterns', () => { - visFields = {}; - - expect(extractIndexPatterns(visParams, visFields)).toEqual([ - '*', - 'example-1-*', - 'example-2-*', - 'notes-*', - ]); - }); - - test('should return index patterns that do not exist in visFields', () => { - expect(extractIndexPatterns(visParams, visFields)).toEqual([ - 'example-1-*', - 'example-2-*', - 'notes-*', - ]); + expect(extractIndexPatterns(panel, '')).toEqual(['*', 'example-1-*', 'example-2-*', 'notes-*']); }); }); diff --git a/src/plugins/vis_type_timeseries/common/extract_index_patterns.js b/src/plugins/vis_type_timeseries/common/extract_index_patterns.ts similarity index 73% rename from src/plugins/vis_type_timeseries/common/extract_index_patterns.js rename to src/plugins/vis_type_timeseries/common/extract_index_patterns.ts index 3fb005b477199..ba30f6da02321 100644 --- a/src/plugins/vis_type_timeseries/common/extract_index_patterns.js +++ b/src/plugins/vis_type_timeseries/common/extract_index_patterns.ts @@ -17,17 +17,21 @@ * under the License. */ import { uniq } from 'lodash'; +import { PanelSchema } from '../common/types'; -export function extractIndexPatterns(panel, excludedFields = {}) { - const patterns = []; +export function extractIndexPatterns( + panel: PanelSchema, + defaultIndex?: PanelSchema['default_index_pattern'] +) { + const patterns: string[] = []; - if (!excludedFields[panel.index_pattern]) { + if (panel.index_pattern) { patterns.push(panel.index_pattern); } panel.series.forEach((series) => { const indexPattern = series.series_index_pattern; - if (indexPattern && series.override_index_pattern && !excludedFields[indexPattern]) { + if (indexPattern && series.override_index_pattern) { patterns.push(indexPattern); } }); @@ -35,15 +39,15 @@ export function extractIndexPatterns(panel, excludedFields = {}) { if (panel.annotations) { panel.annotations.forEach((item) => { const indexPattern = item.index_pattern; - if (indexPattern && !excludedFields[indexPattern]) { + if (indexPattern) { patterns.push(indexPattern); } }); } - if (patterns.length === 0) { - patterns.push(''); + if (patterns.length === 0 && defaultIndex) { + patterns.push(defaultIndex); } - return uniq(patterns).sort(); + return uniq(patterns).sort(); } diff --git a/src/plugins/vis_type_timeseries/common/field_types.ts b/src/plugins/vis_type_timeseries/common/field_types.ts new file mode 100644 index 0000000000000..d12d4fe831659 --- /dev/null +++ b/src/plugins/vis_type_timeseries/common/field_types.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export enum FIELD_TYPES { + BOOLEAN = 'boolean', + DATE = 'date', + GEO = 'geo_point', + NUMBER = 'number', + STRING = 'string', +} diff --git a/src/plugins/vis_type_timeseries/common/model_options.ts b/src/plugins/vis_type_timeseries/common/model_options.ts new file mode 100644 index 0000000000000..1eefc92e7615a --- /dev/null +++ b/src/plugins/vis_type_timeseries/common/model_options.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export enum MODEL_TYPES { + UNWEIGHTED = 'simple', + WEIGHTED_EXPONENTIAL = 'ewma', + WEIGHTED_EXPONENTIAL_DOUBLE = 'holt', + WEIGHTED_EXPONENTIAL_TRIPLE = 'holt_winters', + WEIGHTED_LINEAR = 'linear', +} diff --git a/src/plugins/vis_type_timeseries/common/timerange_data_modes.js b/src/plugins/vis_type_timeseries/common/timerange_data_modes.ts similarity index 91% rename from src/plugins/vis_type_timeseries/common/timerange_data_modes.js rename to src/plugins/vis_type_timeseries/common/timerange_data_modes.ts index 7d69d36f213b7..7c8755baf3164 100644 --- a/src/plugins/vis_type_timeseries/common/timerange_data_modes.js +++ b/src/plugins/vis_type_timeseries/common/timerange_data_modes.ts @@ -22,19 +22,19 @@ * @constant * @public */ -export const TIME_RANGE_DATA_MODES = { +export enum TIME_RANGE_DATA_MODES { /** * Entire timerange mode will match all the documents selected in the * timerange timepicker */ - ENTIRE_TIME_RANGE: 'entire_time_range', + ENTIRE_TIME_RANGE = 'entire_time_range', /** * Last value mode will match only the documents for the specified interval * from the end of the timerange. */ - LAST_VALUE: 'last_value', -}; + LAST_VALUE = 'last_value', +} /** * Key for getting the Time Range mode from the Panel configuration object. diff --git a/src/plugins/vis_type_timeseries/common/to_percentile_number.js b/src/plugins/vis_type_timeseries/common/to_percentile_number.ts similarity index 94% rename from src/plugins/vis_type_timeseries/common/to_percentile_number.js rename to src/plugins/vis_type_timeseries/common/to_percentile_number.ts index b81133db8f084..51d60b99089eb 100644 --- a/src/plugins/vis_type_timeseries/common/to_percentile_number.js +++ b/src/plugins/vis_type_timeseries/common/to_percentile_number.ts @@ -18,5 +18,5 @@ */ const percentileNumberTest = /\d+\.\d+/; -export const toPercentileNumber = (value) => +export const toPercentileNumber = (value: string) => percentileNumberTest.test(`${value}`) ? value : `${value}.0`; diff --git a/src/plugins/vis_type_timeseries/common/types.ts b/src/plugins/vis_type_timeseries/common/types.ts index f8e1b740fc646..754a338811df9 100644 --- a/src/plugins/vis_type_timeseries/common/types.ts +++ b/src/plugins/vis_type_timeseries/common/types.ts @@ -18,7 +18,7 @@ */ import { TypeOf } from '@kbn/config-schema'; -import { metricsItems, panel, seriesItems, visPayloadSchema } from './vis_schema'; +import { metricsItems, panel, seriesItems, visPayloadSchema, fieldObject } from './vis_schema'; import { PANEL_TYPES } from './panel_types'; import { TimeseriesUIRestrictions } from './ui_restrictions'; @@ -26,6 +26,7 @@ export type SeriesItemsSchema = TypeOf; export type MetricsItemsSchema = TypeOf; export type PanelSchema = TypeOf; export type VisPayload = TypeOf; +export type FieldObject = TypeOf; interface PanelData { id: string; @@ -53,3 +54,9 @@ export type TimeseriesVisData = SeriesData & { */ series?: unknown[]; }; + +export interface SanitizedFieldType { + name: string; + type: string; + label?: string; +} diff --git a/src/plugins/vis_type_timeseries/common/vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts index a90fa752ad7dc..40198ab98026e 100644 --- a/src/plugins/vis_type_timeseries/common/vis_schema.ts +++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts @@ -47,6 +47,8 @@ const numberOptionalOrEmptyString = schema.maybe( schema.oneOf([numberOptional, schema.literal('')]) ); +export const fieldObject = stringOptionalNullable; + const annotationsItems = schema.object({ color: stringOptionalNullable, fields: stringOptionalNullable, @@ -58,7 +60,7 @@ const annotationsItems = schema.object({ index_pattern: stringOptionalNullable, query_string: schema.maybe(queryObject), template: stringOptionalNullable, - time_field: stringOptionalNullable, + time_field: fieldObject, }); const backgroundColorRulesItems = schema.object({ @@ -77,8 +79,9 @@ const gaugeColorRulesItems = schema.object({ value: schema.maybe(schema.nullable(schema.number())), }); export const metricsItems = schema.object({ - field: stringOptionalNullable, + field: fieldObject, id: stringRequired, + alias: stringOptionalNullable, metric_agg: stringOptionalNullable, numerator: schema.maybe(queryObject), denominator: schema.maybe(queryObject), @@ -98,7 +101,7 @@ export const metricsItems = schema.object({ variables: schema.maybe( schema.arrayOf( schema.object({ - field: stringOptionalNullable, + field: fieldObject, id: stringRequired, name: stringOptionalNullable, }) @@ -109,7 +112,7 @@ export const metricsItems = schema.object({ schema.arrayOf( schema.object({ id: stringRequired, - field: stringOptionalNullable, + field: fieldObject, mode: schema.oneOf([schema.literal('line'), schema.literal('band')]), shade: schema.oneOf([numberOptional, stringOptionalNullable]), value: schema.maybe(schema.oneOf([numberOptional, stringOptionalNullable])), @@ -123,7 +126,7 @@ export const metricsItems = schema.object({ size: stringOrNumberOptionalNullable, agg_with: stringOptionalNullable, order: stringOptionalNullable, - order_by: stringOptionalNullable, + order_by: fieldObject, }); const splitFiltersItems = schema.object({ @@ -134,7 +137,7 @@ const splitFiltersItems = schema.object({ }); export const seriesItems = schema.object({ - aggregate_by: stringOptionalNullable, + aggregate_by: fieldObject, aggregate_function: stringOptionalNullable, axis_position: stringRequired, axis_max: stringOrNumberOptionalNullable, @@ -176,7 +179,7 @@ export const seriesItems = schema.object({ seperate_axis: numberIntegerOptional, series_index_pattern: stringOptionalNullable, series_max_bars: numberIntegerOptional, - series_time_field: stringOptionalNullable, + series_time_field: fieldObject, series_interval: stringOptionalNullable, series_drop_last_bucket: numberIntegerOptional, split_color_mode: stringOptionalNullable, @@ -184,7 +187,7 @@ export const seriesItems = schema.object({ split_mode: stringRequired, stacked: stringRequired, steps: numberIntegerOptional, - terms_field: stringOptionalNullable, + terms_field: fieldObject, terms_order_by: stringOptionalNullable, terms_size: stringOptionalNullable, terms_direction: stringOptionalNullable, @@ -241,7 +244,7 @@ export const panel = schema.object({ markdown_vertical_align: stringOptionalNullable, markdown_less: stringOptionalNullable, markdown_css: stringOptionalNullable, - pivot_id: stringOptionalNullable, + pivot_id: fieldObject, pivot_label: stringOptionalNullable, pivot_type: stringOptionalNullable, pivot_rows: stringOptionalNullable, @@ -251,7 +254,7 @@ export const panel = schema.object({ tooltip_mode: schema.maybe( schema.oneOf([schema.literal('show_all'), schema.literal('show_focused')]) ), - time_field: stringOptionalNullable, + time_field: fieldObject, time_range_mode: stringOptionalNullable, type: schema.oneOf([ schema.literal('table'), diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx index e5236c3833b19..f027e52a9220d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx @@ -59,6 +59,10 @@ export function Agg(props: AggProps) { ...props.style, }; + const indexPattern = + (props.series.override_index_pattern && props.series.series_index_pattern) || + props.panel.index_pattern; + return (
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js index 5bf4fb55ee5e5..7d8e71f9e002d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js @@ -44,7 +44,7 @@ const checkModel = (model) => Array.isArray(model.variables) && model.script !== export function CalculationAgg(props) { const htmlId = htmlIdGenerator(); - const { siblings, model } = props; + const { siblings, model, indexPattern, fields } = props; const handleChange = createChangeHandler(props.onChange, model); const handleSelectChange = createSelectHandler(handleChange); @@ -97,6 +97,8 @@ export function CalculationAgg(props) { @@ -93,6 +95,7 @@ export function CumulativeSumAgg(props) { CumulativeSumAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, + indexPattern: PropTypes.string, model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js index fa1289dc74c72..c07e7ae024bb2 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js @@ -38,7 +38,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; export const DerivativeAgg = (props) => { - const { siblings } = props; + const { siblings, fields, indexPattern } = props; const defaults = { unit: '' }; const model = { ...defaults, ...props.model }; @@ -91,6 +91,7 @@ export const DerivativeAgg = (props) => { onChange={handleSelectChange('field')} metrics={siblings} metric={model} + fields={fields[indexPattern]} value={model.field} exclude={[METRIC_TYPES.TOP_HIT]} fullWidth @@ -120,6 +121,7 @@ export const DerivativeAgg = (props) => { DerivativeAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, + indexPattern: PropTypes.string, model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.js deleted file mode 100644 index b1ff749494b10..0000000000000 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.js +++ /dev/null @@ -1,115 +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 PropTypes from 'prop-types'; -import React from 'react'; -import { EuiComboBox } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; -import { isFieldEnabled } from '../../lib/check_ui_restrictions'; -import { i18n } from '@kbn/i18n'; - -const isFieldTypeEnabled = (fieldRestrictions, fieldType) => - fieldRestrictions.length ? fieldRestrictions.includes(fieldType) : true; - -function FieldSelectUi({ - type, - fields, - indexPattern, - value, - onChange, - disabled, - restrict, - placeholder, - uiRestrictions, - ...rest -}) { - if (type === 'count') { - return null; - } - - const selectedOptions = []; - const options = Object.values( - (fields[indexPattern] || []).reduce((acc, field) => { - if ( - isFieldTypeEnabled(restrict, field.type) && - isFieldEnabled(field.name, type, uiRestrictions) - ) { - const item = { - label: field.name, - value: field.name, - }; - - if (acc[field.type]) { - acc[field.type].options.push(item); - } else { - acc[field.type] = { - options: [item], - label: field.type, - }; - } - - if (value === item.value) { - selectedOptions.push(item); - } - } - - return acc; - }, {}) - ); - - if (onChange && value && !selectedOptions.length) { - onChange(); - } - - return ( - - ); -} - -FieldSelectUi.defaultProps = { - indexPattern: '', - disabled: false, - restrict: [], - placeholder: i18n.translate('visTypeTimeseries.fieldSelect.selectFieldPlaceholder', { - defaultMessage: 'Select field...', - }), -}; - -FieldSelectUi.propTypes = { - disabled: PropTypes.bool, - fields: PropTypes.object, - id: PropTypes.string, - indexPattern: PropTypes.string, - onChange: PropTypes.func, - restrict: PropTypes.array, - type: PropTypes.string, - value: PropTypes.string, - uiRestrictions: PropTypes.object, - placeholder: PropTypes.string, -}; - -export const FieldSelect = injectI18n(FieldSelectUi); diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx new file mode 100644 index 0000000000000..559db6dac8155 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx @@ -0,0 +1,139 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiComboBox, EuiComboBoxProps, EuiComboBoxOptionOption } from '@elastic/eui'; +import { METRIC_TYPES } from '../../../../common/metric_types'; + +import type { SanitizedFieldType } from '../../../../common/types'; +import type { TimeseriesUIRestrictions } from '../../../../common/ui_restrictions'; + +// @ts-ignore +import { isFieldEnabled } from '../../lib/check_ui_restrictions'; + +interface FieldSelectProps { + type: string; + fields: Record; + indexPattern: string; + value: string; + onChange: (options: Array>) => void; + disabled?: boolean; + restrict?: string[]; + placeholder?: string; + uiRestrictions?: TimeseriesUIRestrictions; + 'data-test-subj'?: string; +} + +const defaultPlaceholder = i18n.translate('visTypeTimeseries.fieldSelect.selectFieldPlaceholder', { + defaultMessage: 'Select field...', +}); + +const isFieldTypeEnabled = (fieldRestrictions: string[], fieldType: string) => + fieldRestrictions.length ? fieldRestrictions.includes(fieldType) : true; + +const sortByLabel = (a: EuiComboBoxOptionOption, b: EuiComboBoxOptionOption) => { + const getNormalizedString = (option: EuiComboBoxOptionOption) => + (option.label || '').toLowerCase(); + + return getNormalizedString(a).localeCompare(getNormalizedString(b)); +}; + +export function FieldSelect({ + type, + fields, + indexPattern = '', + value = '', + onChange, + disabled = false, + restrict = [], + placeholder = defaultPlaceholder, + uiRestrictions, + 'data-test-subj': dataTestSubj = 'metricsIndexPatternFieldsSelect', +}: FieldSelectProps) { + if (type === METRIC_TYPES.COUNT) { + return null; + } + + const selectedOptions: Array> = []; + let newPlaceholder = placeholder; + const groupedOptions: EuiComboBoxProps['options'] = Object.values( + (fields[indexPattern] || []).reduce>>( + (acc, field) => { + if (placeholder === field?.name) { + newPlaceholder = field.label ?? field.name; + } + + if ( + isFieldTypeEnabled(restrict, field.type) && + isFieldEnabled(field.name, type, uiRestrictions) + ) { + const item: EuiComboBoxOptionOption = { + value: field.name, + label: field.label ?? field.name, + }; + + const fieldTypeOptions = acc[field.type]?.options; + + if (fieldTypeOptions) { + fieldTypeOptions.push(item); + } else { + acc[field.type] = { + options: [item], + label: field.type, + }; + } + + if (value === item.value) { + selectedOptions.push(item); + } + } + + return acc; + }, + {} + ) + ); + + // sort groups + groupedOptions.sort(sortByLabel); + + // sort items + groupedOptions.forEach((group) => { + if (Array.isArray(group.options)) { + group.options.sort(sortByLabel); + } + }); + + if (value && !selectedOptions.length) { + onChange([]); + } + + return ( + + ); +} diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js index 1c7ab65ecd298..06887f67db84d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js @@ -24,6 +24,7 @@ import { FieldSelect } from './field_select'; import { AggRow } from './agg_row'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; + import { htmlIdGenerator, EuiFlexGroup, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js index 20fc88ba724bc..dd64d27fed918 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js @@ -42,7 +42,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; const checkModel = (model) => Array.isArray(model.variables) && model.script !== undefined; export function MathAgg(props) { - const { siblings, model } = props; + const { siblings, model, fields, indexPattern } = props; const htmlId = htmlIdGenerator(); const handleChange = createChangeHandler(props.onChange, model); @@ -95,6 +95,8 @@ export function MathAgg(props) { { - if (includes(exclude, metric.type)) return false; + if (exclude.includes(metric.type)) { + return false; + } switch (restrict) { case 'basic': - return includes(basicAggs, metric.type); + return basicAggs.includes(metric.type); default: return true; } @@ -55,21 +57,20 @@ export function filterRows(includeSiblings) { }; } -function MetricSelectUi(props) { +export function MetricSelect(props) { const { additionalOptions, restrict, metric, + fields, metrics, onChange, value, exclude, includeSiblings, clearable, - intl, ...rest } = props; - const calculatedMetrics = metrics.filter(createTypeFilter(restrict, exclude)); const siblings = calculateSiblings(calculatedMetrics, metric); @@ -80,7 +81,7 @@ function MetricSelectUi(props) { const percentileOptions = siblings .filter((row) => /^percentile/.test(row.type)) .reduce((acc, row) => { - const label = calculateLabel(row, calculatedMetrics); + const label = calculateLabel(row, calculatedMetrics, fields); switch (row.type) { case METRIC_TYPES.PERCENTILE_RANK: @@ -110,7 +111,7 @@ function MetricSelectUi(props) { }, []); const options = siblings.filter(filterRows(includeSiblings)).map((row) => { - const label = calculateLabel(row, calculatedMetrics); + const label = calculateLabel(row, calculatedMetrics, fields); return { value: row.id, label }; }); const allOptions = [...options, ...additionalOptions, ...percentileOptions]; @@ -122,8 +123,7 @@ function MetricSelectUi(props) { return ( type === MODEL_TYPES.WEIGHTED_EXPONENTIAL_TRIPLE && period * 2 > window; export const MovingAverageAgg = (props) => { - const { siblings } = props; + const { siblings, fields, indexPattern } = props; const model = { ...DEFAULTS, ...props.model }; const modelOptions = [ @@ -153,6 +153,7 @@ export const MovingAverageAgg = (props) => { onChange={handleSelectChange('field')} metrics={siblings} metric={model} + fields={fields[indexPattern]} value={model.field} exclude={[METRIC_TYPES.TOP_HIT]} /> @@ -315,6 +316,7 @@ export const MovingAverageAgg = (props) => { MovingAverageAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, + indexPattern: PropTypes.string, model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx index f78df9b1ddef4..9de0344f92cc6 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx @@ -40,8 +40,8 @@ import { createNumberHandler } from '../../lib/create_number_handler'; import { AggRow } from '../agg_row'; import { PercentileRankValues } from './percentile_rank_values'; -import { IFieldType, KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; -import { MetricsItemsSchema, PanelSchema, SeriesItemsSchema } from '../../../../../common/types'; +import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; +import { MetricsItemsSchema, PanelSchema, SanitizedFieldType } from '../../../../../common/types'; import { DragHandleProps } from '../../../../types'; import { PercentileHdr } from '../percentile_hdr'; @@ -49,10 +49,10 @@ const RESTRICT_FIELDS = [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM]; interface PercentileRankAggProps { disableDelete: boolean; - fields: IFieldType[]; + fields: Record; + indexPattern: string; model: MetricsItemsSchema; panel: PanelSchema; - series: SeriesItemsSchema; siblings: MetricsItemsSchema[]; dragHandleProps: DragHandleProps; onAdd(): void; @@ -61,12 +61,10 @@ interface PercentileRankAggProps { } export const PercentileRankAgg = (props: PercentileRankAggProps) => { - const { series, panel, fields } = props; + const { panel, fields, indexPattern } = props; const defaults = { values: [''] }; const model = { ...defaults, ...props.model }; - const indexPattern = - (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; const htmlId = htmlIdGenerator(); const isTablePanel = panel.type === 'table'; const handleChange = createChangeHandler(props.onChange, model); @@ -79,7 +77,6 @@ export const PercentileRankAgg = (props: PercentileRankAggProps) => { values, }); }; - return ( { type={model.type} restrict={RESTRICT_FIELDS} indexPattern={indexPattern} - value={model.field} + value={model.field ?? ''} onChange={handleSelectChange('field')} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js index 6ca5fa8e7447f..481a9cbd22ea0 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js @@ -36,7 +36,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; export const PositiveOnlyAgg = (props) => { - const { siblings } = props; + const { siblings, fields, indexPattern } = props; const defaults = { unit: '' }; const model = { ...defaults, ...props.model }; @@ -85,6 +85,7 @@ export const PositiveOnlyAgg = (props) => { onChange={handleSelectChange('field')} metrics={siblings} metric={model} + fields={fields[indexPattern]} value={model.field} exclude={[METRIC_TYPES.TOP_HIT]} /> @@ -98,6 +99,7 @@ export const PositiveOnlyAgg = (props) => { PositiveOnlyAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, + indexPattern: PropTypes.string, model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js index e3a0c74273539..31ad0c432bc2d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js @@ -37,7 +37,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; export const SerialDiffAgg = (props) => { - const { siblings } = props; + const { siblings, fields, indexPattern } = props; const defaults = { lag: '' }; const model = { ...defaults, ...props.model }; @@ -87,6 +87,7 @@ export const SerialDiffAgg = (props) => { onChange={handleSelectChange('field')} metrics={siblings} metric={model} + fields={fields[indexPattern]} value={model.field} exclude={[METRIC_TYPES.TOP_HIT]} /> @@ -125,6 +126,7 @@ export const SerialDiffAgg = (props) => { SerialDiffAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, + indexPattern: PropTypes.string, model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js index b882981f6b5c7..fb8b31834048a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js @@ -37,8 +37,10 @@ import { getSupportedFieldsByMetricType } from '../lib/get_supported_fields_by_m export function StandardAgg(props) { const { model, panel, series, fields, uiRestrictions } = props; + const handleChange = createChangeHandler(props.onChange, model); const handleSelectChange = createSelectHandler(handleChange); + const restrictFields = getSupportedFieldsByMetricType(model.type); const indexPattern = (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js index bed5e9caa9f87..456e03eeea1c9 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js @@ -40,7 +40,7 @@ import { import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; const StandardSiblingAggUi = (props) => { - const { siblings, intl } = props; + const { siblings, intl, fields, indexPattern } = props; const defaults = { sigma: '' }; const model = { ...defaults, ...props.model }; const htmlId = htmlIdGenerator(); @@ -158,6 +158,7 @@ const StandardSiblingAggUi = (props) => { onChange={handleSelectChange('field')} exclude={[METRIC_TYPES.PERCENTILE, METRIC_TYPES.TOP_HIT]} metrics={siblings} + fields={fields[indexPattern]} metric={model} value={model.field} /> @@ -173,6 +174,7 @@ const StandardSiblingAggUi = (props) => { StandardSiblingAggUi.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, + indexPattern: PropTypes.string, model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js index 15b02b067e353..bae839fe9ad11 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js @@ -70,6 +70,7 @@ export class CalculationVars extends Component { metrics={this.props.metrics} metric={this.props.model} value={row.field} + fields={this.props.fields[this.props.indexPattern]} includeSiblings={this.props.includeSiblings} exclude={this.props.exclude} /> @@ -105,6 +106,8 @@ CalculationVars.defaultProps = { }; CalculationVars.propTypes = { + fields: PropTypes.object, + indexPattern: PropTypes.string, metrics: PropTypes.array, model: PropTypes.object, name: PropTypes.string, diff --git a/src/plugins/vis_type_timeseries/public/application/components/annotations_editor.js b/src/plugins/vis_type_timeseries/public/application/components/annotations_editor.js index 3d38aa72fc271..dcdc2324c9b0a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/annotations_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/annotations_editor.js @@ -74,6 +74,7 @@ export class AnnotationsEditor extends Component { handleChange(_.assign({}, item, part)); }; } + handleQueryChange = (model, filter) => { const part = { query_string: filter }; collectionActions.handleChange(this.props, { diff --git a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js index e976519dfe635..ab234be28abea 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js +++ b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js @@ -78,10 +78,6 @@ export const IndexPattern = ({ allowLevelofDetail, }) => { const config = getUISettings(); - - const handleSelectChange = createSelectHandler(onChange); - const handleTextChange = createTextHandler(onChange); - const timeFieldName = `${prefix}time_field`; const indexPatternName = `${prefix}index_pattern`; const intervalName = `${prefix}interval`; @@ -100,6 +96,9 @@ export const IndexPattern = ({ [onChange, maxBarsName] ); + const handleSelectChange = createSelectHandler(onChange); + const handleTextChange = createTextHandler(onChange); + const timeRangeOptions = [ { label: i18n.translate('visTypeTimeseries.indexPattern.timeRange.lastValue', { @@ -119,7 +118,7 @@ export const IndexPattern = ({ const defaults = { default_index_pattern: '', - [indexPatternName]: '*', + [indexPatternName]: '', [intervalName]: AUTO_INTERVAL, [dropBucketName]: 1, [maxBarsName]: config.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), @@ -191,7 +190,7 @@ export const IndexPattern = ({ data-test-subj="metricsIndexPatternInput" disabled={disabled} placeholder={model.default_index_pattern} - onChange={handleTextChange(indexPatternName, '*')} + onChange={handleTextChange(indexPatternName)} value={model[indexPatternName]} /> @@ -204,7 +203,6 @@ export const IndexPattern = ({ })} > { + describe('createSelectHandler()', () => { + let handleChange: HandleChange; + let changeHandler: ReturnType; + + beforeEach(() => { + handleChange = jest.fn(); + changeHandler = createSelectHandler(handleChange); + }); + + test('should calls handleChange() function with the correct data', () => { + const fn = changeHandler('test'); + + fn([{ value: 'foo', label: 'foo' }]); + + expect(handleChange).toHaveBeenCalledWith({ + test: 'foo', + }); + }); + }); +}); diff --git a/src/plugins/vis_type_timeseries/common/model_options.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.ts similarity index 71% rename from src/plugins/vis_type_timeseries/common/model_options.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.ts index 7d01226bdc040..15f4e19702a7f 100644 --- a/src/plugins/vis_type_timeseries/common/model_options.test.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.ts @@ -16,13 +16,13 @@ * specific language governing permissions and limitations * under the License. */ +import { EuiComboBoxOptionOption } from '@elastic/eui'; -import { MODEL_TYPES } from './model_options'; +export type HandleChange = (partialModel: Record) => void; -describe('src/legacy/core_plugins/metrics/common/model_options.js', () => { - describe('MODEL_TYPES', () => { - test('should match a snapshot of constants', () => { - expect(MODEL_TYPES).toMatchSnapshot(); - }); +export const createSelectHandler = (handleChange: HandleChange) => (name: string) => ( + selected: EuiComboBoxOptionOption[] = [] +) => + handleChange?.({ + [name]: selected[0]?.value ?? null, }); -}); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js b/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js index b6b99d7782762..854d9bf5c59bc 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js @@ -17,7 +17,6 @@ * under the License. */ -import _ from 'lodash'; import { newMetricAggFn } from './new_metric_agg_fn'; import { isBasicAgg } from '../../../../common/agg_lookup'; import { handleAdd, handleChange } from './collection_actions'; @@ -30,8 +29,10 @@ export const seriesChangeHandler = (props, items) => (doc) => { handleAdd.call(null, props, () => { const metric = newMetricAggFn(); metric.type = doc.type; - const incompatPipelines = ['calculation', 'series_agg']; - if (!_.includes(incompatPipelines, doc.type)) metric.field = doc.id; + + if (!['calculation', 'series_agg'].includes(doc.type)) { + metric.field = doc.id; + } return metric; }); } else { diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js index ef7aec61a2f0d..9a59c3ece5849 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js @@ -46,7 +46,7 @@ const lessC = less(window, { env: 'production' }); import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { QueryBarWrapper } from '../query_bar_wrapper'; import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; -import { VisDataContext } from './../../contexts/vis_data_context'; +import { VisDataContext } from '../../contexts/vis_data_context'; class MarkdownPanelConfigUi extends Component { constructor(props) { diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js index b2ea90d6a87fe..16bc4b0631396 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js @@ -45,7 +45,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { QueryBarWrapper } from '../query_bar_wrapper'; import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; -import { VisDataContext } from './../../contexts/vis_data_context'; +import { VisDataContext } from '../../contexts/vis_data_context'; import { BUCKET_TYPES } from '../../../../common/metric_types'; export class TablePanelConfig extends Component { static contextType = VisDataContext; diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap index daff68e40dbae..09cd6d550fd96 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap +++ b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap @@ -43,7 +43,7 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js } labelType="label" > - - { this.setState({ @@ -191,6 +189,31 @@ export class VisEditor extends Component { } componentDidMount() { + const dataStart = getDataStart(); + + dataStart.indexPatterns.getDefault().then(async (index) => { + const defaultIndexTitle = index?.title ?? ''; + const indexPatterns = extractIndexPatterns(this.props.visParams, defaultIndexTitle); + + this.setState({ + model: { + ...this.props.visParams, + /** @legacy + * please use IndexPatterns service instead + * **/ + default_index_pattern: defaultIndexTitle, + /** @legacy + * please use IndexPatterns service instead + * **/ + default_timefield: index?.timeFieldName ?? '', + }, + dirty: false, + autoApply: true, + visFields: await fetchFields(indexPatterns), + extractedIndexPatterns: [''], + }); + }); + this.props.eventEmitter.on('updateEditor', this.updateModel); } @@ -207,10 +230,8 @@ VisEditor.defaultProps = { VisEditor.propTypes = { vis: PropTypes.object, visData: PropTypes.object, - visFields: PropTypes.object, renderComplete: PropTypes.func, config: PropTypes.object, - savedObj: PropTypes.object, timeRange: PropTypes.object, appState: PropTypes.object, }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js index a31be694cd172..46f266f631911 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js @@ -22,7 +22,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { RedirectAppLinks } from '../../../../../../kibana_react/public'; import { createTickFormatter } from '../../lib/tick_formatter'; -import { calculateLabel } from '../../../../../common/calculate_label'; import { isSortable } from './is_sortable'; import { EuiToolTip, EuiIcon } from '@elastic/eui'; import { replaceVars } from '../../lib/replace_vars'; @@ -30,8 +29,6 @@ import { fieldFormats } from '../../../../../../../plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; import { getFieldFormats, getCoreStart } from '../../../../services'; -import { METRIC_TYPES } from '../../../../../common/metric_types'; - function getColor(rules, colorKey, value) { let color; if (rules) { @@ -109,30 +106,19 @@ class TableVis extends Component { }; renderHeader() { - const { model, uiState, onUiState } = this.props; + const { model, uiState, onUiState, visData } = this.props; const stateKey = `${model.type}.sort`; const sort = uiState.get(stateKey, { column: '_default_', order: 'asc', }); - const calculateHeaderLabel = (metric, item) => { - const defaultLabel = item.label || calculateLabel(metric, item.metrics); - - switch (metric.type) { - case METRIC_TYPES.PERCENTILE: - return `${defaultLabel} (${last(metric.percentiles).value || 0})`; - case METRIC_TYPES.PERCENTILE_RANK: - return `${defaultLabel} (${last(metric.values) || 0})`; - default: - return defaultLabel; - } - }; + const calculateHeaderLabel = (metric, item) => + item.label || visData.series[0]?.series?.find((s) => item.id === s.id)?.label; const columns = this.visibleSeries.map((item) => { const metric = last(item.metrics); const label = calculateHeaderLabel(metric, item); - const handleClick = () => { if (!isSortable(metric)) return; let order; @@ -179,7 +165,7 @@ class TableVis extends Component { ); }); - const label = model.pivot_label || model.pivot_field || model.pivot_id; + const label = visData.pivot_label || model.pivot_label || model.pivot_id; let sortIcon; if (sort.column === '_default_') { sortIcon = sort.order === 'asc' ? 'sortUp' : 'sortDown'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js index 27891cdbb3943..8438b6899e835 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js @@ -20,8 +20,7 @@ import React from 'react'; import { getDisplayName } from './lib/get_display_name'; import { labelDateFormatter } from './lib/label_date_formatter'; -import { last, findIndex, first } from 'lodash'; -import { calculateLabel } from '../../../common/calculate_label'; +import { findIndex, first } from 'lodash'; export function visWithSplits(WrappedComponent) { function SplitVisComponent(props) { @@ -35,8 +34,8 @@ export function visWithSplits(WrappedComponent) { const [seriesId, splitId] = series.id.split(':'); const seriesModel = model.series.find((s) => s.id === seriesId); if (!seriesModel || !splitId) return acc; - const metric = last(seriesModel.metrics); - const label = calculateLabel(metric, seriesModel.metrics); + + const label = series.splitByLabel; if (!acc[splitId]) { acc[splitId] = { @@ -102,6 +101,7 @@ export function visWithSplits(WrappedComponent) { return
{rows}
; } + SplitVisComponent.displayName = `SplitVisComponent(${getDisplayName(WrappedComponent)})`; return SplitVisComponent; } diff --git a/src/plugins/vis_type_timeseries/public/application/editor_controller.js b/src/plugins/vis_type_timeseries/public/application/editor_controller.js index 548bf2623fc1a..e147f4be6c46f 100644 --- a/src/plugins/vis_type_timeseries/public/application/editor_controller.js +++ b/src/plugins/vis_type_timeseries/public/application/editor_controller.js @@ -19,8 +19,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { fetchIndexPatternFields } from './lib/fetch_fields'; -import { getSavedObjectsClient, getUISettings, getI18n } from '../services'; +import { getUISettings, getI18n } from '../services'; import { VisEditor } from './components/vis_editor_lazy'; export class EditorController { @@ -31,42 +30,18 @@ export class EditorController { this.eventEmitter = eventEmitter; this.state = { - fields: [], vis: vis, - isLoaded: false, }; } - fetchDefaultIndexPattern = async () => { - const indexPattern = await getSavedObjectsClient().client.get( - 'index-pattern', - getUISettings().get('defaultIndex') - ); - - return indexPattern.attributes; - }; - - fetchDefaultParams = async () => { - const { title, timeFieldName } = await this.fetchDefaultIndexPattern(); - - this.state.vis.params.default_index_pattern = title; - this.state.vis.params.default_timefield = timeFieldName; - this.state.fields = await fetchIndexPatternFields(this.state.vis); - - this.state.isLoaded = true; - }; - async render(params) { const I18nContext = getI18n().Context; - !this.state.isLoaded && (await this.fetchDefaultParams()); - render( {}} diff --git a/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.js b/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts similarity index 64% rename from src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.js rename to src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts index a32ab71f36357..ae78178af957e 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.js +++ b/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts @@ -17,31 +17,43 @@ * under the License. */ import { i18n } from '@kbn/i18n'; -import { extractIndexPatterns } from '../../../common/extract_index_patterns'; -import { getCoreStart } from '../../services'; +import { getCoreStart, getDataStart } from '../../services'; import { ROUTES } from '../../../common/constants'; +import { SanitizedFieldType } from '../../../common/types'; + +export async function fetchFields( + indexes: string[] = [], + signal?: AbortSignal +): Promise> { + const patterns = Array.isArray(indexes) ? indexes : [indexes]; + const coreStart = getCoreStart(); + const dataStart = getDataStart(); -export async function fetchFields(indexPatterns = [], signal) { - const patterns = Array.isArray(indexPatterns) ? indexPatterns : [indexPatterns]; try { + const defaultIndexPattern = await dataStart.indexPatterns.getDefault(); const indexFields = await Promise.all( - patterns.map((pattern) => - getCoreStart().http.get(ROUTES.FIELDS, { + patterns.map(async (pattern) => { + return coreStart.http.get(ROUTES.FIELDS, { query: { index: pattern, }, signal, - }) - ) + }); + }) ); - return patterns.reduce( + const fields: Record = patterns.reduce( (cumulatedFields, currentPattern, index) => ({ ...cumulatedFields, [currentPattern]: indexFields[index], }), {} ); + + if (defaultIndexPattern?.title && patterns.includes(defaultIndexPattern.title)) { + fields[''] = fields[defaultIndexPattern.title]; + } + return fields; } catch (error) { if (error.name !== 'AbortError') { getCoreStart().notifications.toasts.addDanger({ @@ -52,11 +64,5 @@ export async function fetchFields(indexPatterns = [], signal) { }); } } - return []; -} - -export async function fetchIndexPatternFields({ params, fields = {} }) { - const indexPatterns = extractIndexPatterns(params, fields); - - return await fetchFields(indexPatterns); + return {}; } diff --git a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts index 8f87318222f2b..d15332b26701c 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts @@ -16,24 +16,31 @@ * specific language governing permissions and limitations * under the License. */ -import { uniqBy, get } from 'lodash'; +import { uniqBy } from 'lodash'; import { first, map } from 'rxjs/operators'; import { KibanaRequest, RequestHandlerContext } from 'kibana/server'; import { Framework } from '../plugin'; -import { - indexPatterns, - IndexPatternFieldDescriptor, - IndexPatternsFetcher, -} from '../../../data/server'; +import { IndexPatternsFetcher } from '../../../data/server'; import { ReqFacade } from './search_strategies/strategies/abstract_search_strategy'; export async function getFields( requestContext: RequestHandlerContext, request: KibanaRequest, framework: Framework, - indexPattern: string + indexPatternString: string ) { + const getIndexPatternsService = async () => { + const [, { data }] = await framework.core.getStartServices(); + + return await data.indexPatterns.indexPatternsServiceFactory( + requestContext.core.savedObjects.client, + requestContext.core.elasticsearch.client.asCurrentUser + ); + }; + + const indexPatternsService = await getIndexPatternsService(); + // NOTE / TODO: This facade has been put in place to make migrating to the New Platform easier. It // removes the need to refactor many layers of dependencies on "req", and instead just augments the top // level object passed from here. The layers should be refactored fully at some point, but for now @@ -44,7 +51,7 @@ export async function getFields( framework, payload: {}, pre: { - indexPatternsService: new IndexPatternsFetcher( + indexPatternsFetcher: new IndexPatternsFetcher( requestContext.core.elasticsearch.client.asCurrentUser ), }, @@ -58,19 +65,13 @@ export async function getFields( ) .toPromise(); }, + getIndexPatternsService: async () => indexPatternsService, }; - let indexPatternString = indexPattern; if (!indexPatternString) { - const [{ savedObjects, elasticsearch }, { data }] = await framework.core.getStartServices(); - const savedObjectsClient = savedObjects.getScopedClient(request); - const clusterClient = elasticsearch.client.asScoped(request).asCurrentUser; - const indexPatternsService = await data.indexPatterns.indexPatternsServiceFactory( - savedObjectsClient, - clusterClient - ); const defaultIndexPattern = await indexPatternsService.getDefault(); - indexPatternString = get(defaultIndexPattern, 'title', ''); + + indexPatternString = defaultIndexPattern?.title ?? ''; } const { @@ -78,12 +79,10 @@ export async function getFields( capabilities, } = (await framework.searchStrategyRegistry.getViableStrategy(reqFacade, indexPatternString))!; - const fields = ((await searchStrategy.getFieldsForWildcard( + const fields = await searchStrategy.getFieldsForWildcard( reqFacade, indexPatternString, capabilities - )) as IndexPatternFieldDescriptor[]).filter( - (field) => field.aggregatable && !indexPatterns.isNestedField(field) ); return uniqBy(fields, (field) => field.name); diff --git a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts index aefbe0ea78d4b..d450bb205c9ce 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts @@ -71,6 +71,14 @@ export function getVisData( ) .toPromise(); }, + getIndexPatternsService: async () => { + const [, { data }] = await framework.core.getStartServices(); + + return await data.indexPatterns.indexPatternsServiceFactory( + requestContext.core.savedObjects.client, + requestContext.core.elasticsearch.client.asCurrentUser + ); + }, }; const promises = reqFacade.payload.panels.map(getPanelData(reqFacade)); return Promise.all(promises).then((res) => { diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategy_registry.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategy_registry.ts index 6fbed1ddfba0f..afe598393ab92 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategy_registry.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategy_registry.ts @@ -24,7 +24,8 @@ import { DefaultSearchStrategy } from './strategies/default_search_strategy'; import { extractIndexPatterns } from '../../../common/extract_index_patterns'; export type RequestFacade = any; -export type Panel = any; + +import { PanelSchema } from '../../../common/types'; export class SearchStrategyRegistry { private strategies: AbstractSearchStrategy[] = []; @@ -53,8 +54,8 @@ export class SearchStrategyRegistry { } } - async getViableStrategyForPanel(req: RequestFacade, panel: Panel) { - const indexPattern = extractIndexPatterns(panel).join(','); + async getViableStrategyForPanel(req: RequestFacade, panel: PanelSchema) { + const indexPattern = extractIndexPatterns(panel, panel.default_index_pattern).join(','); return this.getViableStrategy(req, indexPattern); } diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js index 2c38e883cd69f..3e3e654617e76 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js @@ -26,14 +26,17 @@ describe('AbstractSearchStrategy', () => { let indexPattern; beforeEach(() => { - mockedFields = {}; + mockedFields = []; req = { payload: {}, pre: { - indexPatternsService: { + indexPatternsFetcher: { getFieldsForWildcard: jest.fn().mockReturnValue(mockedFields), }, }, + getIndexPatternsService: jest.fn(() => ({ + find: jest.fn(() => []), + })), }; abstractSearchStrategy = new AbstractSearchStrategy(); @@ -48,9 +51,10 @@ describe('AbstractSearchStrategy', () => { test('should return fields for wildcard', async () => { const fields = await abstractSearchStrategy.getFieldsForWildcard(req, indexPattern); - expect(fields).toBe(mockedFields); - expect(req.pre.indexPatternsService.getFieldsForWildcard).toHaveBeenCalledWith({ + expect(fields).toEqual(mockedFields); + expect(req.pre.indexPatternsFetcher.getFieldsForWildcard).toHaveBeenCalledWith({ pattern: indexPattern, + metaFields: [], fieldCapsOptions: { allow_no_indices: true }, }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts index 71461d319f2b6..e27241c5eef62 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts @@ -17,16 +17,19 @@ * under the License. */ -import { +import type { RequestHandlerContext, FakeRequest, IUiSettingsClient, SavedObjectsClientContract, } from 'kibana/server'; -import { Framework } from '../../../plugin'; -import { IndexPatternsFetcher } from '../../../../../data/server'; -import { VisPayload } from '../../../../common/types'; +import type { Framework } from '../../../plugin'; +import type { IndexPatternsFetcher, IFieldType } from '../../../../../data/server'; +import type { VisPayload } from '../../../../common/types'; +import type { IndexPatternsService } from '../../../../../data/common'; +import { indexPatterns } from '../../../../../data/server'; +import { SanitizedFieldType } from '../../../../common/types'; /** * ReqFacade is a regular KibanaRequest object extended with additional service @@ -39,13 +42,27 @@ export interface ReqFacade extends FakeRequest { framework: Framework; payload: T; pre: { - indexPatternsService?: IndexPatternsFetcher; + indexPatternsFetcher?: IndexPatternsFetcher; }; getUiSettingsService: () => IUiSettingsClient; getSavedObjectsClient: () => SavedObjectsClientContract; getEsShardTimeout: () => Promise; + getIndexPatternsService: () => Promise; } +const toSanitizedFieldType = (fields: IFieldType[]) => { + return fields + .filter((field) => field.aggregatable && !indexPatterns.isNestedField(field)) + .map( + (field: IFieldType) => + ({ + name: field.name, + label: field.customLabel ?? field.name, + type: field.type, + } as SanitizedFieldType) + ); +}; + export abstract class AbstractSearchStrategy { async search(req: ReqFacade, bodies: any[], indexType?: string) { const requests: any[] = []; @@ -81,13 +98,27 @@ export abstract class AbstractSearchStrategy { async getFieldsForWildcard( req: ReqFacade, indexPattern: string, - capabilities?: unknown + capabilities?: unknown, + options?: Partial<{ + type: string; + rollupIndex: string; + }> ) { - const { indexPatternsService } = req.pre; + const { indexPatternsFetcher } = req.pre; + const indexPatternsService = await req.getIndexPatternsService(); + const kibanaIndexPattern = (await indexPatternsService.find(indexPattern)).find( + (index) => index.title === indexPattern + ); - return await indexPatternsService!.getFieldsForWildcard({ - pattern: indexPattern, - fieldCapsOptions: { allow_no_indices: true }, - }); + return toSanitizedFieldType( + kibanaIndexPattern + ? kibanaIndexPattern.fields.getAll() + : await indexPatternsFetcher!.getFieldsForWildcard({ + pattern: indexPattern, + fieldCapsOptions: { allow_no_indices: true }, + metaFields: [], + ...options, + }) + ); } } diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts index e1f519456d373..0744b50c27ca2 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts @@ -30,4 +30,12 @@ export class DefaultSearchStrategy extends AbstractSearchStrategy { capabilities: new DefaultSearchCapabilities(req), }); } + + async getFieldsForWildcard( + req: ReqFacade, + indexPattern: string, + capabilities?: unknown + ) { + return super.getFieldsForWildcard(req, indexPattern, capabilities); + } } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/build_request_body.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/build_request_body.js index 9ea62e2700517..1e93967bdde9e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/build_request_body.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/build_request_body.js @@ -32,8 +32,8 @@ import { processors } from '../request_processors/annotations'; * ] * @returns {Object} doc - processed body */ -export function buildAnnotationRequest(...args) { +export async function buildAnnotationRequest(...args) { const processor = buildProcessorFunction(processors, ...args); - const doc = processor({}); + const doc = await processor({}); return doc; } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/get_request_params.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/get_request_params.js index 1b2334c7dea94..16099ccd6dba4 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/get_request_params.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/annotations/get_request_params.js @@ -19,7 +19,6 @@ import { buildAnnotationRequest } from './build_request_body'; import { getEsShardTimeout } from '../helpers/get_es_shard_timeout'; import { getIndexPatternObject } from '../helpers/get_index_pattern'; -import { UI_SETTINGS } from '../../../../../data/common'; export async function getAnnotationRequestParams( req, @@ -32,17 +31,14 @@ export async function getAnnotationRequestParams( const esShardTimeout = await getEsShardTimeout(req); const indexPattern = annotation.index_pattern; const { indexPatternObject, indexPatternString } = await getIndexPatternObject(req, indexPattern); - const request = buildAnnotationRequest( + const request = await buildAnnotationRequest( req, panel, annotation, esQueryConfig, indexPatternObject, capabilities, - { - maxBarsUiSettings: await uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), - barTargetUiSettings: await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), - } + uiSettings ); return { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js index 232efc7514a5a..f699a50db66af 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js @@ -45,11 +45,14 @@ export async function getSeriesData(req, panel) { ); const data = await searchStrategy.search(req, searches); - const handleResponseBodyFn = handleResponseBody(panel); + const handleResponseBodyFn = handleResponseBody(panel, req, searchStrategy, capabilities); - const series = data.map((resp) => - handleResponseBodyFn(resp.rawResponse ? resp.rawResponse : resp) + const series = await Promise.all( + data.map( + async (resp) => await handleResponseBodyFn(resp.rawResponse ? resp.rawResponse : resp) + ) ); + let annotations = null; if (panel.annotations && panel.annotations.length) { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js index eaaa5a9605b4b..a7b304e5e5866 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js @@ -16,13 +16,15 @@ * specific language governing permissions and limitations * under the License. */ + import { buildRequestBody } from './table/build_request_body'; import { handleErrorResponse } from './handle_error_response'; import { get } from 'lodash'; import { processBucket } from './table/process_bucket'; import { getEsQueryConfig } from './helpers/get_es_query_uisettings'; import { getIndexPatternObject } from './helpers/get_index_pattern'; -import { UI_SETTINGS } from '../../../../data/common'; +import { createFieldsFetcher } from './helpers/fields_fetcher'; +import { extractFieldLabel } from '../../../common/calculate_label'; export async function getTableData(req, panel) { const panelIndexPattern = panel.index_pattern; @@ -33,18 +35,33 @@ export async function getTableData(req, panel) { } = await req.framework.searchStrategyRegistry.getViableStrategy(req, panelIndexPattern); const esQueryConfig = await getEsQueryConfig(req); const { indexPatternObject } = await getIndexPatternObject(req, panelIndexPattern); + const extractFields = createFieldsFetcher(req, searchStrategy, capabilities); + + const calculatePivotLabel = async () => { + if (panel.pivot_id && indexPatternObject?.title) { + const fields = await extractFields(indexPatternObject.title); + + return extractFieldLabel(fields, panel.pivot_id); + } + return panel.pivot_id; + }; const meta = { type: panel.type, + pivot_label: panel.pivot_label || (await calculatePivotLabel()), uiRestrictions: capabilities.uiRestrictions, }; try { const uiSettings = req.getUiSettingsService(); - const body = buildRequestBody(req, panel, esQueryConfig, indexPatternObject, capabilities, { - maxBarsUiSettings: await uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), - barTargetUiSettings: await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), - }); + const body = await buildRequestBody( + req, + panel, + esQueryConfig, + indexPatternObject, + capabilities, + uiSettings + ); const [resp] = await searchStrategy.search(req, [ { @@ -59,9 +76,13 @@ export async function getTableData(req, panel) { [] ); + const series = await Promise.all( + buckets.map(processBucket(panel, req, searchStrategy, capabilities, extractFields)) + ); + return { ...meta, - series: buckets.map(processBucket(panel)), + series, }; } catch (err) { if (err.body || err.name === 'KQLSyntaxError') { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js index dc2936072165e..431b5fb83d831 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js @@ -128,7 +128,8 @@ export const bucketTransform = { }, }; if (bucket.order_by) { - set(body, 'aggs.docs.top_hits.sort', [{ [bucket.order_by]: { order: bucket.order } }]); + const orderField = bucket.order_by; + set(body, 'aggs.docs.top_hits.sort', [{ [orderField]: { order: bucket.order } }]); } return body; }, diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/fields_fetcher.ts similarity index 59% rename from src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.test.js rename to src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/fields_fetcher.ts index a8d5351341c17..6ee90eabf9f02 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/create_select_handler.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/fields_fetcher.ts @@ -17,23 +17,24 @@ * under the License. */ -import { createSelectHandler } from './create_select_handler'; +import { AbstractSearchStrategy, DefaultSearchCapabilities, ReqFacade } from '../../..'; -describe('createSelectHandler()', () => { - let handleChange; - let changeHandler; +export const createFieldsFetcher = ( + req: ReqFacade, + searchStrategy: AbstractSearchStrategy, + capabilities: DefaultSearchCapabilities +) => { + const fieldsCacheMap = new Map(); - beforeEach(() => { - handleChange = jest.fn(); - changeHandler = createSelectHandler(handleChange); - const fn = changeHandler('test'); - fn([{ value: 'foo' }]); - }); + return async (index: string) => { + if (fieldsCacheMap.has(index)) { + return fieldsCacheMap.get(index); + } - test('calls handleChange() function with partial', () => { - expect(handleChange.mock.calls.length).toEqual(1); - expect(handleChange.mock.calls[0][0]).toEqual({ - test: 'foo', - }); - }); -}); + const fields = await searchStrategy.getFieldsForWildcard(req, index, capabilities); + + fieldsCacheMap.set(index, fields); + + return fields; + }; +}; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js index 6c162e327fb21..e491a6795ba5d 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js @@ -27,7 +27,7 @@ import { formatKey } from './format_key'; const getTimeSeries = (resp, series) => _.get(resp, `aggregations.timeseries`) || _.get(resp, `aggregations.${series.id}.timeseries`); -export function getSplits(resp, panel, series, meta) { +export async function getSplits(resp, panel, series, meta, extractFields) { if (!meta) { meta = _.get(resp, `aggregations.${series.id}.meta`); } @@ -35,12 +35,17 @@ export function getSplits(resp, panel, series, meta) { const color = new Color(series.color); const metric = getLastMetric(series); const buckets = _.get(resp, `aggregations.${series.id}.buckets`); + + const fieldsForMetaIndex = meta.index ? await extractFields(meta.index) : []; + const splitByLabel = calculateLabel(metric, series.metrics, fieldsForMetaIndex); + if (buckets) { if (Array.isArray(buckets)) { const size = buckets.length; const colors = getSplitColors(series.color, size, series.split_color_mode); return buckets.map((bucket) => { bucket.id = `${series.id}:${bucket.key}`; + bucket.splitByLabel = splitByLabel; bucket.label = formatKey(bucket.key, series); bucket.labelFormatted = bucket.key_as_string ? formatKey(bucket.key_as_string, series) : ''; bucket.color = panel.type === 'top_n' ? color.string() : colors.shift(); @@ -72,10 +77,12 @@ export function getSplits(resp, panel, series, meta) { .forEach((m) => { mergeObj[m.id] = _.get(resp, `aggregations.${series.id}.${m.id}`); }); + return [ { id: series.id, - label: series.label || calculateLabel(metric, series.metrics), + splitByLabel, + label: series.label || splitByLabel, color: color.string(), ...mergeObj, meta, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js index 5ccd61a1c9102..dac269ed2f6c3 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js @@ -20,7 +20,7 @@ import { getSplits } from './get_splits'; describe('getSplits(resp, panel, series)', () => { - test('should return a splits for everything/filter group bys', () => { + test('should return a splits for everything/filter group bys', async () => { const resp = { aggregations: { SERIES: { @@ -40,19 +40,20 @@ describe('getSplits(resp, panel, series)', () => { { id: 'SIBAGG', type: 'avg_bucket', field: 'AVG' }, ], }; - expect(getSplits(resp, panel, series)).toEqual([ + expect(await getSplits(resp, panel, series, undefined)).toEqual([ { id: 'SERIES', label: 'Overall Average of Average of cpu', meta: { bucketSize: 10 }, color: 'rgb(255, 0, 0)', + splitByLabel: 'Overall Average of Average of cpu', timeseries: { buckets: [] }, SIBAGG: { value: 1 }, }, ]); }); - test('should return a splits for terms group bys for top_n', () => { + test('should return a splits for terms group bys for top_n', async () => { const resp = { aggregations: { SERIES: { @@ -84,7 +85,7 @@ describe('getSplits(resp, panel, series)', () => { ], }; const panel = { type: 'top_n' }; - expect(getSplits(resp, panel, series)).toEqual([ + expect(await getSplits(resp, panel, series)).toEqual([ { id: 'SERIES:example-01', key: 'example-01', @@ -92,6 +93,7 @@ describe('getSplits(resp, panel, series)', () => { labelFormatted: '', meta: { bucketSize: 10 }, color: 'rgb(255, 0, 0)', + splitByLabel: 'Overall Average of Average of cpu', timeseries: { buckets: [] }, SIBAGG: { value: 1 }, }, @@ -102,13 +104,14 @@ describe('getSplits(resp, panel, series)', () => { labelFormatted: '', meta: { bucketSize: 10 }, color: 'rgb(255, 0, 0)', + splitByLabel: 'Overall Average of Average of cpu', timeseries: { buckets: [] }, SIBAGG: { value: 2 }, }, ]); }); - test('should return a splits for terms group with label formatted by {{key}} placeholder', () => { + test('should return a splits for terms group with label formatted by {{key}} placeholder', async () => { const resp = { aggregations: { SERIES: { @@ -141,7 +144,7 @@ describe('getSplits(resp, panel, series)', () => { ], }; const panel = { type: 'top_n' }; - expect(getSplits(resp, panel, series)).toEqual([ + expect(await getSplits(resp, panel, series)).toEqual([ { id: 'SERIES:example-01', key: 'example-01', @@ -149,6 +152,7 @@ describe('getSplits(resp, panel, series)', () => { labelFormatted: '', meta: { bucketSize: 10 }, color: 'rgb(255, 0, 0)', + splitByLabel: 'Overall Average of Average of cpu', timeseries: { buckets: [] }, SIBAGG: { value: 1 }, }, @@ -159,13 +163,14 @@ describe('getSplits(resp, panel, series)', () => { labelFormatted: '', meta: { bucketSize: 10 }, color: 'rgb(255, 0, 0)', + splitByLabel: 'Overall Average of Average of cpu', timeseries: { buckets: [] }, SIBAGG: { value: 2 }, }, ]); }); - test('should return a splits for terms group with labelFormatted if {{key}} placeholder is applied and key_as_string exists', () => { + test('should return a splits for terms group with labelFormatted if {{key}} placeholder is applied and key_as_string exists', async () => { const resp = { aggregations: { SERIES: { @@ -200,7 +205,8 @@ describe('getSplits(resp, panel, series)', () => { ], }; const panel = { type: 'top_n' }; - expect(getSplits(resp, panel, series)).toEqual([ + + expect(await getSplits(resp, panel, series)).toEqual([ { id: 'SERIES:example-01', key: 'example-01', @@ -209,6 +215,7 @@ describe('getSplits(resp, panel, series)', () => { labelFormatted: '--false--', meta: { bucketSize: 10 }, color: 'rgb(255, 0, 0)', + splitByLabel: 'Overall Average of Average of cpu', timeseries: { buckets: [] }, SIBAGG: { value: 1 }, }, @@ -220,6 +227,7 @@ describe('getSplits(resp, panel, series)', () => { labelFormatted: '--true--', meta: { bucketSize: 10 }, color: 'rgb(255, 0, 0)', + splitByLabel: 'Overall Average of Average of cpu', timeseries: { buckets: [] }, SIBAGG: { value: 2 }, }, @@ -247,7 +255,7 @@ describe('getSplits(resp, panel, series)', () => { }, }; - test('should return a splits with no color', () => { + test('should return a splits with no color', async () => { const series = { id: 'SERIES', color: '#F00', @@ -260,7 +268,8 @@ describe('getSplits(resp, panel, series)', () => { ], }; const panel = { type: 'timeseries' }; - expect(getSplits(resp, panel, series)).toEqual([ + + expect(await getSplits(resp, panel, series)).toEqual([ { id: 'SERIES:example-01', key: 'example-01', @@ -268,6 +277,7 @@ describe('getSplits(resp, panel, series)', () => { labelFormatted: '', meta: { bucketSize: 10 }, color: undefined, + splitByLabel: 'Overall Average of Average of cpu', timeseries: { buckets: [] }, SIBAGG: { value: 1 }, }, @@ -278,13 +288,14 @@ describe('getSplits(resp, panel, series)', () => { labelFormatted: '', meta: { bucketSize: 10 }, color: undefined, + splitByLabel: 'Overall Average of Average of cpu', timeseries: { buckets: [] }, SIBAGG: { value: 2 }, }, ]); }); - test('should return gradient color', () => { + test('should return gradient color', async () => { const series = { id: 'SERIES', color: '#F00', @@ -298,7 +309,8 @@ describe('getSplits(resp, panel, series)', () => { ], }; const panel = { type: 'timeseries' }; - expect(getSplits(resp, panel, series)).toEqual([ + + expect(await getSplits(resp, panel, series)).toEqual([ expect.objectContaining({ color: 'rgb(255, 0, 0)', }), @@ -308,7 +320,7 @@ describe('getSplits(resp, panel, series)', () => { ]); }); - test('should return rainbow color', () => { + test('should return rainbow color', async () => { const series = { id: 'SERIES', color: '#F00', @@ -322,7 +334,8 @@ describe('getSplits(resp, panel, series)', () => { ], }; const panel = { type: 'timeseries' }; - expect(getSplits(resp, panel, series)).toEqual([ + + expect(await getSplits(resp, panel, series)).toEqual([ expect.objectContaining({ color: '#68BC00', }), @@ -333,7 +346,7 @@ describe('getSplits(resp, panel, series)', () => { }); }); - test('should return a splits for filters group bys', () => { + test('should return a splits for filters group bys', async () => { const resp = { aggregations: { SERIES: { @@ -360,7 +373,8 @@ describe('getSplits(resp, panel, series)', () => { metrics: [{ id: 'COUNT', type: 'count' }], }; const panel = { type: 'timeseries' }; - expect(getSplits(resp, panel, series)).toEqual([ + + expect(await getSplits(resp, panel, series)).toEqual([ { id: 'SERIES:filter-1', key: 'filter-1', diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js index 617a75f6bd59f..f32bacac82dac 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js @@ -20,7 +20,8 @@ import { overwrite } from '../../helpers'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { getTimerange } from '../../helpers/get_timerange'; -import { search } from '../../../../../../../plugins/data/server'; +import { search, UI_SETTINGS } from '../../../../../../../plugins/data/server'; + const { dateHistogramInterval } = search.aggs; export function dateHistogram( @@ -30,9 +31,10 @@ export function dateHistogram( esQueryConfig, indexPatternObject, capabilities, - { barTargetUiSettings } + uiSettings ) { - return (next) => (doc) => { + return (next) => async (doc) => { + const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); const timeField = annotation.time_field; const { bucketSize, intervalString } = getBucketSize( req, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/query.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/query.js index cf02f601ea5ff..ec6f52ca30a9c 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/query.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/query.js @@ -19,7 +19,7 @@ import { getBucketSize } from '../../helpers/get_bucket_size'; import { getTimerange } from '../../helpers/get_timerange'; -import { esQuery } from '../../../../../../data/server'; +import { esQuery, UI_SETTINGS } from '../../../../../../data/server'; export function query( req, @@ -28,9 +28,10 @@ export function query( esQueryConfig, indexPattern, capabilities, - { barTargetUiSettings } + uiSettings ) { - return (next) => (doc) => { + return (next) => async (doc) => { + const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); const timeField = annotation.time_field; const { bucketSize } = getBucketSize(req, 'auto', capabilities, barTargetUiSettings); const { from, to } = getTimerange(req); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/top_hits.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/top_hits.js index bdf2aac943b1b..2e1d9291d1c5f 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/top_hits.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/top_hits.js @@ -23,6 +23,7 @@ export function topHits(req, panel, annotation) { return (next) => (doc) => { const fields = (annotation.fields && annotation.fields.split(/[,\s]+/)) || []; const timeField = annotation.time_field; + overwrite(doc, `aggs.${annotation.id}.aggs.hits.top_hits`, { sort: [ { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js index 98c683bda1fdb..2133d39913180 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js @@ -22,7 +22,7 @@ import { getBucketSize } from '../../helpers/get_bucket_size'; import { offsetTime } from '../../offset_time'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { isLastValueTimerangeMode } from '../../helpers/get_timerange_mode'; -import { search } from '../../../../../../../plugins/data/server'; +import { search, UI_SETTINGS } from '../../../../../../../plugins/data/server'; const { dateHistogramInterval } = search.aggs; export function dateHistogram( @@ -32,9 +32,12 @@ export function dateHistogram( esQueryConfig, indexPatternObject, capabilities, - { maxBarsUiSettings, barTargetUiSettings } + uiSettings ) { - return (next) => (doc) => { + return (next) => async (doc) => { + const maxBarsUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS); + const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); + const { timeField, interval, maxBars } = getIntervalAndTimefield( panel, series, @@ -73,11 +76,10 @@ export function dateHistogram( ? getDateHistogramForLastBucketMode() : getDateHistogramForEntireTimerangeMode(); - // master - overwrite(doc, `aggs.${series.id}.meta`, { timeField, intervalString, + index: indexPatternObject?.title, bucketSize, seriesId: series.id, }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js index aa95a79a62796..05f2d5f39aa65 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js @@ -19,6 +19,7 @@ import { DefaultSearchCapabilities } from '../../../search_strategies/default_search_capabilities'; import { dateHistogram } from './date_histogram'; +import { UI_SETTINGS } from '../../../../../../data/common'; describe('dateHistogram(req, panel, series)', () => { let panel; @@ -51,20 +52,30 @@ describe('dateHistogram(req, panel, series)', () => { }; indexPatternObject = {}; capabilities = new DefaultSearchCapabilities(req); - uiSettings = { maxBarsUiSettings: 100, barTargetUiSettings: 50 }; + uiSettings = { + get: async (key) => (key === UI_SETTINGS.HISTOGRAM_MAX_BARS ? 100 : 50), + }; }); - test('calls next when finished', () => { + test('calls next when finished', async () => { const next = jest.fn(); - dateHistogram(req, panel, series, config, indexPatternObject, capabilities, uiSettings)(next)( - {} - ); + + await dateHistogram( + req, + panel, + series, + config, + indexPatternObject, + capabilities, + uiSettings + )(next)({}); + expect(next.mock.calls.length).toEqual(1); }); - test('returns valid date histogram', () => { + test('returns valid date histogram', async () => { const next = (doc) => doc; - const doc = dateHistogram( + const doc = await dateHistogram( req, panel, series, @@ -102,10 +113,10 @@ describe('dateHistogram(req, panel, series)', () => { }); }); - test('returns valid date histogram (offset by 1h)', () => { + test('returns valid date histogram (offset by 1h)', async () => { series.offset_time = '1h'; const next = (doc) => doc; - const doc = dateHistogram( + const doc = await dateHistogram( req, panel, series, @@ -143,13 +154,13 @@ describe('dateHistogram(req, panel, series)', () => { }); }); - test('returns valid date histogram with overridden index pattern', () => { + test('returns valid date histogram with overridden index pattern', async () => { series.override_index_pattern = 1; series.series_index_pattern = '*'; series.series_time_field = 'timestamp'; series.series_interval = '20s'; const next = (doc) => doc; - const doc = dateHistogram( + const doc = await dateHistogram( req, panel, series, @@ -188,12 +199,12 @@ describe('dateHistogram(req, panel, series)', () => { }); describe('dateHistogram for entire time range mode', () => { - test('should ignore entire range mode for timeseries', () => { + test('should ignore entire range mode for timeseries', async () => { panel.time_range_mode = 'entire_time_range'; panel.type = 'timeseries'; const next = (doc) => doc; - const doc = dateHistogram( + const doc = await dateHistogram( req, panel, series, @@ -207,11 +218,11 @@ describe('dateHistogram(req, panel, series)', () => { expect(doc.aggs.test.aggs.timeseries.date_histogram).toBeDefined(); }); - test('should returns valid date histogram for entire range mode', () => { + test('should returns valid date histogram for entire range mode', async () => { panel.time_range_mode = 'entire_time_range'; const next = (doc) => doc; - const doc = dateHistogram( + const doc = await dateHistogram( req, panel, series, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js index 6c1699912f76f..9e05a7631a9f5 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js @@ -17,11 +17,12 @@ * under the License. */ -const filter = (metric) => metric.type === 'filter_ratio'; import { bucketTransform } from '../../helpers/bucket_transform'; import { overwrite } from '../../helpers'; import { esQuery } from '../../../../../../data/server'; +const filter = (metric) => metric.type === 'filter_ratio'; + export function ratios(req, panel, series, esQueryConfig, indexPatternObject) { return (next) => (doc) => { if (series.metrics.some(filter)) { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js index 023ee054a5e13..7441ed0a274f2 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js @@ -20,6 +20,7 @@ import { overwrite } from '../../helpers'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { bucketTransform } from '../../helpers/bucket_transform'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; +import { UI_SETTINGS } from '../../../../../../data/common'; export function metricBuckets( req, @@ -28,9 +29,11 @@ export function metricBuckets( esQueryConfig, indexPatternObject, capabilities, - { barTargetUiSettings } + uiSettings ) { - return (next) => (doc) => { + return (next) => async (doc) => { + const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); + const { interval } = getIntervalAndTimefield(panel, series, indexPatternObject); const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js index 2154d2257815b..1789517c0f332 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js @@ -63,20 +63,20 @@ describe('metricBuckets(req, panel, series)', () => { {}, undefined, { - barTargetUiSettings: 50, + get: async () => 50, } ); }); - test('calls next when finished', () => { + test('calls next when finished', async () => { const next = jest.fn(); - metricBucketsProcessor(next)({}); + await metricBucketsProcessor(next)({}); expect(next.mock.calls.length).toEqual(1); }); - test('returns metric aggs', () => { + test('returns metric aggs', async () => { const next = (doc) => doc; - const doc = metricBucketsProcessor(next)({}); + const doc = await metricBucketsProcessor(next)({}); expect(doc).toEqual({ aggs: { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js index c16e0fd3aaf15..8e18ba725b003 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js @@ -21,6 +21,7 @@ import { getBucketSize } from '../../helpers/get_bucket_size'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { bucketTransform } from '../../helpers/bucket_transform'; import { overwrite } from '../../helpers'; +import { UI_SETTINGS } from '../../../../../../data/common'; export const filter = (metric) => metric.type === 'positive_rate'; @@ -29,7 +30,11 @@ export const createPositiveRate = (doc, intervalString, aggRoot) => (metric) => const derivativeFn = bucketTransform.derivative; const positiveOnlyFn = bucketTransform.positive_only; - const maxMetric = { id: `${metric.id}-positive-rate-max`, type: 'max', field: metric.field }; + const maxMetric = { + id: `${metric.id}-positive-rate-max`, + type: 'max', + field: metric.field, + }; const derivativeMetric = { id: `${metric.id}-positive-rate-derivative`, type: 'derivative', @@ -64,9 +69,11 @@ export function positiveRate( esQueryConfig, indexPatternObject, capabilities, - { barTargetUiSettings } + uiSettings ) { - return (next) => (doc) => { + return (next) => async (doc) => { + const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); + const { interval } = getIntervalAndTimefield(panel, series, indexPatternObject); const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js index d891fc01bb266..acec4b466f397 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js @@ -51,19 +51,21 @@ describe('positiveRate(req, panel, series)', () => { }, }; uiSettings = { - barTargetUiSettings: 50, + get: async () => 50, }; }); - test('calls next when finished', () => { + test('calls next when finished', async () => { const next = jest.fn(); - positiveRate(req, panel, series, {}, {}, undefined, uiSettings)(next)({}); + await positiveRate(req, panel, series, {}, {}, undefined, uiSettings)(next)({}); + expect(next.mock.calls.length).toEqual(1); }); - test('returns positive rate aggs', () => { + test('returns positive rate aggs', async () => { const next = (doc) => doc; - const doc = positiveRate(req, panel, series, {}, {}, undefined, uiSettings)(next)({}); + const doc = await positiveRate(req, panel, series, {}, {}, undefined, uiSettings)(next)({}); + expect(doc).toEqual({ aggs: { test: { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js index f69473b613d1b..b17d63c10877a 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js @@ -21,6 +21,7 @@ import { overwrite } from '../../helpers'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { bucketTransform } from '../../helpers/bucket_transform'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; +import { UI_SETTINGS } from '../../../../../../data/common'; export function siblingBuckets( req, @@ -29,9 +30,10 @@ export function siblingBuckets( esQueryConfig, indexPatternObject, capabilities, - { barTargetUiSettings } + uiSettings ) { - return (next) => (doc) => { + return (next) => async (doc) => { + const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); const { interval } = getIntervalAndTimefield(panel, series, indexPatternObject); const { bucketSize } = getBucketSize(req, interval, capabilities, barTargetUiSettings); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js index 48714e83341ea..367b6fa242b72 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js @@ -56,19 +56,19 @@ describe('siblingBuckets(req, panel, series)', () => { }, }; uiSettings = { - barTargetUiSettings: 50, + get: async () => 50, }; }); - test('calls next when finished', () => { + test('calls next when finished', async () => { const next = jest.fn(); - siblingBuckets(req, panel, series, {}, {}, undefined, uiSettings)(next)({}); + await siblingBuckets(req, panel, series, {}, {}, undefined, uiSettings)(next)({}); expect(next.mock.calls.length).toEqual(1); }); - test('returns sibling aggs', () => { + test('returns sibling aggs', async () => { const next = (doc) => doc; - const doc = siblingBuckets(req, panel, series, {}, {}, undefined, uiSettings)(next)({}); + const doc = await siblingBuckets(req, panel, series, {}, {}, undefined, uiSettings)(next)({}); expect(doc).toEqual({ aggs: { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js index 240a5d76b7c6a..6a8006f2f5fea 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js @@ -25,9 +25,12 @@ import { bucketTransform } from '../../helpers/bucket_transform'; export function splitByTerms(req, panel, series) { return (next) => (doc) => { if (series.split_mode === 'terms' && series.terms_field) { + const termsField = series.terms_field; + const orderByTerms = series.terms_order_by; + const direction = series.terms_direction || 'desc'; - const metric = series.metrics.find((item) => item.id === series.terms_order_by); - overwrite(doc, `aggs.${series.id}.terms.field`, series.terms_field); + const metric = series.metrics.find((item) => item.id === orderByTerms); + overwrite(doc, `aggs.${series.id}.terms.field`, termsField); overwrite(doc, `aggs.${series.id}.terms.size`, series.terms_size); if (series.terms_include) { overwrite(doc, `aggs.${series.id}.terms.include`, series.terms_include); @@ -36,16 +39,16 @@ export function splitByTerms(req, panel, series) { overwrite(doc, `aggs.${series.id}.terms.exclude`, series.terms_exclude); } if (metric && metric.type !== 'count' && ~basicAggs.indexOf(metric.type)) { - const sortAggKey = `${series.terms_order_by}-SORT`; + const sortAggKey = `${orderByTerms}-SORT`; const fn = bucketTransform[metric.type]; - const bucketPath = getBucketsPath(series.terms_order_by, series.metrics).replace( - series.terms_order_by, + const bucketPath = getBucketsPath(orderByTerms, series.metrics).replace( + orderByTerms, sortAggKey ); overwrite(doc, `aggs.${series.id}.terms.order`, { [bucketPath]: direction }); overwrite(doc, `aggs.${series.id}.aggs`, { [sortAggKey]: fn(metric) }); - } else if (['_key', '_count'].includes(series.terms_order_by)) { - overwrite(doc, `aggs.${series.id}.terms.order`, { [series.terms_order_by]: direction }); + } else if (['_key', '_count'].includes(orderByTerms)) { + overwrite(doc, `aggs.${series.id}.terms.order`, { [orderByTerms]: direction }); } else { overwrite(doc, `aggs.${series.id}.terms.order`, { _count: direction }); } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js index ba65e583cc094..7dfc0cec74d12 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js @@ -23,7 +23,7 @@ import { isLastValueTimerangeMode } from '../../helpers/get_timerange_mode'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { getTimerange } from '../../helpers/get_timerange'; import { calculateAggRoot } from './calculate_agg_root'; -import { search } from '../../../../../../../plugins/data/server'; +import { search, UI_SETTINGS } from '../../../../../../../plugins/data/server'; const { dateHistogramInterval } = search.aggs; export function dateHistogram( @@ -32,12 +32,14 @@ export function dateHistogram( esQueryConfig, indexPatternObject, capabilities, - { barTargetUiSettings } + uiSettings ) { - return (next) => (doc) => { + return (next) => async (doc) => { + const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); const { timeField, interval } = getIntervalAndTimefield(panel, {}, indexPatternObject); const meta = { timeField, + index: indexPatternObject?.title, }; const getDateHistogramForLastBucketMode = () => { @@ -65,7 +67,7 @@ export function dateHistogram( }); overwrite(doc, aggRoot.replace(/\.aggs$/, '.meta'), { - timeField, + ...meta, intervalString, bucketSize, }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js index 13874166bc558..0166537146e0b 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js @@ -17,12 +17,13 @@ * under the License. */ -const filter = (metric) => metric.type === 'filter_ratio'; import { esQuery } from '../../../../../../data/server'; import { bucketTransform } from '../../helpers/bucket_transform'; import { overwrite } from '../../helpers'; import { calculateAggRoot } from './calculate_agg_root'; +const filter = (metric) => metric.type === 'filter_ratio'; + export function ratios(req, panel, esQueryConfig, indexPatternObject) { return (next) => (doc) => { panel.series.forEach((column) => { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js index fe6a8b537d64b..c5501233f8135 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js @@ -22,6 +22,7 @@ import { getBucketSize } from '../../helpers/get_bucket_size'; import { bucketTransform } from '../../helpers/bucket_transform'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { calculateAggRoot } from './calculate_agg_root'; +import { UI_SETTINGS } from '../../../../../../data/common'; export function metricBuckets( req, @@ -29,9 +30,10 @@ export function metricBuckets( esQueryConfig, indexPatternObject, capabilities, - { barTargetUiSettings } + uiSettings ) { - return (next) => (doc) => { + return (next) => async (doc) => { + const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); const { interval } = getIntervalAndTimefield(panel, {}, indexPatternObject); const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/pivot.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/pivot.js index ad085f25cf451..8ae50f62ca210 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/pivot.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/pivot.js @@ -27,6 +27,7 @@ import { bucketTransform } from '../../helpers/bucket_transform'; export function pivot(req, panel) { return (next) => (doc) => { const { sort } = req.payload.state; + if (panel.pivot_id) { overwrite(doc, 'aggs.pivot.terms.field', panel.pivot_id); overwrite(doc, 'aggs.pivot.terms.size', panel.pivot_rows); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js index 6cf165d124e26..914b246bc8fdf 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/positive_rate.js @@ -21,6 +21,7 @@ import { getBucketSize } from '../../helpers/get_bucket_size'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { calculateAggRoot } from './calculate_agg_root'; import { createPositiveRate, filter } from '../series/positive_rate'; +import { UI_SETTINGS } from '../../../../../../data/common'; export function positiveRate( req, @@ -28,9 +29,10 @@ export function positiveRate( esQueryConfig, indexPatternObject, capabilities, - { barTargetUiSettings } + uiSettings ) { - return (next) => (doc) => { + return (next) => async (doc) => { + const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); const { interval } = getIntervalAndTimefield(panel, {}, indexPatternObject); const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js index ba08b18256dec..1333cecc36a92 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js @@ -22,6 +22,7 @@ import { getBucketSize } from '../../helpers/get_bucket_size'; import { bucketTransform } from '../../helpers/bucket_transform'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { calculateAggRoot } from './calculate_agg_root'; +import { UI_SETTINGS } from '../../../../../../data/common'; export function siblingBuckets( req, @@ -29,9 +30,10 @@ export function siblingBuckets( esQueryConfig, indexPatternObject, capabilities, - { barTargetUiSettings } + uiSettings ) { - return (next) => (doc) => { + return (next) => async (doc) => { + const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); const { interval } = getIntervalAndTimefield(panel, {}, indexPatternObject); const { bucketSize } = getBucketSize(req, interval, capabilities, barTargetUiSettings); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.js index f8752ce8fa3a8..2e4c05273ee3f 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.js @@ -25,8 +25,8 @@ import { getSplits } from '../../helpers/get_splits'; import { mapBucket } from '../../helpers/map_bucket'; import { evaluate } from 'tinymath'; -export function mathAgg(resp, panel, series, meta) { - return (next) => (results) => { +export function mathAgg(resp, panel, series, meta, extractFields) { + return (next) => async (results) => { const mathMetric = last(series.metrics); if (mathMetric.type !== 'math') return next(results); // Filter the results down to only the ones that match the series.id. Sometimes @@ -38,7 +38,7 @@ export function mathAgg(resp, panel, series, meta) { return true; }); const decoration = getDefaultDecoration(series); - const splits = getSplits(resp, panel, series, meta); + const splits = await getSplits(resp, panel, series, meta, extractFields); const mathSeries = splits.map((split) => { if (mathMetric.variables.length) { // Gather the data for the splits. The data will either be a sibling agg or diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js index 79cfd2ddd54bb..c65571379d50a 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js @@ -91,15 +91,16 @@ describe('math(resp, panel, series)', () => { }; }); - test('calls next when finished', () => { + test('calls next when finished', async () => { const next = jest.fn(); - mathAgg(resp, panel, series)(next)([]); + await mathAgg(resp, panel, series)(next)([]); expect(next.mock.calls.length).toEqual(1); }); - test('creates a series', () => { - const next = mathAgg(resp, panel, series)((results) => results); - const results = stdMetric(resp, panel, series)(next)([]); + test('creates a series', async () => { + const next = await mathAgg(resp, panel, series)((results) => results); + const results = await stdMetric(resp, panel, series)(next)([]); + expect(results).toHaveLength(1); expect(results[0]).toEqual({ @@ -118,12 +119,12 @@ describe('math(resp, panel, series)', () => { }); }); - test('turns division by zero into null values', () => { + test('turns division by zero into null values', async () => { resp.aggregations.test.buckets[0].timeseries.buckets[0].mincpu = 0; - const next = mathAgg(resp, panel, series)((results) => results); - const results = stdMetric(resp, panel, series)(next)([]); - expect(results).toHaveLength(1); + const next = await mathAgg(resp, panel, series)((results) => results); + const results = await stdMetric(resp, panel, series)(next)([]); + expect(results).toHaveLength(1); expect(results[0]).toEqual( expect.objectContaining({ data: [ @@ -134,15 +135,35 @@ describe('math(resp, panel, series)', () => { ); }); - test('throws on actual tinymath expression errors', () => { + test('throws on actual tinymath expression errors #1', async () => { series.metrics[2].script = 'notExistingFn(params.a)'; - expect(() => - stdMetric(resp, panel, series)(mathAgg(resp, panel, series)((results) => results))([]) - ).toThrow(); + try { + await stdMetric( + resp, + panel, + series + )(await mathAgg(resp, panel, series)((results) => results))([]); + } catch (e) { + expect(e.message).toEqual( + 'Failed to parse expression. Expected "*", "+", "-", "/", or end of input but "(" found.' + ); + } + }); + + test('throws on actual tinymath expression errors #2', async () => { series.metrics[2].script = 'divide(params.a, params.b'; - expect(() => - stdMetric(resp, panel, series)(mathAgg(resp, panel, series)((results) => results))([]) - ).toThrow(); + + try { + await stdMetric( + resp, + panel, + series + )(await mathAgg(resp, panel, series)((results) => results))([]); + } catch (e) { + expect(e.message).toEqual( + 'Failed to parse expression. Expected "*", "+", "-", "/", or end of input but "(" found.' + ); + } }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.js index 2e0d176f80b23..6703202ef6b49 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.js @@ -23,15 +23,15 @@ import { getSplits } from '../../helpers/get_splits'; import { getLastMetric } from '../../helpers/get_last_metric'; import { METRIC_TYPES } from '../../../../../common/metric_types'; -export function percentile(resp, panel, series, meta) { - return (next) => (results) => { +export function percentile(resp, panel, series, meta, extractFields) { + return (next) => async (results) => { const metric = getLastMetric(series); if (metric.type !== METRIC_TYPES.PERCENTILE) { return next(results); } - getSplits(resp, panel, series, meta).forEach((split) => { + (await getSplits(resp, panel, series, meta, extractFields)).forEach((split) => { metric.percentiles.forEach((percentile) => { const percentileValue = percentile.value ? percentile.value : 0; const id = `${split.id}:${percentile.id}`; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.test.js index d239cb2138fe1..225cc5e261a3a 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.test.js @@ -80,15 +80,18 @@ describe('percentile(resp, panel, series)', () => { }; }); - test('calls next when finished', () => { + test('calls next when finished', async () => { const next = jest.fn(); - percentile(resp, panel, series)(next)([]); + + await percentile(resp, panel, series, {})(next)([]); + expect(next.mock.calls.length).toEqual(1); }); - test('creates a series', () => { + test('creates a series', async () => { const next = (results) => results; - const results = percentile(resp, panel, series)(next)([]); + const results = await percentile(resp, panel, series, {})(next)([]); + expect(results).toHaveLength(2); expect(results[0]).toHaveProperty('id', 'test:10-90'); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js index c163605af7ac5..7660dd80d143d 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js @@ -23,15 +23,15 @@ import { getLastMetric } from '../../helpers/get_last_metric'; import { toPercentileNumber } from '../../../../../common/to_percentile_number'; import { METRIC_TYPES } from '../../../../../common/metric_types'; -export function percentileRank(resp, panel, series, meta) { - return (next) => (results) => { +export function percentileRank(resp, panel, series, meta, extractFields) { + return (next) => async (results) => { const metric = getLastMetric(series); if (metric.type !== METRIC_TYPES.PERCENTILE_RANK) { return next(results); } - getSplits(resp, panel, series, meta).forEach((split) => { + (await getSplits(resp, panel, series, meta, extractFields)).forEach((split) => { (metric.values || []).forEach((percentileRank, index) => { const data = split.timeseries.buckets.map((bucket) => [ bucket.key, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.js index d2b0ce9226bb9..07aebea78a1fb 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.js @@ -22,8 +22,8 @@ import _ from 'lodash'; import { getDefaultDecoration } from '../../helpers/get_default_decoration'; import { calculateLabel } from '../../../../../common/calculate_label'; -export function seriesAgg(resp, panel, series) { - return (next) => (results) => { +export function seriesAgg(resp, panel, series, meta, extractFields) { + return (next) => async (results) => { if (series.metrics.some((m) => m.type === 'series_agg')) { const decoration = getDefaultDecoration(series); @@ -43,9 +43,14 @@ export function seriesAgg(resp, panel, series) { const fn = SeriesAgg[m.function]; return (fn && fn(acc)) || acc; }, targetSeries); + + const fieldsForMetaIndex = meta.index ? await extractFields(meta.index) : []; + results.push({ id: `${series.id}`, - label: series.label || calculateLabel(_.last(series.metrics), series.metrics), + label: + series.label || + calculateLabel(_.last(series.metrics), series.metrics, fieldsForMetaIndex), color: series.color, data: _.first(data), ...decoration, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.test.js index b458eee17bce5..ddf4f620f0657 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.test.js @@ -91,15 +91,16 @@ describe('seriesAgg(resp, panel, series)', () => { }; }); - test('calls next when finished', () => { + test('calls next when finished', async () => { const next = jest.fn(); - seriesAgg(resp, panel, series)(next)([]); + await seriesAgg(resp, panel, series, {})(next)([]); expect(next.mock.calls.length).toEqual(1); }); - test('creates a series', () => { - const next = seriesAgg(resp, panel, series)((results) => results); - const results = stdMetric(resp, panel, series)(next)([]); + test('creates a series', async () => { + const next = await seriesAgg(resp, panel, series, {})((results) => results); + const results = await stdMetric(resp, panel, series, {})(next)([]); + expect(results).toHaveLength(1); expect(results[0]).toEqual({ diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.js index be95913c6ec38..ffc3d2636d1af 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.js @@ -20,36 +20,38 @@ import { getAggValue, getLastMetric, getSplits } from '../../helpers'; import { METRIC_TYPES } from '../../../../../common/metric_types'; -export function stdDeviationBands(resp, panel, series, meta) { - return (next) => (results) => { +export function stdDeviationBands(resp, panel, series, meta, extractFields) { + return (next) => async (results) => { const metric = getLastMetric(series); if (metric.type === METRIC_TYPES.STD_DEVIATION && metric.mode === 'band') { - getSplits(resp, panel, series, meta).forEach(({ id, color, label, timeseries }) => { - const data = timeseries.buckets.map((bucket) => [ - bucket.key, - getAggValue(bucket, { ...metric, mode: 'upper' }), - getAggValue(bucket, { ...metric, mode: 'lower' }), - ]); + (await getSplits(resp, panel, series, meta, extractFields)).forEach( + ({ id, color, label, timeseries }) => { + const data = timeseries.buckets.map((bucket) => [ + bucket.key, + getAggValue(bucket, { ...metric, mode: 'upper' }), + getAggValue(bucket, { ...metric, mode: 'lower' }), + ]); - results.push({ - id, - label, - color, - data, - lines: { - show: series.chart_type === 'line', - fill: 0.5, - lineWidth: 0, - mode: 'band', - }, - bars: { - show: series.chart_type === 'bar', - fill: 0.5, - mode: 'band', - }, - points: { show: false }, - }); - }); + results.push({ + id, + label, + color, + data, + lines: { + show: series.chart_type === 'line', + fill: 0.5, + lineWidth: 0, + mode: 'band', + }, + bars: { + show: series.chart_type === 'bar', + fill: 0.5, + mode: 'band', + }, + points: { show: false }, + }); + } + ); } return next(results); }; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.test.js index 4899eaed2611a..97d0cc8a84f72 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.test.js @@ -77,15 +77,15 @@ describe('stdDeviationBands(resp, panel, series)', () => { }; }); - test('calls next when finished', () => { + test('calls next when finished', async () => { const next = jest.fn(); - stdDeviationBands(resp, panel, series)(next)([]); + await stdDeviationBands(resp, panel, series, {})(next)([]); expect(next.mock.calls.length).toEqual(1); }); - test('creates a series', () => { + test('creates a series', async () => { const next = (results) => results; - const results = stdDeviationBands(resp, panel, series)(next)([]); + const results = await stdDeviationBands(resp, panel, series, {})(next)([]); expect(results).toHaveLength(1); expect(results[0]).toEqual({ diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.js index 321e1c5125e1c..652cd2feb979a 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.js @@ -19,11 +19,11 @@ import { getSplits, getLastMetric, getSiblingAggValue } from '../../helpers'; -export function stdDeviationSibling(resp, panel, series, meta) { - return (next) => (results) => { +export function stdDeviationSibling(resp, panel, series, meta, extractFields) { + return (next) => async (results) => { const metric = getLastMetric(series); if (metric.mode === 'band' && metric.type === 'std_deviation_bucket') { - getSplits(resp, panel, series, meta).forEach((split) => { + (await getSplits(resp, panel, series, meta, extractFields)).forEach((split) => { const data = split.timeseries.buckets.map((bucket) => [ bucket.key, getSiblingAggValue(split, { ...metric, mode: 'upper' }), diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.test.js index b6acf822bc242..93386a008ae96 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.test.js @@ -77,15 +77,15 @@ describe('stdDeviationSibling(resp, panel, series)', () => { }; }); - test('calls next when finished', () => { + test('calls next when finished', async () => { const next = jest.fn(); - stdDeviationSibling(resp, panel, series)(next)([]); + await stdDeviationSibling(resp, panel, series, {})(next)([]); expect(next.mock.calls.length).toEqual(1); }); - test('creates a series', () => { + test('creates a series', async () => { const next = (results) => results; - const results = stdDeviationSibling(resp, panel, series)(next)([]); + const results = await stdDeviationSibling(resp, panel, series, {})(next)([]); expect(results).toHaveLength(1); expect(results[0]).toEqual({ diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.js index e04c3a93e81bb..9b9dacd1af957 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.js @@ -23,8 +23,8 @@ import { getLastMetric } from '../../helpers/get_last_metric'; import { mapBucket } from '../../helpers/map_bucket'; import { METRIC_TYPES } from '../../../../../common/metric_types'; -export function stdMetric(resp, panel, series, meta) { - return (next) => (results) => { +export function stdMetric(resp, panel, series, meta, extractFields) { + return (next) => async (results) => { const metric = getLastMetric(series); if (metric.type === METRIC_TYPES.STD_DEVIATION && metric.mode === 'band') { return next(results); @@ -35,17 +35,20 @@ export function stdMetric(resp, panel, series, meta) { } if (/_bucket$/.test(metric.type)) return next(results); const decoration = getDefaultDecoration(series); - getSplits(resp, panel, series, meta).forEach((split) => { + + (await getSplits(resp, panel, series, meta, extractFields)).forEach((split) => { const data = split.timeseries.buckets.map(mapBucket(metric)); results.push({ id: `${split.id}`, label: split.label, + splitByLabel: split.splitByLabel, labelFormatted: split.labelFormatted, color: split.color, data, ...decoration, }); }); + return next(results); }; } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.test.js index 78a29c04732d7..07590ba359bfe 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.test.js @@ -58,32 +58,38 @@ describe('stdMetric(resp, panel, series)', () => { }; }); - test('calls next when finished', () => { + test('calls next when finished', async () => { const next = jest.fn(); - stdMetric(resp, panel, series)(next)([]); + await stdMetric(resp, panel, series, {})(next)([]); + expect(next.mock.calls.length).toEqual(1); }); - test('calls next when finished (percentile)', () => { + test('calls next when finished (percentile)', async () => { series.metrics[0].type = 'percentile'; + const next = jest.fn((d) => d); - const results = stdMetric(resp, panel, series)(next)([]); + const results = await stdMetric(resp, panel, series, {})(next)([]); + expect(next.mock.calls.length).toEqual(1); expect(results).toHaveLength(0); }); - test('calls next when finished (std_deviation band)', () => { + test('calls next when finished (std_deviation band)', async () => { series.metrics[0].type = 'std_deviation'; series.metrics[0].mode = 'band'; + const next = jest.fn((d) => d); - const results = stdMetric(resp, panel, series)(next)([]); + const results = await stdMetric(resp, panel, series, {})(next)([]); + expect(next.mock.calls.length).toEqual(1); expect(results).toHaveLength(0); }); - test('creates a series', () => { + test('creates a series', async () => { const next = (results) => results; - const results = stdMetric(resp, panel, series)(next)([]); + const results = await stdMetric(resp, panel, series, {})(next)([]); + expect(results).toHaveLength(1); expect(results[0]).toHaveProperty('color', 'rgb(255, 0, 0)'); expect(results[0]).toHaveProperty('id', 'test'); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_sibling.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_sibling.js index b5d972a38769e..80f1f6c404724 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_sibling.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_sibling.js @@ -22,15 +22,15 @@ import { getSplits } from '../../helpers/get_splits'; import { getLastMetric } from '../../helpers/get_last_metric'; import { getSiblingAggValue } from '../../helpers/get_sibling_agg_value'; -export function stdSibling(resp, panel, series, meta) { - return (next) => (results) => { +export function stdSibling(resp, panel, series, meta, extractFields) { + return (next) => async (results) => { const metric = getLastMetric(series); if (!/_bucket$/.test(metric.type)) return next(results); if (metric.type === 'std_deviation_bucket' && metric.mode === 'band') return next(results); const decoration = getDefaultDecoration(series); - getSplits(resp, panel, series, meta).forEach((split) => { + (await getSplits(resp, panel, series, meta, extractFields)).forEach((split) => { const data = split.timeseries.buckets.map((bucket) => { return [bucket.key, getSiblingAggValue(split, metric)]; }); @@ -42,6 +42,7 @@ export function stdSibling(resp, panel, series, meta) { ...decoration, }); }); + return next(results); }; } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_sibling.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_sibling.test.js index d99ae6771c437..52edb8408b5bd 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_sibling.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_sibling.test.js @@ -72,23 +72,23 @@ describe('stdSibling(resp, panel, series)', () => { }; }); - test('calls next when finished', () => { + test('calls next when finished', async () => { const next = jest.fn(); - stdSibling(resp, panel, series)(next)([]); + await stdSibling(resp, panel, series, {})(next)([]); expect(next.mock.calls.length).toEqual(1); }); - test('calls next when std. deviation bands set', () => { + test('calls next when std. deviation bands set', async () => { series.metrics[1].mode = 'band'; const next = jest.fn((results) => results); - const results = stdSibling(resp, panel, series)(next)([]); + const results = await stdSibling(resp, panel, series, {})(next)([]); expect(next.mock.calls.length).toEqual(1); expect(results).toHaveLength(0); }); - test('creates a series', () => { + test('creates a series', async () => { const next = (results) => results; - const results = stdSibling(resp, panel, series)(next)([]); + const results = await stdSibling(resp, panel, series, {})(next)([]); expect(results).toHaveLength(1); expect(results[0]).toEqual({ diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js index 57c1a8aead7dc..019d5d32c1c30 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js @@ -60,15 +60,17 @@ describe('timeShift(resp, panel, series)', () => { }; }); - test('calls next when finished', () => { + test('calls next when finished', async () => { const next = jest.fn(); - timeShift(resp, panel, series)(next)([]); + await timeShift(resp, panel, series, {})(next)([]); + expect(next.mock.calls.length).toEqual(1); }); - test('creates a series', () => { - const next = timeShift(resp, panel, series)((results) => results); - const results = stdMetric(resp, panel, series)(next)([]); + test('creates a series', async () => { + const next = await timeShift(resp, panel, series, {})((results) => results); + const results = await stdMetric(resp, panel, series, {})(next)([]); + expect(results).toHaveLength(1); expect(results[0]).toHaveProperty('color', 'rgb(255, 0, 0)'); expect(results[0]).toHaveProperty('id', 'test'); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/index.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/index.js index cbd775bcd816b..ee4161288673c 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/index.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/index.js @@ -17,7 +17,6 @@ * under the License. */ -// import percentile from './percentile'; import { stdMetric } from './std_metric'; import { stdSibling } from './std_sibling'; import { seriesAgg } from './series_agg'; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/percentile.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/percentile.js index 788bb6f14a0c7..33af66855ef3b 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/percentile.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/percentile.js @@ -22,8 +22,8 @@ import { getLastMetric } from '../../helpers/get_last_metric'; import { toPercentileNumber } from '../../../../../common/to_percentile_number'; import { METRIC_TYPES } from '../../../../../common/metric_types'; -export function percentile(bucket, panel, series) { - return (next) => (results) => { +export function percentile(bucket, panel, series, meta, extractFields) { + return (next) => async (results) => { const metric = getLastMetric(series); if (metric.type !== METRIC_TYPES.PERCENTILE) { @@ -34,7 +34,7 @@ export function percentile(bucket, panel, series) { aggregations: bucket, }; - getSplits(fakeResp, panel, series).forEach((split) => { + (await getSplits(fakeResp, panel, series, meta, extractFields)).forEach((split) => { // table allows only one percentile in a series (the last one will be chosen in case of several) const percentile = last(metric.percentiles); const percentileKey = toPercentileNumber(percentile.value); @@ -45,6 +45,7 @@ export function percentile(bucket, panel, series) { results.push({ id: split.id, + label: `${split.label} (${percentile.value ?? 0})`, data, }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/percentile_rank.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/percentile_rank.js index c280538c7ce5f..c902631aaf47b 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/percentile_rank.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/percentile_rank.js @@ -23,8 +23,8 @@ import { toPercentileNumber } from '../../../../../common/to_percentile_number'; import { getAggValue } from '../../helpers/get_agg_value'; import { METRIC_TYPES } from '../../../../../common/metric_types'; -export function percentileRank(bucket, panel, series) { - return (next) => (results) => { +export function percentileRank(bucket, panel, series, meta, extractFields) { + return (next) => async (results) => { const metric = getLastMetric(series); if (metric.type !== METRIC_TYPES.PERCENTILE_RANK) { @@ -35,7 +35,7 @@ export function percentileRank(bucket, panel, series) { aggregations: bucket, }; - getSplits(fakeResp, panel, series).forEach((split) => { + (await getSplits(fakeResp, panel, series, meta, extractFields)).forEach((split) => { // table allows only one percentile rank in a series (the last one will be chosen in case of several) const lastRankValue = last(metric.values); const percentileRank = toPercentileNumber(lastRankValue); @@ -51,7 +51,7 @@ export function percentileRank(bucket, panel, series) { results.push({ data, id: split.id, - label: `${split.label} (${percentileRank || 0})`, + label: `${split.label} (${lastRankValue ?? 0})`, }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/series_agg.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/series_agg.js index 343198bc22a6e..56a3207cd115a 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/series_agg.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/series_agg.js @@ -21,8 +21,8 @@ import { SeriesAgg } from './_series_agg'; import _ from 'lodash'; import { calculateLabel } from '../../../../../common/calculate_label'; -export function seriesAgg(resp, panel, series) { - return (next) => (results) => { +export function seriesAgg(resp, panel, series, meta, extractFields) { + return (next) => async (results) => { if (series.aggregate_by && series.aggregate_function) { const targetSeries = []; // Filter out the seires with the matching metric and store them @@ -36,9 +36,14 @@ export function seriesAgg(resp, panel, series) { }); const fn = SeriesAgg[series.aggregate_function]; const data = fn(targetSeries); + + const fieldsForMetaIndex = meta.index ? await extractFields(meta.index) : []; + results.push({ id: `${series.id}`, - label: series.label || calculateLabel(_.last(series.metrics), series.metrics), + label: + series.label || + calculateLabel(_.last(series.metrics), series.metrics, fieldsForMetaIndex), data: _.first(data), }); } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/std_metric.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/std_metric.js index 2242b338e7b92..6f7e8b0edd10d 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/std_metric.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/std_metric.js @@ -22,8 +22,8 @@ import { getLastMetric } from '../../helpers/get_last_metric'; import { mapBucket } from '../../helpers/map_bucket'; import { METRIC_TYPES } from '../../../../../common/metric_types'; -export function stdMetric(bucket, panel, series) { - return (next) => (results) => { +export function stdMetric(bucket, panel, series, meta, extractFields) { + return (next) => async (results) => { const metric = getLastMetric(series); if (metric.type === METRIC_TYPES.STD_DEVIATION && metric.mode === 'band') { @@ -42,7 +42,7 @@ export function stdMetric(bucket, panel, series) { aggregations: bucket, }; - getSplits(fakeResp, panel, series).forEach((split) => { + (await getSplits(fakeResp, panel, series, meta, extractFields)).forEach((split) => { const data = split.timeseries.buckets.map(mapBucket(metric)); results.push({ id: split.id, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/std_sibling.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/std_sibling.js index 17cfac6e8895b..001da9019f116 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/std_sibling.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/table/std_sibling.js @@ -21,15 +21,15 @@ import { getSplits } from '../../helpers/get_splits'; import { getLastMetric } from '../../helpers/get_last_metric'; import { getSiblingAggValue } from '../../helpers/get_sibling_agg_value'; -export function stdSibling(bucket, panel, series) { - return (next) => (results) => { +export function stdSibling(bucket, panel, series, meta, extractFields) { + return (next) => async (results) => { const metric = getLastMetric(series); if (!/_bucket$/.test(metric.type)) return next(results); if (metric.type === 'std_deviation_bucket' && metric.mode === 'band') return next(results); const fakeResp = { aggregations: bucket }; - getSplits(fakeResp, panel, series).forEach((split) => { + (await getSplits(fakeResp, panel, series, meta, extractFields)).forEach((split) => { const data = split.timeseries.buckets.map((b) => { return [b.key, getSiblingAggValue(split, metric)]; }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts index 6b2ef320d54b7..64cd3bf7a7dea 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts @@ -78,7 +78,7 @@ const body = JSON.parse(` `); describe('buildRequestBody(req)', () => { - test('returns a valid body', () => { + test('returns a valid body', async () => { const panel = body.panels[0]; const series = panel.series[0]; const getValidTimeInterval = jest.fn(() => '10s'); @@ -91,14 +91,16 @@ describe('buildRequestBody(req)', () => { queryStringOptions: {}, }; const indexPatternObject = {}; - const doc = buildRequestBody( + const doc = await buildRequestBody( { payload: body }, panel, series, config, indexPatternObject, capabilities, - { barTargetUiSettings: 50 } + { + get: async () => 50, + } ); expect(doc).toEqual({ diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.ts index 9d8f3fca789f0..45d3d788df4f3 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.ts @@ -34,8 +34,8 @@ import { processors } from '../request_processors/series/index'; * ] * @returns {Object} doc - processed body */ -export function buildRequestBody(...args: any[]) { +export async function buildRequestBody(...args: any[]) { const processor = buildProcessorFunction(processors, ...args); - const doc = processor({}); + const doc = await processor({}); return doc; } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.js index 3804b1407b086..ca4f631f9ea70 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.js @@ -19,7 +19,6 @@ import { buildRequestBody } from './build_request_body'; import { getEsShardTimeout } from '../helpers/get_es_shard_timeout'; import { getIndexPatternObject } from '../helpers/get_index_pattern'; -import { UI_SETTINGS } from '../../../../../data/common'; export async function getSeriesRequestParams(req, panel, series, esQueryConfig, capabilities) { const uiSettings = req.getUiSettingsService(); @@ -27,17 +26,14 @@ export async function getSeriesRequestParams(req, panel, series, esQueryConfig, (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; const { indexPatternObject, indexPatternString } = await getIndexPatternObject(req, indexPattern); - const request = buildRequestBody( + const request = await buildRequestBody( req, panel, series, esQueryConfig, indexPatternObject, capabilities, - { - maxBarsUiSettings: await uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), - barTargetUiSettings: await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), - } + uiSettings ); const esShardTimeout = await getEsShardTimeout(req); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/handle_response_body.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/handle_response_body.js index eef7143990e98..c04e56de57a4e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/handle_response_body.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/handle_response_body.js @@ -21,9 +21,10 @@ import { buildProcessorFunction } from '../build_processor_function'; import { processors } from '../response_processors/series'; import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { createFieldsFetcher } from './../helpers/fields_fetcher'; -export function handleResponseBody(panel) { - return (resp) => { +export function handleResponseBody(panel, req, searchStrategy, capabilities) { + return async (resp) => { if (resp.error) { const err = new Error(resp.error.type); err.response = JSON.stringify(resp); @@ -48,8 +49,11 @@ export function handleResponseBody(panel) { const [seriesId] = keys; const meta = get(resp, `aggregations.${seriesId}.meta`, {}); const series = panel.series.find((s) => s.id === (meta.seriesId || seriesId)); - const processor = buildProcessorFunction(processors, resp, panel, series, meta); - return processor([]); + const extractFields = createFieldsFetcher(req, searchStrategy, capabilities); + + const processor = buildProcessorFunction(processors, resp, panel, series, meta, extractFields); + + return await processor([]); }; } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/build_request_body.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/table/build_request_body.js index 7715c803d374b..3fcf38bb07e20 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/build_request_body.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/table/build_request_body.js @@ -20,8 +20,8 @@ import { buildProcessorFunction } from '../build_processor_function'; import { processors } from '../request_processors/table'; -export function buildRequestBody(...args) { +export async function buildRequestBody(...args) { const processor = buildProcessorFunction(processors, ...args); - const doc = processor({}); + const doc = await processor({}); return doc; } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js index 909cee456c31f..9604904af2c7d 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js @@ -34,27 +34,40 @@ function trendSinceLastBucket(data) { return Number.isNaN(trend) ? 0 : trend; } -export function processBucket(panel) { - return (bucket) => { - const series = getActiveSeries(panel).map((series) => { - const timeseries = get(bucket, `${series.id}.timeseries`); - const buckets = get(bucket, `${series.id}.buckets`); +export function processBucket(panel, req, searchStrategy, capabilities, extractFields) { + return async (bucket) => { + const series = await Promise.all( + getActiveSeries(panel).map(async (series) => { + const timeseries = get(bucket, `${series.id}.timeseries`); + const buckets = get(bucket, `${series.id}.buckets`); + let meta = {}; + + if (!timeseries && buckets) { + meta = get(bucket, `${series.id}.meta`); + const timeseries = { + buckets: get(bucket, `${series.id}.buckets`), + }; + overwrite(bucket, series.id, { meta, timeseries }); + } + + const processor = buildProcessorFunction( + processors, + bucket, + panel, + series, + meta, + extractFields + ); + const result = first(await processor([])); + + if (!result) return null; + const data = get(result, 'data', []); + result.slope = trendSinceLastBucket(data); + result.last = getLastValue(data); + return result; + }) + ); - if (!timeseries && buckets) { - const meta = get(bucket, `${series.id}.meta`); - const timeseries = { - buckets: get(bucket, `${series.id}.buckets`), - }; - overwrite(bucket, series.id, { meta, timeseries }); - } - const processor = buildProcessorFunction(processors, bucket, panel, series); - const result = first(processor([])); - if (!result) return null; - const data = get(result, 'data', []); - result.slope = trendSinceLastBucket(data); - result.last = getLastValue(data); - return result; - }); return { key: bucket.key, series }; }; } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.test.js index a4f9c71a5953d..83479468e7199 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.test.js @@ -94,17 +94,17 @@ describe('processBucket(panel)', () => { panel = createPanel([SERIES_ID]); }); - test('return the correct trend direction', () => { + test('return the correct trend direction', async () => { const bucketProcessor = processBucket(panel); const buckets = createBuckets([SERIES_ID]); for (const bucket of buckets) { - const result = bucketProcessor(bucket); + const result = await bucketProcessor(bucket); expect(result.key).toEqual(bucket.key); expect(trendChecker(bucket.expectedTrend, result.series[0].slope)).toBeTruthy(); } }); - test('properly handle 0 values for trend', () => { + test('properly handle 0 values for trend', async () => { const bucketProcessor = processBucket(panel); const bucketforNaNResult = { key: 'NaNScenario', @@ -121,16 +121,18 @@ describe('processBucket(panel)', () => { ], }, }; - const result = bucketProcessor(bucketforNaNResult); + const result = await bucketProcessor(bucketforNaNResult); expect(result.key).toEqual(bucketforNaNResult.key); expect(trendChecker(bucketforNaNResult.expectedTrend, result.series[0].slope)).toEqual(true); }); - test('have the side effect to create the timeseries property if missing on bucket', () => { + test('have the side effect to create the timeseries property if missing on bucket', async () => { const bucketProcessor = processBucket(panel); const buckets = createBuckets([SERIES_ID]); + for (const bucket of buckets) { - bucketProcessor(bucket); + await bucketProcessor(bucket); + expect(bucket[SERIES_ID].buckets).toBeUndefined(); expect(bucket[SERIES_ID].timeseries).toBeDefined(); } @@ -145,11 +147,12 @@ describe('processBucket(panel)', () => { panel = createPanel(SERIES); }); - test('return the correct trend direction', () => { + test('return the correct trend direction', async () => { const bucketProcessor = processBucket(panel); const buckets = createBuckets(SERIES); for (const bucket of buckets) { - const result = bucketProcessor(bucket); + const result = await bucketProcessor(bucket); + expect(result.key).toEqual(bucket.key); expect(trendChecker(bucket.expectedTrend, result.series[0].slope)).toBeTruthy(); expect(trendChecker(bucket.expectedTrend, result.series[1].slope)).toBeTruthy(); diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 3956f930758d7..590b65e1af13d 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -108,7 +108,6 @@ export class VisualizeEmbeddable private vis: Vis; private domNode: any; public readonly type = VISUALIZE_EMBEDDABLE_TYPE; - private autoRefreshFetchSubscription: Subscription; private abortController?: AbortController; private readonly deps: VisualizeEmbeddableFactoryDeps; private readonly inspectorAdapters?: Adapters; @@ -152,10 +151,6 @@ export class VisualizeEmbeddable this.attributeService = attributeService; this.savedVisualizationsLoader = savedVisualizationsLoader; - this.autoRefreshFetchSubscription = timefilter - .getAutoRefreshFetch$() - .subscribe(this.updateHandler.bind(this)); - this.subscriptions.push( this.getUpdated$().subscribe(() => { const isDirty = this.handleChanges(); @@ -368,7 +363,6 @@ export class VisualizeEmbeddable this.handler.destroy(); this.handler.getElement().remove(); } - this.autoRefreshFetchSubscription.unsubscribe(); } public reload = () => { diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index 80bf145e2a9f5..94519dceb3014 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -197,6 +197,7 @@ export class VisualizeEmbeddableFactory const saveOptions = { confirmOverwrite: false, returnToOrigin: true, + isTitleDuplicateConfirmed: true, }; savedVis.title = title; savedVis.copyOnSave = false; diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 854e04325b078..ad56f6a34c368 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -44,6 +44,9 @@ export type { ReactVisTypeOptions, Schema, ISchemas, + VisEditorConstructor, + IEditorController, + EditorRenderProps, } from './vis_types'; export { VisParams, SerializedVis, SerializedVisData, VisData } from './vis'; export type VisualizeEmbeddableFactoryContract = PublicContract; diff --git a/src/plugins/visualizations/public/vis_types/index.ts b/src/plugins/visualizations/public/vis_types/index.ts index 43de5d1ecce53..68c613f11f8df 100644 --- a/src/plugins/visualizations/public/vis_types/index.ts +++ b/src/plugins/visualizations/public/vis_types/index.ts @@ -20,6 +20,13 @@ export * from './types_service'; export { Schemas } from './schemas'; export { VisGroups } from './types'; -export type { VisType, ISchemas, Schema } from './types'; +export type { + VisType, + ISchemas, + Schema, + IEditorController, + VisEditorConstructor, + EditorRenderProps, +} from './types'; export type { BaseVisTypeOptions } from './base_vis_type'; export type { ReactVisTypeOptions } from './react_vis_type'; diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts index 88a4dad106897..c51c2415af041 100644 --- a/src/plugins/visualizations/public/vis_types/types.ts +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -16,13 +16,24 @@ * specific language governing permissions and limitations * under the License. */ - +import { EventEmitter } from 'events'; import { IconType } from '@elastic/eui'; import React, { ReactNode } from 'react'; import { Adapters } from 'src/plugins/inspector'; -import { VisEditorConstructor } from 'src/plugins/visualize/public'; -import { IndexPattern, AggGroupNames, AggParam, AggGroupName } from '../../../data/public'; +import { CoreStart } from 'src/core/public'; +import { SavedObject } from 'src/plugins/saved_objects/public'; +import { + IndexPattern, + AggGroupNames, + AggParam, + AggGroupName, + DataPublicPluginStart, + Filter, + TimeRange, + Query, +} from '../../../data/public'; import { Vis, VisParams, VisToExpressionAst, VisualizationControllerConstructor } from '../types'; +import { PersistedState, VisualizeEmbeddableContract } from '../index'; export interface VisTypeOptions { showTimePicker: boolean; @@ -152,3 +163,29 @@ export interface VisType { readonly editorConfig: Record; readonly visConfig: Record; } + +export type VisEditorConstructor = new ( + element: HTMLElement, + vis: Vis, + eventEmitter: EventEmitter, + embeddableHandler: VisualizeEmbeddableContract +) => IEditorController; + +export interface IEditorController { + render(props: EditorRenderProps): Promise | void; + destroy(): void; +} + +export interface EditorRenderProps { + core: CoreStart; + data: DataPublicPluginStart; + filters: Filter[]; + timeRange: TimeRange; + query?: Query; + savedSearch?: SavedObject; + uiState: PersistedState; + /** + * Flag to determine if visualiztion is linked to the saved search + */ + linked: boolean; +} diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx index 627d5cd00147b..becabc60eff16 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -190,6 +190,17 @@ const TopNav = ({ } }, [vis.params, vis.type, services.data.indexPatterns, vis.data.indexPattern]); + useEffect(() => { + const autoRefreshFetchSub = services.data.query.timefilter.timefilter + .getAutoRefreshFetch$() + .subscribe(() => { + visInstance.embeddableHandler.reload(); + }); + return () => { + autoRefreshFetchSub.unsubscribe(); + }; + }, [services.data.query.timefilter.timefilter, visInstance.embeddableHandler]); + return isChromeVisible ? ( /** * Most visualizations have all search bar components enabled. diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index 1729d273e24bc..78727492ac012 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -18,9 +18,8 @@ */ import { History } from 'history'; -import { TimeRange, Query, Filter, DataPublicPluginStart } from 'src/plugins/data/public'; +import { Query, Filter, DataPublicPluginStart } from 'src/plugins/data/public'; import { - PersistedState, SavedVisState, VisualizationsStart, Vis, @@ -45,7 +44,6 @@ import { SharePluginStart } from 'src/plugins/share/public'; import { SavedObjectsStart, SavedObject } from 'src/plugins/saved_objects/public'; import { EmbeddableStart, EmbeddableStateTransfer } from 'src/plugins/embeddable/public'; import { UrlForwardingStart } from 'src/plugins/url_forwarding/public'; -import { EventEmitter } from 'events'; import { DashboardStart } from '../../../dashboard/public'; import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public'; @@ -80,20 +78,6 @@ export type VisualizeAppStateContainer = ReduxLikeStateContainer< VisualizeAppStateTransitions >; -export interface EditorRenderProps { - core: CoreStart; - data: DataPublicPluginStart; - filters: Filter[]; - timeRange: TimeRange; - query?: Query; - savedSearch?: SavedObject; - uiState: PersistedState; - /** - * Flag to determine if visualiztion is linked to the saved search - */ - linked: boolean; -} - export interface VisualizeServices extends CoreStart { stateTransferService: EmbeddableStateTransfer; embeddable: EmbeddableStart; @@ -135,15 +119,3 @@ export interface ByValueVisInstance { } export type VisualizeEditorVisInstance = SavedVisInstance | ByValueVisInstance; - -export type VisEditorConstructor = new ( - element: HTMLElement, - vis: Vis, - eventEmitter: EventEmitter, - embeddableHandler: VisualizeEmbeddableContract -) => IEditorController; - -export interface IEditorController { - render(props: EditorRenderProps): Promise | void; - destroy(): void; -} diff --git a/src/plugins/visualize/public/application/utils/use/use_editor_updates.test.ts b/src/plugins/visualize/public/application/utils/use/use_editor_updates.test.ts index 23894368d606c..c9ffc8c27a978 100644 --- a/src/plugins/visualize/public/application/utils/use/use_editor_updates.test.ts +++ b/src/plugins/visualize/public/application/utils/use/use_editor_updates.test.ts @@ -21,12 +21,8 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { EventEmitter } from 'events'; import { useEditorUpdates } from './use_editor_updates'; -import { - VisualizeServices, - VisualizeAppStateContainer, - SavedVisInstance, - IEditorController, -} from '../../types'; +import { VisualizeServices, VisualizeAppStateContainer, SavedVisInstance } from '../../types'; +import type { IEditorController } from '../../../../../visualizations/public'; import { visualizeAppStateStub } from '../stubs'; import { createVisualizeServicesMock } from '../mocks'; diff --git a/src/plugins/visualize/public/application/utils/use/use_editor_updates.ts b/src/plugins/visualize/public/application/utils/use/use_editor_updates.ts index c29f6337a6246..1edb5564b48a7 100644 --- a/src/plugins/visualize/public/application/utils/use/use_editor_updates.ts +++ b/src/plugins/visualize/public/application/utils/use/use_editor_updates.ts @@ -25,9 +25,9 @@ import { VisualizeServices, VisualizeAppState, VisualizeAppStateContainer, - IEditorController, VisualizeEditorVisInstance, } from '../../types'; +import type { IEditorController } from '../../../../../visualizations/public'; export const useEditorUpdates = ( services: VisualizeServices, diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts index 3f9b3ca9b8b73..a8d17a3cdfadb 100644 --- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts @@ -26,9 +26,10 @@ import { redirectWhenMissing } from '../../../../../kibana_utils/public'; import { getVisualizationInstance } from '../get_visualization_instance'; import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs'; -import { SavedVisInstance, IEditorController, VisualizeServices } from '../../types'; +import { SavedVisInstance, VisualizeServices } from '../../types'; import { VisualizeConstants } from '../../visualize_constants'; import { getDefaultEditor } from '../../../services'; +import type { IEditorController } from '../../../../../visualizations/public'; /** * This effect is responsible for instantiating a saved vis or creating a new one diff --git a/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts b/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts index 9e222d208f460..6c9a464d64a72 100644 --- a/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts +++ b/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts @@ -20,7 +20,8 @@ import { EventEmitter } from 'events'; import { useEffect, useRef, useState } from 'react'; import { VisualizeInput } from 'src/plugins/visualizations/public'; -import { ByValueVisInstance, IEditorController, VisualizeServices } from '../../types'; +import { ByValueVisInstance, VisualizeServices } from '../../types'; +import type { IEditorController } from '../../../../../visualizations/public'; import { getVisualizationInstanceFromInput } from '../get_visualization_instance'; import { getEditBreadcrumbs } from '../breadcrumbs'; import { getDefaultEditor } from '../../../services'; diff --git a/src/plugins/visualize/public/index.ts b/src/plugins/visualize/public/index.ts index c9ac85c5123ce..385313f81ff68 100644 --- a/src/plugins/visualize/public/index.ts +++ b/src/plugins/visualize/public/index.ts @@ -20,11 +20,6 @@ import { PluginInitializerContext } from 'kibana/public'; import { VisualizePlugin, VisualizePluginSetup } from './plugin'; -export type { - EditorRenderProps, - IEditorController, - VisEditorConstructor, -} from './application/types'; export { VisualizeConstants } from './application/visualize_constants'; export { VisualizePluginSetup }; diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 5eef58a336eab..b1507155316e2 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -41,10 +41,10 @@ import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../d import { NavigationPublicPluginStart as NavigationStart } from '../../navigation/public'; import { SharePluginStart, SharePluginSetup } from '../../share/public'; import { UrlForwardingSetup, UrlForwardingStart } from '../../url_forwarding/public'; -import { VisualizationsStart } from '../../visualizations/public'; +import { VisualizationsStart, VisEditorConstructor } from '../../visualizations/public'; import { VisualizeConstants } from './application/visualize_constants'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/public'; -import { VisEditorConstructor, VisualizeServices } from './application/types'; +import { VisualizeServices } from './application/types'; import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { SavedObjectsStart } from '../../saved_objects/public'; import { EmbeddableStart } from '../../embeddable/public'; diff --git a/src/plugins/visualize/public/services.ts b/src/plugins/visualize/public/services.ts index 7994ad14543d5..5a8ac00430fbf 100644 --- a/src/plugins/visualize/public/services.ts +++ b/src/plugins/visualize/public/services.ts @@ -20,8 +20,8 @@ import { ApplicationStart, IUiSettingsClient } from '../../../core/public'; import { createGetterSetter } from '../../../plugins/kibana_utils/public'; import { IndexPatternsContract, DataPublicPluginStart } from '../../../plugins/data/public'; +import { VisEditorConstructor } from '../../../plugins/visualizations/public'; import { SharePluginStart } from '../../../plugins/share/public'; -import { VisEditorConstructor } from './application/types'; export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); diff --git a/test/common/config.js b/test/common/config.js index 6c7d64e3e0bc0..6809b87bc807d 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -44,7 +44,14 @@ export default function () { '--logging.json=false', `--server.port=${kbnTestConfig.getPort()}`, '--status.allowAnonymous=true', - `--elasticsearch.hosts=${formatUrl(servers.elasticsearch)}`, + // We shouldn't embed credentials into the URL since Kibana requests to Elasticsearch should + // either include `kibanaServerTestUser` credentials, or credentials provided by the test + // user, or none at all in case anonymous access is used. + `--elasticsearch.hosts=${formatUrl( + Object.fromEntries( + Object.entries(servers.elasticsearch).filter(([key]) => key.toLowerCase() !== 'auth') + ) + )}`, `--elasticsearch.username=${kibanaServerTestUser.username}`, `--elasticsearch.password=${kibanaServerTestUser.password}`, `--home.disableWelcomeScreen=true`, diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 5b07cb0e534db..7541d9e5cf3d7 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -61,6 +61,9 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro let isPresent = false; await retry.try(async () => { isPresent = await testSubjects.exists(testSubj, { timeout: 20000 }); + if (!isPresent) { + isPresent = await testSubjects.exists('visNoResult', { timeout: 1000 }); + } }); if (!isPresent) { throw new Error(`TSVB ${name} tab is not loaded`); diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index 7e5ef068e2184..8ae7a6b50c111 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -33,6 +33,7 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft const log = getService('log'); const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['header', 'common']); + const inspector = getService('inspector'); return new (class DashboardPanelActions { async findContextMenu(parent?: WebElementWrapper) { @@ -163,6 +164,16 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft await this.openInspector(header); } + async getSearchSessionIdByTitle(title: string) { + await this.openInspectorByTitle(title); + await inspector.openInspectorRequestsView(); + const searchSessionId = await ( + await testSubjects.find('inspectorRequestSearchSessionId') + ).getAttribute('data-search-session-id'); + await inspector.close(); + return searchSessionId; + } + async openInspector(parent?: WebElementWrapper) { await this.openContextMenu(parent); const exists = await testSubjects.exists(OPEN_INSPECTOR_TEST_SUBJ); diff --git a/test/scripts/checks/mocha_coverage.sh b/test/scripts/checks/mocha_coverage.sh deleted file mode 100755 index e1afad0ab775f..0000000000000 --- a/test/scripts/checks/mocha_coverage.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn nyc --reporter=html --reporter=json-summary --report-dir=./target/kibana-coverage/mocha node scripts/mocha diff --git a/test/scripts/jenkins_unit.sh b/test/scripts/jenkins_unit.sh index 7384cec36b277..2edd66579f72f 100755 --- a/test/scripts/jenkins_unit.sh +++ b/test/scripts/jenkins_unit.sh @@ -44,8 +44,4 @@ else rename_coverage_file "oss-integration" echo "" echo "" - echo " -> Running mocha tests with coverage" - ./test/scripts/checks/mocha_coverage.sh - echo "" - echo "" fi diff --git a/test/tsconfig.json b/test/tsconfig.json index f9008505ed66e..f1d5115bf35fb 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -8,8 +8,12 @@ "exclude": ["plugin_functional/plugins/**/*", "interpreter_functional/plugins/**/*"], "references": [ { "path": "../src/core/tsconfig.json" }, + { "path": "../src/plugins/advanced_settings/tsconfig.json" }, { "path": "../src/plugins/management/tsconfig.json" }, { "path": "../src/plugins/bfetch/tsconfig.json" }, + { "path": "../src/plugins/charts/tsconfig.json" }, + { "path": "../src/plugins/dashboard/tsconfig.json" }, + { "path": "../src/plugins/discover/tsconfig.json" }, { "path": "../src/plugins/embeddable/tsconfig.json" }, { "path": "../src/plugins/expressions/tsconfig.json" }, { "path": "../src/plugins/home/tsconfig.json" }, @@ -24,6 +28,7 @@ { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "../src/plugins/telemetry/tsconfig.json" }, { "path": "../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../src/plugins/url_forwarding/tsconfig.json" }, { "path": "../src/plugins/usage_collection/tsconfig.json" }, ] } diff --git a/tsconfig.json b/tsconfig.json index 20e2e57ce654e..1d8c61d515fdf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,9 +7,12 @@ "exclude": [ "src/**/__fixtures__/**/*", "src/core/**/*", - "src/plugins/management/**/*", + "src/plugins/advanced_settings/**/*", "src/plugins/apm_oss/**/*", "src/plugins/bfetch/**/*", + "src/plugins/charts/**/*", + "src/plugins/dashboard/**/*", + "src/plugins/discover/**/*", "src/plugins/data/**/*", "src/plugins/dev_tools/**/*", "src/plugins/embeddable/**/*", @@ -20,17 +23,20 @@ "src/plugins/kibana_react/**/*", "src/plugins/kibana_usage_collection/**/*", "src/plugins/kibana_utils/**/*", + "src/plugins/management/**/*", "src/plugins/navigation/**/*", "src/plugins/newsfeed/**/*", "src/plugins/saved_objects/**/*", - "src/plugins/security_oss/**/*", "src/plugins/saved_objects_tagging_oss/**/*", + "src/plugins/security_oss/**/*", "src/plugins/share/**/*", + "src/plugins/spaces_oss/**/*", "src/plugins/telemetry/**/*", "src/plugins/telemetry_collection_manager/**/*", "src/plugins/ui_actions/**/*", "src/plugins/url_forwarding/**/*", - "src/plugins/usage_collection/**/*" + "src/plugins/usage_collection/**/*", + "src/plugins/presentation_util/**/*" // In the build we actually exclude **/public/**/* from this config so that // we can run the TSC on both this and the .browser version of this config // file, but if we did it during development IDEs would not be able to find @@ -39,9 +45,12 @@ ], "references": [ { "path": "./src/core/tsconfig.json" }, - { "path": "./src/plugins/management/tsconfig.json"}, + { "path": "./src/plugins/advanced_settings/tsconfig.json" }, { "path": "./src/plugins/apm_oss/tsconfig.json" }, { "path": "./src/plugins/bfetch/tsconfig.json" }, + { "path": "./src/plugins/charts/tsconfig.json" }, + { "path": "./src/plugins/dashboard/tsconfig.json" }, + { "path": "./src/plugins/discover/tsconfig.json" }, { "path": "./src/plugins/data/tsconfig.json" }, { "path": "./src/plugins/dev_tools/tsconfig.json" }, { "path": "./src/plugins/embeddable/tsconfig.json" }, @@ -52,12 +61,14 @@ { "path": "./src/plugins/kibana_react/tsconfig.json" }, { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, + { "path": "./src/plugins/management/tsconfig.json" }, { "path": "./src/plugins/navigation/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, { "path": "./src/plugins/saved_objects/tsconfig.json" }, { "path": "./src/plugins/saved_objects_tagging_oss/tsconfig.json" }, { "path": "./src/plugins/security_oss/tsconfig.json" }, { "path": "./src/plugins/share/tsconfig.json" }, + { "path": "./src/plugins/spaces_oss/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "./src/plugins/ui_actions/tsconfig.json" }, diff --git a/tsconfig.refs.json b/tsconfig.refs.json index c27d2ff2ec6f0..d6e87da6ac19d 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -2,24 +2,32 @@ "include": [], "references": [ { "path": "./src/core/tsconfig.json" }, + { "path": "./src/plugins/advanced_settings/tsconfig.json" }, { "path": "./src/plugins/apm_oss/tsconfig.json" }, { "path": "./src/plugins/bfetch/tsconfig.json" }, + { "path": "./src/plugins/charts/tsconfig.json" }, { "path": "./src/plugins/data/tsconfig.json" }, { "path": "./src/plugins/dev_tools/tsconfig.json" }, + { "path": "./src/plugins/discover/tsconfig.json" }, { "path": "./src/plugins/embeddable/tsconfig.json" }, { "path": "./src/plugins/expressions/tsconfig.json" }, { "path": "./src/plugins/home/tsconfig.json" }, + { "path": "./src/plugins/dashboard/tsconfig.json" }, + { "path": "./src/plugins/dev_tools/tsconfig.json" }, { "path": "./src/plugins/inspector/tsconfig.json" }, { "path": "./src/plugins/kibana_legacy/tsconfig.json" }, { "path": "./src/plugins/kibana_react/tsconfig.json" }, { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, + { "path": "./src/plugins/management/tsconfig.json" }, { "path": "./src/plugins/navigation/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, { "path": "./src/plugins/saved_objects/tsconfig.json" }, { "path": "./src/plugins/saved_objects_tagging_oss/tsconfig.json" }, + { "path": "./src/plugins/presentation_util/tsconfig.json" }, { "path": "./src/plugins/security_oss/tsconfig.json" }, { "path": "./src/plugins/share/tsconfig.json" }, + { "path": "./src/plugins/spaces_oss/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "./src/plugins/ui_actions/tsconfig.json" }, diff --git a/vars/kibanaCoverage.groovy b/vars/kibanaCoverage.groovy index 521672e4bf48c..019eb8088dbfc 100644 --- a/vars/kibanaCoverage.groovy +++ b/vars/kibanaCoverage.groovy @@ -60,7 +60,6 @@ def uploadCoverageHtmls(prefix) { [ 'target/kibana-coverage/functional-combined', 'target/kibana-coverage/jest-combined', - 'target/kibana-coverage/mocha-combined', ].each { uploadWithVault(prefix, it) } } @@ -78,7 +77,6 @@ def prokLinks(title) { kibanaPipeline.bash(''' cat << EOF > src/dev/code_coverage/www/index_partial_2.html Latest Jest - Latest Mocha Latest FTR
@@ -151,7 +149,6 @@ def generateReports(title) { . src/dev/code_coverage/shell_scripts/extract_archives.sh . src/dev/code_coverage/shell_scripts/fix_html_reports_parallel.sh . src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh - . src/dev/code_coverage/shell_scripts/copy_mocha_reports.sh # zip combined reports tar -czf kibana-coverage.tar.gz target/kibana-coverage/**/* """, title) diff --git a/x-pack/examples/embedded_lens_example/.eslintrc.json b/x-pack/examples/embedded_lens_example/.eslintrc.json new file mode 100644 index 0000000000000..2aab6c2d9093b --- /dev/null +++ b/x-pack/examples/embedded_lens_example/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "@typescript-eslint/consistent-type-definitions": 0 + } +} diff --git a/x-pack/examples/embedded_lens_example/README.md b/x-pack/examples/embedded_lens_example/README.md new file mode 100644 index 0000000000000..ac9ba99569c6d --- /dev/null +++ b/x-pack/examples/embedded_lens_example/README.md @@ -0,0 +1,13 @@ +# Embedded Lens examples + +To run this example plugin, use the command `yarn start --run-examples`. + +This example shows how to embed Lens into other applications. Using the `EmbeddableComponent` of the `lens` start plugin, +you can pass in a valid Lens configuration which will get rendered the same way Lens dashboard panels work. Updating the +configuration will reload the embedded visualization. + +## Link to editor + +It is possible to use the same configuration and the `navigateToPrefilledEditor` method to navigate the current user to a +prefilled Lens editor so they can manipulate the configuration on their own and even save the results to a dashboard. +Make sure to always check permissions using `canUseEditor` whether the current user has permissions to access Lens. \ No newline at end of file diff --git a/x-pack/examples/embedded_lens_example/kibana.json b/x-pack/examples/embedded_lens_example/kibana.json new file mode 100644 index 0000000000000..5e4caead90bc3 --- /dev/null +++ b/x-pack/examples/embedded_lens_example/kibana.json @@ -0,0 +1,15 @@ +{ + "id": "embeddedLensExample", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["embedded_lens_example"], + "server": false, + "ui": true, + "requiredPlugins": [ + "lens", + "data", + "developerExamples" + ], + "optionalPlugins": [], + "requiredBundles": [] +} diff --git a/x-pack/examples/embedded_lens_example/package.json b/x-pack/examples/embedded_lens_example/package.json new file mode 100644 index 0000000000000..f66a0d5adba4e --- /dev/null +++ b/x-pack/examples/embedded_lens_example/package.json @@ -0,0 +1,14 @@ +{ + "name": "embedded_lens_example", + "version": "1.0.0", + "main": "target/examples/embedded_lens_example", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../node_modules/.bin/tsc" + } +} \ No newline at end of file diff --git a/x-pack/examples/embedded_lens_example/public/app.tsx b/x-pack/examples/embedded_lens_example/public/app.tsx new file mode 100644 index 0000000000000..9f35907ca335d --- /dev/null +++ b/x-pack/examples/embedded_lens_example/public/app.tsx @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, +} from '@elastic/eui'; +import { IndexPattern } from 'src/plugins/data/public'; +import { CoreStart } from 'kibana/public'; +import { TypedLensByValueInput } from '../../../plugins/lens/public'; +import { StartDependencies } from './plugin'; + +// Generate a Lens state based on some app-specific input parameters. +// `TypedLensByValueInput` can be used for type-safety - it uses the same interfaces as Lens-internal code. +function getLensAttributes( + defaultIndexPattern: IndexPattern, + color: string +): TypedLensByValueInput['attributes'] { + return { + visualizationType: 'lnsXY', + title: 'Prefilled from example app', + references: [ + { + id: defaultIndexPattern.id!, + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: defaultIndexPattern.id!, + name: 'indexpattern-datasource-layer-layer1', + type: 'index-pattern', + }, + ], + state: { + datasourceStates: { + indexpattern: { + layers: { + layer1: { + columnOrder: ['col1', 'col2'], + columns: { + col2: { + dataType: 'number', + isBucketed: false, + label: 'Count of records', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + col1: { + dataType: 'date', + isBucketed: true, + label: '@timestamp', + operationType: 'date_histogram', + params: { interval: 'auto' }, + scale: 'interval', + sourceField: defaultIndexPattern.timeFieldName!, + }, + }, + }, + }, + }, + }, + filters: [], + query: { language: 'kuery', query: '' }, + visualization: { + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + fittingFunction: 'None', + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + layers: [ + { + accessors: ['col2'], + layerId: 'layer1', + seriesType: 'bar_stacked', + xAccessor: 'col1', + yConfig: [{ forAccessor: 'col2', color }], + }, + ], + legend: { isVisible: true, position: 'right' }, + preferredSeriesType: 'bar_stacked', + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + valueLabels: 'hide', + }, + }, + }; +} + +export const App = (props: { + core: CoreStart; + plugins: StartDependencies; + defaultIndexPattern: IndexPattern | null; +}) => { + const [color, setColor] = useState('green'); + const LensComponent = props.plugins.lens.EmbeddableComponent; + return ( + + + + + +

Embedded Lens vis

+
+
+
+ + +

+ This app embeds a Lens visualization by specifying the configuration. Data fetching + and rendering is completely managed by Lens itself. +

+

+ The Change color button will update the configuration by picking a new random color of + the series which causes Lens to re-render. The Edit button will take the current + configuration and navigate to a prefilled editor. +

+ {props.defaultIndexPattern && props.defaultIndexPattern.isTimeBased() ? ( + <> + + + { + // eslint-disable-next-line no-bitwise + const newColor = '#' + ((Math.random() * 0xffffff) << 0).toString(16); + setColor(newColor); + }} + > + Change color + + + + { + props.plugins.lens.navigateToPrefilledEditor({ + id: '', + timeRange: { + from: 'now-5d', + to: 'now', + }, + attributes: getLensAttributes(props.defaultIndexPattern!, color), + }); + // eslint-disable-next-line no-bitwise + const newColor = '#' + ((Math.random() * 0xffffff) << 0).toString(16); + setColor(newColor); + }} + > + Edit + + + + + + ) : ( +

This demo only works if your default index pattern is set and time based

+ )} +
+
+
+
+ ); +}; diff --git a/x-pack/plugins/apm/public/utils/isValidCoordinateValue.ts b/x-pack/examples/embedded_lens_example/public/index.ts similarity index 59% rename from x-pack/plugins/apm/public/utils/isValidCoordinateValue.ts rename to x-pack/examples/embedded_lens_example/public/index.ts index c36efc232b782..b9a263d5e6e88 100644 --- a/x-pack/plugins/apm/public/utils/isValidCoordinateValue.ts +++ b/x-pack/examples/embedded_lens_example/public/index.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Maybe } from '../../typings/common'; -export const isValidCoordinateValue = (value: Maybe): value is number => - value !== null && value !== undefined; +import { EmbeddedLensExamplePlugin } from './plugin'; + +export const plugin = () => new EmbeddedLensExamplePlugin(); diff --git a/x-pack/examples/embedded_lens_example/public/mount.tsx b/x-pack/examples/embedded_lens_example/public/mount.tsx new file mode 100644 index 0000000000000..21d0a01df1636 --- /dev/null +++ b/x-pack/examples/embedded_lens_example/public/mount.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { CoreSetup, AppMountParameters } from 'kibana/public'; +import { StartDependencies } from './plugin'; + +export const mount = (coreSetup: CoreSetup) => async ({ + element, +}: AppMountParameters) => { + const [core, plugins] = await coreSetup.getStartServices(); + const { App } = await import('./app'); + + const deps = { + core, + plugins, + }; + + const defaultIndexPattern = await plugins.data.indexPatterns.getDefault(); + + const reactElement = ; + render(reactElement, element); + return () => unmountComponentAtNode(element); +}; diff --git a/x-pack/examples/embedded_lens_example/public/plugin.ts b/x-pack/examples/embedded_lens_example/public/plugin.ts new file mode 100644 index 0000000000000..2f2e988d42025 --- /dev/null +++ b/x-pack/examples/embedded_lens_example/public/plugin.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreSetup, AppNavLinkStatus } from '../../../../src/core/public'; +import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { LensPublicStart } from '../../../plugins/lens/public'; +import { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public'; +import { mount } from './mount'; + +export interface SetupDependencies { + developerExamples: DeveloperExamplesSetup; +} + +export interface StartDependencies { + data: DataPublicPluginStart; + lens: LensPublicStart; +} + +export class EmbeddedLensExamplePlugin + implements Plugin { + public setup(core: CoreSetup, { developerExamples }: SetupDependencies) { + core.application.register({ + id: 'embedded_lens_example', + title: 'Embedded Lens example', + navLinkStatus: AppNavLinkStatus.hidden, + mount: mount(core), + }); + + developerExamples.register({ + appId: 'embedded_lens_example', + title: 'Embedded Lens', + description: + 'Embed Lens visualizations into other applications and link to a pre-configured Lens editor to allow users to use visualizations in your app as starting points for further explorations.', + links: [ + { + label: 'README', + href: + 'https://github.com/elastic/kibana/tree/master/x-pack/examples/embedded_lens_example', + iconType: 'logoGithub', + size: 's', + target: '_blank', + }, + ], + }); + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/examples/embedded_lens_example/tsconfig.json b/x-pack/examples/embedded_lens_example/tsconfig.json new file mode 100644 index 0000000000000..2bf577e87041c --- /dev/null +++ b/x-pack/examples/embedded_lens_example/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../typings/**/*" + ], + "exclude": [], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/share/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts index 7e938e766657c..32e1b233274c9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -5,10 +5,11 @@ */ import axios from 'axios'; -import HttpProxyAgent from 'http-proxy-agent'; import { Logger } from '../../../../../../src/core/server'; import { addTimeZoneToDate, request, patch, getErrorMessage } from './axios_utils'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { getProxyAgents } from './get_proxy_agents'; + const logger = loggingSystemMock.create().get() as jest.Mocked; jest.mock('axios'); const axiosMock = (axios as unknown) as jest.Mock; @@ -27,6 +28,7 @@ describe('addTimeZoneToDate', () => { describe('request', () => { beforeEach(() => { + jest.resetAllMocks(); axiosMock.mockImplementation(() => ({ status: 200, headers: { 'content-type': 'application/json' }, @@ -58,23 +60,57 @@ describe('request', () => { }); }); - test('it have been called with proper proxy agent', async () => { + test('it have been called with proper proxy agent for a valid url', async () => { + const proxySettings = { + proxyRejectUnauthorizedCertificates: true, + proxyUrl: 'https://localhost:1212', + }; + const { httpAgent, httpsAgent } = getProxyAgents(proxySettings, logger); + const res = await request({ axios, - url: '/testProxy', + url: 'http://testProxy', logger, proxySettings: { - proxyUrl: 'http://localhost:1212', + proxyUrl: 'https://localhost:1212', + proxyRejectUnauthorizedCertificates: true, + }, + }); + + expect(axiosMock).toHaveBeenCalledWith('http://testProxy', { + method: 'get', + data: {}, + headers: undefined, + httpAgent, + httpsAgent, + params: undefined, + proxy: false, + validateStatus: undefined, + }); + expect(res).toEqual({ + status: 200, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + }); + }); + + test('it have been called with proper proxy agent for an invalid url', async () => { + const res = await request({ + axios, + url: 'https://testProxy', + logger, + proxySettings: { + proxyUrl: ':nope:', proxyRejectUnauthorizedCertificates: false, }, }); - expect(axiosMock).toHaveBeenCalledWith('/testProxy', { + expect(axiosMock).toHaveBeenCalledWith('https://testProxy', { method: 'get', data: {}, headers: undefined, - httpAgent: new HttpProxyAgent('http://localhost:1212'), - httpsAgent: new HttpProxyAgent('http://localhost:1212'), + httpAgent: undefined, + httpsAgent: undefined, params: undefined, proxy: false, validateStatus: undefined, diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts index e26a3b686179c..322da1077af18 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts @@ -7,7 +7,7 @@ import { AxiosInstance, Method, AxiosResponse, AxiosBasicCredentials } from 'axios'; import { Logger } from '../../../../../../src/core/server'; import { ProxySettings } from '../../types'; -import { getProxyAgent } from './get_proxy_agent'; +import { getProxyAgents } from './get_proxy_agents'; export const request = async ({ axios, @@ -32,15 +32,17 @@ export const request = async ({ validateStatus?: (status: number) => boolean; auth?: AxiosBasicCredentials; }): Promise => { + const { httpAgent, httpsAgent } = getProxyAgents(proxySettings, logger); + return await axios(url, { method, data: data ?? {}, params, auth, - // use httpsAgent and embedded proxy: false, to be able to handle fail on invalid certs - httpsAgent: proxySettings ? getProxyAgent(proxySettings, logger) : undefined, - httpAgent: proxySettings ? getProxyAgent(proxySettings, logger) : undefined, - proxy: false, // the same way as it done for IncomingWebhook in + // use httpAgent and httpsAgent and set axios proxy: false, to be able to handle fail on invalid certs + httpAgent, + httpsAgent, + proxy: false, headers, validateStatus, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.test.ts deleted file mode 100644 index 8623a67e8a68e..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import HttpProxyAgent from 'http-proxy-agent'; -import { HttpsProxyAgent } from 'https-proxy-agent'; -import { Logger } from '../../../../../../src/core/server'; -import { getProxyAgent } from './get_proxy_agent'; -import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; -const logger = loggingSystemMock.create().get() as jest.Mocked; - -describe('getProxyAgent', () => { - test('return HttpsProxyAgent for https proxy url', () => { - const agent = getProxyAgent( - { proxyUrl: 'https://someproxyhost', proxyRejectUnauthorizedCertificates: false }, - logger - ); - expect(agent instanceof HttpsProxyAgent).toBeTruthy(); - }); - - test('return HttpProxyAgent for http proxy url', () => { - const agent = getProxyAgent( - { proxyUrl: 'http://someproxyhost', proxyRejectUnauthorizedCertificates: false }, - logger - ); - expect(agent instanceof HttpProxyAgent).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.ts deleted file mode 100644 index 957d31546b019..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import HttpProxyAgent from 'http-proxy-agent'; -import { HttpsProxyAgent } from 'https-proxy-agent'; -import { Logger } from '../../../../../../src/core/server'; -import { ProxySettings } from '../../types'; - -export function getProxyAgent( - proxySettings: ProxySettings, - logger: Logger -): HttpsProxyAgent | HttpProxyAgent { - logger.debug(`Create proxy agent for ${proxySettings.proxyUrl}.`); - - if (/^https/i.test(proxySettings.proxyUrl)) { - const proxyUrl = new URL(proxySettings.proxyUrl); - return new HttpsProxyAgent({ - host: proxyUrl.hostname, - port: Number(proxyUrl.port), - protocol: proxyUrl.protocol, - headers: proxySettings.proxyHeaders, - // do not fail on invalid certs if value is false - rejectUnauthorized: proxySettings.proxyRejectUnauthorizedCertificates, - }); - } else { - return new HttpProxyAgent(proxySettings.proxyUrl); - } -} diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agents.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agents.test.ts new file mode 100644 index 0000000000000..759ca92968263 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agents.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import HttpProxyAgent from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { Logger } from '../../../../../../src/core/server'; +import { getProxyAgents } from './get_proxy_agents'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +const logger = loggingSystemMock.create().get() as jest.Mocked; + +describe('getProxyAgents', () => { + test('get agents for valid proxy URL', () => { + const { httpAgent, httpsAgent } = getProxyAgents( + { proxyUrl: 'https://someproxyhost', proxyRejectUnauthorizedCertificates: false }, + logger + ); + expect(httpAgent instanceof HttpProxyAgent).toBeTruthy(); + expect(httpsAgent instanceof HttpsProxyAgent).toBeTruthy(); + }); + + test('return undefined agents for invalid proxy URL', () => { + const { httpAgent, httpsAgent } = getProxyAgents( + { proxyUrl: ':nope: not a valid URL', proxyRejectUnauthorizedCertificates: false }, + logger + ); + expect(httpAgent).toBe(undefined); + expect(httpsAgent).toBe(undefined); + }); + + test('return undefined agents for null proxy options', () => { + const { httpAgent, httpsAgent } = getProxyAgents(null, logger); + expect(httpAgent).toBe(undefined); + expect(httpsAgent).toBe(undefined); + }); + + test('return undefined agents for undefined proxy options', () => { + const { httpAgent, httpsAgent } = getProxyAgents(undefined, logger); + expect(httpAgent).toBe(undefined); + expect(httpsAgent).toBe(undefined); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agents.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agents.ts new file mode 100644 index 0000000000000..45f962429ad2b --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agents.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Agent } from 'http'; +import HttpProxyAgent from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { Logger } from '../../../../../../src/core/server'; +import { ProxySettings } from '../../types'; + +interface GetProxyAgentsResponse { + httpAgent: Agent | undefined; + httpsAgent: Agent | undefined; +} + +export function getProxyAgents( + proxySettings: ProxySettings | undefined | null, + logger: Logger +): GetProxyAgentsResponse { + const undefinedResponse = { + httpAgent: undefined, + httpsAgent: undefined, + }; + + if (!proxySettings) { + return undefinedResponse; + } + + logger.debug(`Creating proxy agents for proxy: ${proxySettings.proxyUrl}`); + let proxyUrl: URL; + try { + proxyUrl = new URL(proxySettings.proxyUrl); + } catch (err) { + logger.warn(`invalid proxy URL "${proxySettings.proxyUrl}" ignored`); + return undefinedResponse; + } + + const httpAgent = new HttpProxyAgent(proxySettings.proxyUrl); + const httpsAgent = (new HttpsProxyAgent({ + host: proxyUrl.hostname, + port: Number(proxyUrl.port), + protocol: proxyUrl.protocol, + headers: proxySettings.proxyHeaders, + // do not fail on invalid certs if value is false + rejectUnauthorized: proxySettings.proxyRejectUnauthorizedCertificates, + }) as unknown) as Agent; + // vsCode wasn't convinced HttpsProxyAgent is an http.Agent, so we convinced it + + return { httpAgent, httpsAgent }; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index c9a3c39afd049..07ea7b62f3606 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -6,8 +6,7 @@ import { URL } from 'url'; import { curry } from 'lodash'; -import { HttpsProxyAgent } from 'https-proxy-agent'; -import HttpProxyAgent from 'http-proxy-agent'; +import { Agent } from 'http'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { IncomingWebhook, IncomingWebhookResult } from '@slack/webhook'; @@ -24,7 +23,7 @@ import { ExecutorType, } from '../types'; import { ActionsConfigurationUtilities } from '../actions_config'; -import { getProxyAgent } from './lib/get_proxy_agent'; +import { getProxyAgents } from './lib/get_proxy_agents'; export type SlackActionType = ActionType<{}, ActionTypeSecretsType, ActionParamsType, unknown>; export type SlackActionTypeExecutorOptions = ActionTypeExecutorOptions< @@ -128,9 +127,13 @@ async function slackExecutor( const { webhookUrl } = secrets; const { message } = params; - let proxyAgent: HttpsProxyAgent | HttpProxyAgent | undefined; + let httpProxyAgent: Agent | undefined; if (execOptions.proxySettings) { - proxyAgent = getProxyAgent(execOptions.proxySettings, logger); + const httpProxyAgents = getProxyAgents(execOptions.proxySettings, logger); + httpProxyAgent = webhookUrl.toLowerCase().startsWith('https') + ? httpProxyAgents.httpsAgent + : httpProxyAgents.httpAgent; + logger.debug(`IncomingWebhook was called with proxyUrl ${execOptions.proxySettings.proxyUrl}`); } @@ -138,8 +141,7 @@ async function slackExecutor( // https://slack.dev/node-slack-sdk/webhook // node-slack-sdk use Axios inside :) const webhook = new IncomingWebhook(webhookUrl, { - // @ts-expect-error The types exposed by 'HttpsProxyAgent' isn't up to date with 'Agent' - agent: proxyAgent, + agent: httpProxyAgent, }); result = await webhook.send(message); } catch (err) { diff --git a/x-pack/plugins/actions/server/manual_tests/forward_proxy.js b/x-pack/plugins/actions/server/manual_tests/forward_proxy.js new file mode 100644 index 0000000000000..5c48b9d2393ab --- /dev/null +++ b/x-pack/plugins/actions/server/manual_tests/forward_proxy.js @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* +This module implements two forward http proxies, http on 8080 and https on 8443, +which can be used with the config xpack.actions.proxyUrl to emulate customers +using forward proxies with Kibana actions. You can use either the http or https +versions, both can forward proxy http and https traffic: + + xpack.actions.proxyUrl: http://localhost:8080 + OR + xpack.actions.proxyUrl: https://localhost:8443 + +When using the https-based version, you may need to set the following option +as well: + + xpack.actions.rejectUnauthorized: false + +If the server you are connecting to via the proxy is https and has self-signed +certificates, you'll also need to set + + xpack.actions.proxyRejectUnauthorizedCertificates: false +*/ + +const HTTP_PORT = 8080; +const HTTPS_PORT = 8443; + +// starts http and https proxies to use to test actions within Kibana + +const fs = require('fs'); +const net = require('net'); +const url = require('url'); +const http = require('http'); +const https = require('https'); +const httpProxy = require('http-proxy'); + +const httpsOptions = { + key: fs.readFileSync('packages/kbn-dev-utils/certs/kibana.key', 'utf8'), + cert: fs.readFileSync('packages/kbn-dev-utils/certs/kibana.crt', 'utf8'), +}; + +const proxy = httpProxy.createServer(); + +createServer('http', HTTP_PORT); +createServer('https', HTTPS_PORT); + +function createServer(protocol, port) { + let httpServer; + + if (protocol === 'http') { + httpServer = http.createServer(); + } else { + httpServer = https.createServer(httpsOptions); + } + + httpServer.on('request', httpRequest); + httpServer.on('connect', httpsRequest); + httpServer.listen(port); + log(`proxy server started: ${protocol}:/localhost:${port}`); + + // handle http requests + function httpRequest(req, res) { + log(`${protocol} server: request for: ${req.url}`); + const parsedUrl = url.parse(req.url); + if (parsedUrl.hostname == null) { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('this is a proxy server'); + return; + } + const target = parsedUrl.protocol + '//' + parsedUrl.hostname; + proxy.web(req, res, { target: target, secure: false }); + } + + // handle https requests + // see: https://nodejs.org/dist/latest-v14.x/docs/api/http.html#http_event_connect + function httpsRequest(req, socket, head) { + log(`${protocol} proxy server: request for target: https://${req.url}`); + const serverUrl = url.parse('https://' + req.url); + const serverSocket = net.connect(serverUrl.port, serverUrl.hostname, () => { + socket.write('HTTP/1.1 200 Connection Established\r\nProxy-agent: Node-Proxy\r\n\r\n'); + serverSocket.write(head); + serverSocket.pipe(socket); + socket.pipe(serverSocket); + }); + socket.on('error', (err) => { + log(`error on socket to proxy: ${err}`); + socket.destroy(); + serverSocket.destroy(); + }); + serverSocket.on('error', (err) => { + log(`error on socket to target: ${err}`); + socket.destroy(); + serverSocket.destroy(); + }); + } +} + +function log(message) { + console.log(`${new Date().toISOString()} - ${message}`); +} + +/* +Test with: + +curl -v -k --proxy-insecure -x http://127.0.0.1:8080 http://www.google.com +curl -v -k --proxy-insecure -x http://127.0.0.1:8080 https://www.google.com +curl -v -k --proxy-insecure -x https://127.0.0.1:8443 http://www.google.com +curl -v -k --proxy-insecure -x https://127.0.0.1:8443 https://www.google.com +*/ diff --git a/x-pack/plugins/apm/common/utils/formatters/formatters.ts b/x-pack/plugins/apm/common/utils/formatters/formatters.ts index 50ce9db096610..7b21a686209ae 100644 --- a/x-pack/plugins/apm/common/utils/formatters/formatters.ts +++ b/x-pack/plugins/apm/common/utils/formatters/formatters.ts @@ -8,11 +8,19 @@ import { Maybe } from '../../../typings/common'; import { NOT_AVAILABLE_LABEL } from '../../i18n'; import { isFiniteNumber } from '../is_finite_number'; -export function asDecimal(value: number) { +export function asDecimal(value?: number | null) { + if (!isFiniteNumber(value)) { + return NOT_AVAILABLE_LABEL; + } + return numeral(value).format('0,0.0'); } -export function asInteger(value: number) { +export function asInteger(value?: number | null) { + if (!isFiniteNumber(value)) { + return NOT_AVAILABLE_LABEL; + } + return numeral(value).format('0,0'); } diff --git a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx index 88a897d7baf50..83bbf49691be9 100644 --- a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useCallback, useMemo } from 'react'; +import { useParams } from 'react-router-dom'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { AlertType } from '../../../../common/alert_types'; +import { getInitialAlertValues } from '../get_initial_alert_values'; import { TriggersAndActionsUIPublicPluginStart } from '../../../../../triggers_actions_ui/public'; - interface Props { addFlyoutVisible: boolean; setAddFlyoutVisibility: React.Dispatch>; @@ -20,10 +21,13 @@ interface KibanaDeps { export function AlertingFlyout(props: Props) { const { addFlyoutVisible, setAddFlyoutVisibility, alertType } = props; + const { serviceName } = useParams<{ serviceName?: string }>(); const { services: { triggersActionsUi }, } = useKibana(); + const initialValues = getInitialAlertValues(alertType, serviceName); + const onCloseAddFlyout = useCallback(() => setAddFlyoutVisibility(false), [ setAddFlyoutVisibility, ]); @@ -36,7 +40,9 @@ export function AlertingFlyout(props: Props) { onClose: onCloseAddFlyout, alertTypeId: alertType, canChangeTrigger: false, + initialValues, }), + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [alertType, onCloseAddFlyout, triggersActionsUi] ); return <>{addFlyoutVisible && addAlertFlyout}; diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx index cce973f8587da..d7375d14e17cf 100644 --- a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx @@ -8,7 +8,6 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { useParams } from 'react-router-dom'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; -import { AlertType, ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { asInteger } from '../../../../common/utils/formatters'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; @@ -110,7 +109,6 @@ export function ErrorCountAlertTrigger(props: Props) { return ( { + expect(getInitialAlertValues(null, undefined)).toEqual({ tags: ['apm'] }); +}); + +test('handles valid alert type', () => { + const alertType = AlertType.ErrorCount; + expect(getInitialAlertValues(alertType, undefined)).toEqual({ + name: ALERT_TYPES_CONFIG[alertType].name, + tags: ['apm'], + }); + + expect(getInitialAlertValues(alertType, 'Service Name')).toEqual({ + name: `${ALERT_TYPES_CONFIG[alertType].name} | Service Name`, + tags: ['apm', `service.name:service name`], + }); +}); diff --git a/x-pack/plugins/apm/public/components/alerting/get_initial_alert_values.ts b/x-pack/plugins/apm/public/components/alerting/get_initial_alert_values.ts new file mode 100644 index 0000000000000..3655c9f90c4bf --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/get_initial_alert_values.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; + +export function getInitialAlertValues( + alertType: AlertType | null, + serviceName: string | undefined +) { + const alertTypeName = alertType + ? ALERT_TYPES_CONFIG[alertType].name + : undefined; + const alertName = alertTypeName + ? serviceName + ? `${alertTypeName} | ${serviceName}` + : alertTypeName + : undefined; + const tags = ['apm']; + if (serviceName) { + tags.push(`service.name:${serviceName}`.toLowerCase()); + } + + return { + tags, + ...(alertName ? { name: alertName } : {}), + }; +} diff --git a/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/index.tsx index 0a12f79bf61a9..a04679bcb689d 100644 --- a/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/index.tsx @@ -9,7 +9,6 @@ import React, { useEffect } from 'react'; import { useParams } from 'react-router-dom'; interface Props { - alertTypeName: string; setAlertParams: (key: string, value: any) => void; setAlertProperty: (key: string, value: any) => void; defaults: Record; @@ -20,14 +19,7 @@ interface Props { export function ServiceAlertTrigger(props: Props) { const { serviceName } = useParams<{ serviceName?: string }>(); - const { - fields, - setAlertParams, - setAlertProperty, - alertTypeName, - defaults, - chartPreview, - } = props; + const { fields, setAlertParams, defaults, chartPreview } = props; const params: Record = { ...defaults, @@ -36,17 +28,6 @@ export function ServiceAlertTrigger(props: Props) { useEffect(() => { // we only want to run this on mount to set default values - - const alertName = params.serviceName - ? `${alertTypeName} | ${params.serviceName}` - : alertTypeName; - setAlertProperty('name', alertName); - - const tags = ['apm']; - if (params.serviceName) { - tags.push(`service.name:${params.serviceName}`.toLowerCase()); - } - setAlertProperty('tags', tags); Object.keys(params).forEach((key) => { setAlertParams(key, params[key]); }); diff --git a/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/service_alert_trigger.test.tsx b/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/service_alert_trigger.test.tsx index 72611043bbed3..7f9a27e884e8e 100644 --- a/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/service_alert_trigger.test.tsx +++ b/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/service_alert_trigger.test.tsx @@ -18,7 +18,6 @@ describe('ServiceAlertTrigger', () => { expect(() => render( {}} diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx index 22840bc2e6ed0..7c0a74f2e1b60 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { useParams } from 'react-router-dom'; import { useFetcher } from '../../../../../observability/public'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; -import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { getDurationFormatter } from '../../../../common/utils/formatters'; import { TimeSeries } from '../../../../typings/timeseries'; @@ -203,7 +202,6 @@ export function TransactionDurationAlertTrigger(props: Props) { return ( ['items'][0]; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.test.tsx similarity index 89% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.test.tsx index 40b8a57f55051..2bfa820ee5e76 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.test.tsx @@ -6,8 +6,8 @@ import { fireEvent, render } from '@testing-library/react'; import React from 'react'; -import { expectTextsInDocument } from '../../../../../utils/testHelpers'; -import { ErrorCount } from '../ErrorCount'; +import { expectTextsInDocument } from '../../../../utils/testHelpers'; +import { ErrorCount } from './ErrorCount'; describe('ErrorCount', () => { it('shows singular error message', () => { diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx index 9a40d7834d18a..49a016f338888 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { EuiButton, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { Transaction as ITransaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; +import { TransactionDetailLink } from '../../../shared/Links/apm/transaction_detail_link'; import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; export const MaybeViewTraceLink = ({ @@ -18,6 +19,9 @@ export const MaybeViewTraceLink = ({ transaction: ITransaction; waterfall: IWaterfall; }) => { + const { + urlParams: { latencyAggregationType }, + } = useUrlParams(); const viewFullTraceButtonLabel = i18n.translate( 'xpack.apm.transactionDetails.viewFullTraceButtonLabel', { @@ -77,6 +81,7 @@ export const MaybeViewTraceLink = ({ traceId={rootTransaction.trace.id} transactionName={rootTransaction.transaction.name} transactionType={rootTransaction.transaction.type} + latencyAggregationType={latencyAggregationType} > {viewFullTraceButtonLabel} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx index 0568930f8157d..a67ec0a69ed87 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx @@ -6,20 +6,25 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; +import { useUrlParams } from '../../../../../../context/url_params_context/use_url_params'; import { SERVICE_NAME, TRANSACTION_NAME, } from '../../../../../../../common/elasticsearch_fieldnames'; import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; -import { TransactionDetailLink } from '../../../../../shared/Links/apm/TransactionDetailLink'; -import { StickyProperties } from '../../../../../shared/StickyProperties'; import { ServiceOrTransactionsOverviewLink } from '../../../../../shared/Links/apm/service_transactions_overview_link'; +import { TransactionDetailLink } from '../../../../../shared/Links/apm/transaction_detail_link'; +import { StickyProperties } from '../../../../../shared/StickyProperties'; interface Props { transaction?: Transaction; } export function FlyoutTopLevelProperties({ transaction }: Props) { + const { + urlParams: { latencyAggregationType }, + } = useUrlParams(); + if (!transaction) { return null; } @@ -51,6 +56,7 @@ export function FlyoutTopLevelProperties({ transaction }: Props) { traceId={transaction.trace.id} transactionName={transaction.transaction.name} transactionType={transaction.transaction.type} + latencyAggregationType={latencyAggregationType} > {transaction.transaction.name} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx index 6bcb9a764a352..5a1f6e3d2a24d 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx @@ -6,17 +6,18 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; +import { useUrlParams } from '../../../../../../../context/url_params_context/use_url_params'; import { + SERVICE_NAME, SPAN_NAME, TRANSACTION_NAME, - SERVICE_NAME, } from '../../../../../../../../common/elasticsearch_fieldnames'; import { NOT_AVAILABLE_LABEL } from '../../../../../../../../common/i18n'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; -import { StickyProperties } from '../../../../../../shared/StickyProperties'; +import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; import { ServiceOrTransactionsOverviewLink } from '../../../../../../shared/Links/apm/service_transactions_overview_link'; -import { TransactionDetailLink } from '../../../../../../shared/Links/apm/TransactionDetailLink'; +import { TransactionDetailLink } from '../../../../../../shared/Links/apm/transaction_detail_link'; +import { StickyProperties } from '../../../../../../shared/StickyProperties'; interface Props { span: Span; @@ -24,6 +25,9 @@ interface Props { } export function StickySpanProperties({ span, transaction }: Props) { + const { + urlParams: { latencyAggregationType }, + } = useUrlParams(); const spanName = span.span.name; const transactionStickyProperties = transaction ? [ @@ -56,6 +60,7 @@ export function StickySpanProperties({ span, transaction }: Props) { traceId={transaction.trace.id} transactionName={transaction.transaction.name} transactionType={transaction.transaction.type} + latencyAggregationType={latencyAggregationType} > {transaction.transaction.name} diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx index 958d25a88434c..fe3cb541617a3 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx @@ -11,6 +11,7 @@ import { isJavaAgentName, isRumAgentName } from '../../../../common/agent_name'; import { enableServiceOverview } from '../../../../common/ui_settings_keys'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useErrorOverviewHref } from '../../shared/Links/apm/ErrorOverviewLink'; import { useMetricOverviewHref } from '../../shared/Links/apm/MetricOverviewLink'; import { useServiceMapHref } from '../../shared/Links/apm/ServiceMapLink'; @@ -46,6 +47,9 @@ interface Props { export function ServiceDetailTabs({ serviceName, tab }: Props) { const { agentName } = useApmServiceContext(); const { uiSettings } = useApmPluginContext().core; + const { + urlParams: { latencyAggregationType }, + } = useUrlParams(); const overviewTab = { key: 'overview', @@ -60,7 +64,7 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { const transactionsTab = { key: 'transactions', - href: useTransactionsOverviewHref(serviceName), + href: useTransactionsOverviewHref({ serviceName, latencyAggregationType }), text: i18n.translate('xpack.apm.serviceDetails.transactionsTabLabel', { defaultMessage: 'Transactions', }), diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index c02f72245cdf5..46c2a4c322c92 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -23,6 +23,7 @@ import { ServiceOverview } from './'; import { waitFor } from '@testing-library/dom'; import * as callApmApiModule from '../../../services/rest/createCallApmApi'; import * as useApmServiceContextHooks from '../../../context/apm_service/use_apm_service_context'; +import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiCounter: () => {} }, @@ -45,7 +46,11 @@ function Wrapper({ children }: { children?: ReactNode }) { {children} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx index 7c90ea68d6f84..307997731e5ef 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx @@ -23,7 +23,6 @@ import { import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; -import { useLatencyAggregationType } from '../../../../hooks/use_latency_Aggregation_type'; import { APIReturnType, callApmApi, @@ -31,7 +30,7 @@ import { import { px, unit } from '../../../../style/variables'; import { SparkPlot } from '../../../shared/charts/spark_plot'; import { ImpactBar } from '../../../shared/ImpactBar'; -import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; +import { TransactionDetailLink } from '../../../shared/Links/apm/transaction_detail_link'; import { TransactionOverviewLink } from '../../../shared/Links/apm/transaction_overview_link'; import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; @@ -54,10 +53,16 @@ const DEFAULT_SORT = { field: 'impact' as const, }; -function getLatencyAggregationTypeLabel( - latencyAggregationType?: LatencyAggregationType -) { +function getLatencyAggregationTypeLabel(latencyAggregationType?: string) { switch (latencyAggregationType) { + case 'avg': { + i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableColumnLatency.avg', + { + defaultMessage: 'Latency (avg.)', + } + ); + } case 'p95': { return i18n.translate( 'xpack.apm.serviceOverview.transactionsTableColumnLatency.p95', @@ -74,24 +79,15 @@ function getLatencyAggregationTypeLabel( } ); } - default: { - return i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableColumnLatency.avg', - { - defaultMessage: 'Latency (avg.)', - } - ); - } } } export function ServiceOverviewTransactionsTable(props: Props) { const { serviceName } = props; const { transactionType } = useApmServiceContext(); - const latencyAggregationType = useLatencyAggregationType(); const { uiFilters, - urlParams: { start, end }, + urlParams: { start, end, latencyAggregationType }, } = useUrlParams(); const [tableOptions, setTableOptions] = useState<{ @@ -135,7 +131,7 @@ export function ServiceOverviewTransactionsTable(props: Props) { sortField: tableOptions.sort.field, sortDirection: tableOptions.sort.direction, transactionType, - latencyAggregationType, + latencyAggregationType: latencyAggregationType as LatencyAggregationType, }, }, }).then((response) => { @@ -187,6 +183,7 @@ export function ServiceOverviewTransactionsTable(props: Props) { serviceName={serviceName} transactionName={name} transactionType={type} + latencyAggregationType={latencyAggregationType} > {name} @@ -282,7 +279,10 @@ export function ServiceOverviewTransactionsTable(props: Props) { - + {i18n.translate( 'xpack.apm.serviceOverview.transactionsTableLinkText', { diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx index ade0a0563b0dc..1699b7e7474fe 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx @@ -8,6 +8,7 @@ import { EuiToolTip, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import styled from 'styled-components'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asMillisecondDuration, @@ -18,7 +19,7 @@ import { ImpactBar } from '../../../shared/ImpactBar'; import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { EmptyMessage } from '../../../shared/EmptyMessage'; -import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; +import { TransactionDetailLink } from '../../../shared/Links/apm/transaction_detail_link'; type TransactionGroup = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups'>['items'][0]; @@ -40,6 +41,9 @@ interface Props { } export function TransactionList({ items, isLoading }: Props) { + const { + urlParams: { latencyAggregationType }, + } = useUrlParams(); const columns: Array> = useMemo( () => [ { @@ -58,6 +62,7 @@ export function TransactionList({ items, isLoading }: Props) { serviceName={serviceName} transactionName={transactionName} transactionType={transactionType} + latencyAggregationType={latencyAggregationType} > , }, ], - [] + [latencyAggregationType] ); const noItemsMessage = ( diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx deleted file mode 100644 index ee798e0208c2b..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { APMLink, APMLinkExtendProps } from './APMLink'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { pickKeys } from '../../../../../common/utils/pick_keys'; - -interface Props extends APMLinkExtendProps { - serviceName: string; - traceId?: string; - transactionId?: string; - transactionName: string; - transactionType: string; -} - -export function TransactionDetailLink({ - serviceName, - traceId, - transactionId, - transactionName, - transactionType, - ...rest -}: Props) { - const { urlParams } = useUrlParams(); - - const persistedFilters = pickKeys( - urlParams, - 'transactionResult', - 'serviceVersion' - ); - - return ( - - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_detail_link.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_detail_link.tsx new file mode 100644 index 0000000000000..8108dcf41321f --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_detail_link.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useLocation } from 'react-router-dom'; +import { EuiLink } from '@elastic/eui'; +import { getAPMHref, APMLinkExtendProps } from './APMLink'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; +import { APMQueryParams } from '../url_helpers'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; + +interface Props extends APMLinkExtendProps { + serviceName: string; + traceId?: string; + transactionId?: string; + transactionName: string; + transactionType: string; + latencyAggregationType?: string; +} + +const persistedFilters: Array = [ + 'transactionResult', + 'serviceVersion', +]; + +export function TransactionDetailLink({ + serviceName, + traceId, + transactionId, + transactionName, + transactionType, + latencyAggregationType, + ...rest +}: Props) { + const { urlParams } = useUrlParams(); + const { core } = useApmPluginContext(); + const location = useLocation(); + const href = getAPMHref({ + basePath: core.http.basePath, + path: `/services/${serviceName}/transactions/view`, + query: { + traceId, + transactionId, + transactionName, + transactionType, + ...(latencyAggregationType ? { latencyAggregationType } : {}), + ...pickKeys(urlParams as APMQueryParams, ...persistedFilters), + }, + search: location.search, + }); + + return ; +} diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.test.tsx index 3ab6feaf5ae12..5ca94884462db 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.test.tsx @@ -17,13 +17,11 @@ import { const history = createMemoryHistory(); -function wrapper({ queryParams }: { queryParams?: Record }) { - return ({ children }: { children: React.ReactElement }) => ( +function Wrapper({ children }: { children: React.ReactElement }) { + return ( - - {children} - + {children} ); @@ -32,18 +30,24 @@ function wrapper({ queryParams }: { queryParams?: Record }) { describe('Transactions overview link', () => { describe('useTransactionsOverviewHref', () => { it('returns transaction link', () => { - const { result } = renderHook(() => useTransactionsOverviewHref('foo'), { - wrapper: wrapper({}), - }); + const { result } = renderHook( + () => useTransactionsOverviewHref({ serviceName: 'foo' }), + { wrapper: Wrapper } + ); expect(result.current).toEqual( '/basepath/app/apm/services/foo/transactions' ); }); it('returns transaction link with persisted query items', () => { - const { result } = renderHook(() => useTransactionsOverviewHref('foo'), { - wrapper: wrapper({ queryParams: { latencyAggregationType: 'avg' } }), - }); + const { result } = renderHook( + () => + useTransactionsOverviewHref({ + serviceName: 'foo', + latencyAggregationType: 'avg', + }), + { wrapper: Wrapper } + ); expect(result.current).toEqual( '/basepath/app/apm/services/foo/transactions?latencyAggregationType=avg' ); @@ -55,13 +59,12 @@ describe('Transactions overview link', () => { .href; } it('returns transaction link', () => { - const Component = wrapper({}); const { container } = render( - + Service name - + ); expect(getHref(container)).toEqual( 'http://localhost/basepath/app/apm/services/foo/transactions' @@ -69,15 +72,15 @@ describe('Transactions overview link', () => { }); it('returns transaction link with persisted query items', () => { - const Component = wrapper({ - queryParams: { latencyAggregationType: 'avg' }, - }); const { container } = render( - - + + Service name - + ); expect(getHref(container)).toEqual( 'http://localhost/basepath/app/apm/services/foo/transactions?latencyAggregationType=avg' diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.tsx index cd6d70b2e2e6d..dd53c5ab15260 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.tsx @@ -6,22 +6,42 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; -import { APMQueryParams } from '../url_helpers'; -import { APMLinkExtendProps, useAPMHref } from './APMLink'; +import { useLocation } from 'react-router-dom'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { APMLinkExtendProps, getAPMHref } from './APMLink'; interface Props extends APMLinkExtendProps { serviceName: string; + latencyAggregationType?: string; } -const persistedFilters: Array = [ - 'latencyAggregationType', -]; +export function useTransactionsOverviewHref({ + serviceName, + latencyAggregationType, +}: { + serviceName: string; + latencyAggregationType?: string; +}) { + const { core } = useApmPluginContext(); + const location = useLocation(); + const { search } = location; -export function useTransactionsOverviewHref(serviceName: string) { - return useAPMHref(`/services/${serviceName}/transactions`, persistedFilters); + return getAPMHref({ + basePath: core.http.basePath, + path: `/services/${serviceName}/transactions`, + query: { ...(latencyAggregationType ? { latencyAggregationType } : {}) }, + search, + }); } -export function TransactionOverviewLink({ serviceName, ...rest }: Props) { - const href = useTransactionsOverviewHref(serviceName); +export function TransactionOverviewLink({ + serviceName, + latencyAggregationType, + ...rest +}: Props) { + const href = useTransactionsOverviewHref({ + serviceName, + latencyAggregationType, + }); return ; } diff --git a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts index aa3881b81cc3f..8576d9ee86353 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts @@ -6,7 +6,6 @@ import { History } from 'history'; import { parse, stringify } from 'query-string'; -import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; import { url } from '../../../../../../../src/plugins/kibana_utils/public'; import { LocalUIFilterName } from '../../../../common/ui_filter'; @@ -85,7 +84,7 @@ export type APMQueryParams = { refreshInterval?: string | number; searchTerm?: string; percentile?: 50 | 75 | 90 | 95 | 99; - latencyAggregationType?: LatencyAggregationType; + latencyAggregationType?: string; } & { [key in LocalUIFilterName]?: string }; // forces every value of T[K] to be type: string diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx index 6f62fd24e71ea..8033b6415319e 100644 --- a/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx @@ -102,8 +102,8 @@ function UnoptimizedManagedTable(props: Props) { ...toQuery(history.location.search), page: options.page.index, pageSize: options.page.size, - sortField: options.sort!.field, - sortDirection: options.sort!.direction, + sortField: options.sort?.field, + sortDirection: options.sort?.direction, }), }); }, diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx index 0a67db0f15b32..a32302d246423 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx @@ -56,7 +56,7 @@ export function CustomLinkMenuSection({ const { data: customLinks = [], status, refetch } = useFetcher( (callApmApi) => callApmApi({ - isCachable: true, + isCachable: false, endpoint: 'GET /api/apm/settings/custom_links', params: { query: convertFiltersToQuery(filters) }, }), diff --git a/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx index 506c27051b511..a135bff14d3c6 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx @@ -16,7 +16,6 @@ import { import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform_metrics_chart'; import { Maybe } from '../../../../../typings/common'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; -import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; import { TimeseriesChart } from '../timeseries_chart'; function getYTickFormatter(chart: GenericMetricsChart) { @@ -33,15 +32,13 @@ function getYTickFormatter(chart: GenericMetricsChart) { return (y: Maybe) => asPercent(y || 0, 1); } case 'time': { - return (y: Maybe) => asDuration(y); + return asDuration; } case 'integer': { - return (y: Maybe) => - isValidCoordinateValue(y) ? asInteger(y) : y; + return asInteger; } default: { - return (y: Maybe) => - isValidCoordinateValue(y) ? asDecimal(y) : y; + return asDecimal; } } } @@ -63,7 +60,7 @@ export function MetricsChart({ chart, fetchStatus }: Props) { fetchStatus={fetchStatus} id={chart.key} timeseries={chart.series} - yLabelFormat={getYTickFormatter(chart) as (y: number) => string} + yLabelFormat={getYTickFormatter(chart)} /> ); diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.test.ts b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.test.ts index e92ecd2aeefd8..6d7e279f47e04 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.test.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.test.ts @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - getResponseTimeTickFormatter, - getResponseTimeTooltipFormatter, - getMaxY, -} from './helper'; +import { getResponseTimeTickFormatter, getMaxY } from './helper'; import { TimeSeries, Coordinate } from '../../../../../typings/timeseries'; import { @@ -25,35 +21,26 @@ describe('transaction chart helper', () => { '1.0 min' ); }); + it('formattes time tick in seconds', () => { const formatter = getDurationFormatter(toMicroseconds(11, 'seconds')); const timeTickFormatter = getResponseTimeTickFormatter(formatter); expect(timeTickFormatter(toMicroseconds(6, 'seconds'))).toEqual('6.0 s'); }); }); - describe('getResponseTimeTooltipFormatter', () => { - const formatter = getDurationFormatter(toMicroseconds(11, 'minutes')); - const tooltipFormatter = getResponseTimeTooltipFormatter(formatter); - it("doesn't format invalid y coordinate", () => { - expect(tooltipFormatter({ x: 1, y: undefined })).toEqual('N/A'); - expect(tooltipFormatter({ x: 1, y: null })).toEqual('N/A'); - }); - it('formattes tooltip in minutes', () => { - expect( - tooltipFormatter({ x: 1, y: toMicroseconds(60, 'seconds') }) - ).toEqual('1.0 min'); - }); - }); + describe('getMaxY', () => { it('returns zero when empty time series', () => { expect(getMaxY([])).toEqual(0); }); + it('returns zero for invalid y coordinate', () => { const timeSeries = ([ { data: [{ x: 1 }, { x: 2 }, { x: 3, y: -1 }] }, ] as unknown) as Array>; expect(getMaxY(timeSeries)).toEqual(0); }); + it('returns the max y coordinate', () => { const timeSeries = ([ { diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.tsx index 4d2a60c494178..a8583b4b0dada 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.tsx @@ -4,23 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; -import { TimeFormatter } from '../../../../../common/utils/formatters'; import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; -import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; +import { TimeFormatter } from '../../../../../common/utils/formatters'; export function getResponseTimeTickFormatter(formatter: TimeFormatter) { - return (t: number) => { - return formatter(t).formatted; - }; -} - -export function getResponseTimeTooltipFormatter(formatter: TimeFormatter) { - return (coordinate: Coordinate) => { - return isValidCoordinateValue(coordinate.y) - ? formatter(coordinate.y).formatted - : NOT_AVAILABLE_LABEL; - }; + return (t: number) => formatter(t).formatted; } export function getMaxY(timeSeries?: Array>) { diff --git a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts index ee0ea7f601f62..6d9f982f92751 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts +++ b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts @@ -5,6 +5,7 @@ */ import { Location } from 'history'; +import { LatencyAggregationType } from '../../../common/latency_aggregation_types'; import { pickKeys } from '../../../common/utils/pick_keys'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { localUIFilterNames } from '../../../server/lib/ui_filters/local_ui_filters/config'; @@ -48,7 +49,7 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { environment, searchTerm, percentile, - latencyAggregationType, + latencyAggregationType = LatencyAggregationType.avg, } = query; const localUIFilters = pickKeys(query, ...localUIFilterNames); diff --git a/x-pack/plugins/apm/public/hooks/use_latency_Aggregation_type.test.ts b/x-pack/plugins/apm/public/hooks/use_latency_Aggregation_type.test.ts deleted file mode 100644 index 901877ca67460..0000000000000 --- a/x-pack/plugins/apm/public/hooks/use_latency_Aggregation_type.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { LatencyAggregationType } from '../../common/latency_aggregation_types'; -import { UIFilters } from '../../typings/ui_filters'; -import { IUrlParams } from '../context/url_params_context/types'; -import * as urlParams from '../context/url_params_context/use_url_params'; -import { useLatencyAggregationType } from './use_latency_Aggregation_type'; - -describe('useLatencyAggregationType', () => { - afterAll(() => { - jest.clearAllMocks(); - }); - it('returns avg when no value was given', () => { - jest.spyOn(urlParams, 'useUrlParams').mockReturnValue({ - urlParams: { latencyAggregationType: undefined } as IUrlParams, - refreshTimeRange: jest.fn(), - uiFilters: {} as UIFilters, - }); - const latencyAggregationType = useLatencyAggregationType(); - expect(latencyAggregationType).toEqual(LatencyAggregationType.avg); - }); - - it('returns avg when no value does not match any of the availabe options', () => { - jest.spyOn(urlParams, 'useUrlParams').mockReturnValue({ - urlParams: { latencyAggregationType: 'invalid_type' } as IUrlParams, - refreshTimeRange: jest.fn(), - uiFilters: {} as UIFilters, - }); - const latencyAggregationType = useLatencyAggregationType(); - expect(latencyAggregationType).toEqual(LatencyAggregationType.avg); - }); - - it('returns the value in the url', () => { - jest.spyOn(urlParams, 'useUrlParams').mockReturnValue({ - urlParams: { latencyAggregationType: 'p95' } as IUrlParams, - refreshTimeRange: jest.fn(), - uiFilters: {} as UIFilters, - }); - const latencyAggregationType = useLatencyAggregationType(); - expect(latencyAggregationType).toEqual(LatencyAggregationType.p95); - }); -}); diff --git a/x-pack/plugins/apm/public/hooks/use_latency_Aggregation_type.ts b/x-pack/plugins/apm/public/hooks/use_latency_Aggregation_type.ts deleted file mode 100644 index 72d07c9e4c22c..0000000000000 --- a/x-pack/plugins/apm/public/hooks/use_latency_Aggregation_type.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { LatencyAggregationType } from '../../common/latency_aggregation_types'; -import { useUrlParams } from '../context/url_params_context/use_url_params'; - -export function useLatencyAggregationType(): LatencyAggregationType { - const { - urlParams: { latencyAggregationType }, - } = useUrlParams(); - - if (!latencyAggregationType) { - return LatencyAggregationType.avg; - } - - if (latencyAggregationType in LatencyAggregationType) { - return latencyAggregationType as LatencyAggregationType; - } - - return LatencyAggregationType.avg; -} diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts index 7b1e7b06ac283..de3e68620b6e4 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts @@ -11,15 +11,14 @@ import { useUrlParams } from '../context/url_params_context/use_url_params'; import { useApmServiceContext } from '../context/apm_service/use_apm_service_context'; import { getLatencyChartSelector } from '../selectors/latency_chart_selectors'; import { useTheme } from './use_theme'; -import { useLatencyAggregationType } from './use_latency_Aggregation_type'; +import { LatencyAggregationType } from '../../common/latency_aggregation_types'; export function useTransactionLatencyChartsFetcher() { const { serviceName } = useParams<{ serviceName?: string }>(); const { transactionType } = useApmServiceContext(); - const latencyAggregationType = useLatencyAggregationType(); const theme = useTheme(); const { - urlParams: { start, end, transactionName }, + urlParams: { start, end, transactionName, latencyAggregationType }, uiFilters, } = useUrlParams(); @@ -43,7 +42,7 @@ export function useTransactionLatencyChartsFetcher() { transactionType, transactionName, uiFilters: JSON.stringify(uiFilters), - latencyAggregationType, + latencyAggregationType: latencyAggregationType as LatencyAggregationType, }, }, }); diff --git a/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts b/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts index dee92bbffd27a..a5c25cfa3e07c 100644 --- a/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts +++ b/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; import { rgba } from 'polished'; import { EuiTheme } from '../../../observability/public'; -import { LatencyAggregationType } from '../../common/latency_aggregation_types'; import { asDuration } from '../../common/utils/formatters'; import { Coordinate, @@ -33,7 +32,7 @@ export function getLatencyChartSelector({ }: { latencyChart?: LatencyChartsResponse; theme: EuiTheme; - latencyAggregationType?: LatencyAggregationType; + latencyAggregationType?: string; }): LatencyChart { if (!latencyChart?.latencyTimeseries || !latencyAggregationType) { return { @@ -63,7 +62,7 @@ function getLatencyTimeseries({ }: { latencyChart: LatencyChartsResponse; theme: EuiTheme; - latencyAggregationType: LatencyAggregationType; + latencyAggregationType: string; }) { const { overallAvgDuration } = latencyChart; const { latencyTimeseries } = latencyChart; diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts index 62fc16fb25053..6d91e64be034d 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts @@ -159,7 +159,7 @@ export async function createApmTelemetry({ logger.debug( `Stored telemetry is out of date. Task will run immediately. Stored: ${currentData.kibanaVersion}, expected: ${kibanaVersion}` ); - taskManagerStart.runNow(APM_TELEMETRY_TASK_NAME); + await taskManagerStart.runNow(APM_TELEMETRY_TASK_NAME); } } catch (err) { if (!SavedObjectsErrorHelpers.isNotFoundError(err)) { diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json index 38bbb074f6dbd..c14e8340957ad 100644 --- a/x-pack/plugins/canvas/kibana.json +++ b/x-pack/plugins/canvas/kibana.json @@ -5,7 +5,30 @@ "configPath": ["xpack", "canvas"], "server": true, "ui": true, - "requiredPlugins": ["bfetch", "data", "embeddable", "expressions", "features", "inspector", "uiActions", "charts"], - "optionalPlugins": ["usageCollection", "home"], - "requiredBundles": ["kibanaReact", "maps", "lens", "visualizations", "kibanaUtils", "kibanaLegacy", "discover", "savedObjects", "reporting", "home"] + "requiredPlugins": [ + "bfetch", + "charts", + "data", + "embeddable", + "expressions", + "features", + "inspector", + "uiActions" + ], + "optionalPlugins": [ + "home", + "usageCollection" + ], + "requiredBundles": [ + "discover", + "home", + "kibanaLegacy", + "kibanaReact", + "kibanaUtils", + "lens", + "maps", + "reporting", + "savedObjects", + "visualizations" + ] } diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__tests__/footer.test.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/footer.test.tsx similarity index 82% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__tests__/footer.test.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/footer.test.tsx index 9df94127ea51d..254c00f15b375 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/__tests__/footer.test.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/footer.test.tsx @@ -6,11 +6,11 @@ import { mount } from 'enzyme'; import React from 'react'; -import { JestContext } from '../../../test/context_jest'; -import { getScrubber as scrubber, getPageControlsCenter as center } from '../../../test/selectors'; -import { Footer } from '../footer'; +import { JestContext } from '../../test/context_jest'; +import { getScrubber as scrubber, getPageControlsCenter as center } from '../../test/selectors'; +import { Footer } from './footer'; -jest.mock('../../../supported_renderers'); +jest.mock('../../supported_renderers'); describe('