diff --git a/.github/workflows/pr-project-assigner.yml b/.github/workflows/pr-project-assigner.yml index 708c9efea404ba..ccaa5b11aa80c1 100644 --- a/.github/workflows/pr-project-assigner.yml +++ b/.github/workflows/pr-project-assigner.yml @@ -8,7 +8,7 @@ jobs: name: Assign a PR to project based on label steps: - name: Assign to project - uses: elastic/github-actions/project-assigner@v1.0.0 + uses: elastic/github-actions/project-assigner@v1.0.1 id: project_assigner with: issue-mappings: | diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index aec3bf88f0ee20..737da4f7fe3710 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -8,7 +8,7 @@ jobs: name: Assign issue or PR to project based on label steps: - name: Assign to project - uses: elastic/github-actions/project-assigner@v1.0.0 + uses: elastic/github-actions/project-assigner@v1.0.1 id: project_assigner with: issue-mappings: '[{"label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173895}, {"label": "Feature:Lens", "projectName": "Lens", "columnId": 6219363}, {"label": "Team:Canvas", "projectName": "canvas", "columnId": 6187593}]' diff --git a/docs/management/watcher-ui/index.asciidoc b/docs/management/watcher-ui/index.asciidoc index 79db96d759aa50..44610a2fd34269 100644 --- a/docs/management/watcher-ui/index.asciidoc +++ b/docs/management/watcher-ui/index.asciidoc @@ -34,7 +34,7 @@ If the {es} {security-features} are enabled, you must have the {ref}/security-privileges.html[`manage_watcher` or `monitor_watcher`] cluster privileges to use Watcher in {kib}. -Alternately, you can have the built-in `kibana_user` role +Alternately, you can have the built-in `kibana_admin` role and either of these watcher roles: * `watcher_admin`. You can perform all Watcher actions, including create and edit watches. diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index df4d8a0b65ee7f..146d4e97b6cf4a 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -80,6 +80,21 @@ specified explicitly. *Impact:* Any workflow that involved manually clearing generated bundles will have to be updated with the new path. +[float] +[[breaking_80_user_role_changes]] +=== User role changes + +[float] +==== `kibana_user` role has been removed and `kibana_admin` has been added. + +*Details:* The `kibana_user` role has been removed and `kibana_admin` has been added to better +reflect its intended use. This role continues to grant all access to every +{kib} feature. If you wish to restrict access to specific features, create +custom roles with {kibana-ref}/kibana-privileges.html[{kib} privileges]. + +*Impact:* Any users currently assigned the `kibana_user` role will need to +instead be assigned the `kibana_admin` role to maintain their current +access level. [float] [[breaking_80_reporting_changes]] diff --git a/docs/plugins/known-plugins.asciidoc b/docs/plugins/known-plugins.asciidoc index cc27eca4c267e5..cb70a1d1c387a4 100644 --- a/docs/plugins/known-plugins.asciidoc +++ b/docs/plugins/known-plugins.asciidoc @@ -20,6 +20,7 @@ This list of plugins is not guaranteed to work on your version of Kibana. Instea * https://github.com/johtani/analyze-api-ui-plugin[Analyze UI] (johtani) - UI for elasticsearch _analyze API * https://github.com/TrumanDu/cleaner[Cleaner] (TrumanDu)- Setting index ttl. * https://github.com/bitsensor/elastalert-kibana-plugin[ElastAlert Kibana Plugin] (BitSensor) - UI to create, test and edit ElastAlert rules +* https://github.com/query-ai/queryai-kibana-plugin[AI Analyst] (Query.AI) - App providing: NLP queries, automation, ML visualizations and insights [float] === Timelion Extensions diff --git a/docs/uptime-guide/security.asciidoc b/docs/uptime-guide/security.asciidoc index 2a960348b1e02c..6651b33ea0e0e8 100644 --- a/docs/uptime-guide/security.asciidoc +++ b/docs/uptime-guide/security.asciidoc @@ -42,7 +42,7 @@ PUT /_security/role/uptime === Assign the role to a user Next, you'll need to create a user with both the `uptime` role, and another role with sufficient {kibana-ref}/kibana-privileges.html[Kibana privileges], -such as the `kibana_user` role. +such as the `kibana_admin` role. You can do this with the following request: ["source","sh",subs="attributes,callouts"] @@ -50,7 +50,7 @@ You can do this with the following request: PUT /_security/user/jacknich { "password" : "j@rV1s", - "roles" : [ "uptime", "kibana_user" ], + "roles" : [ "uptime", "kibana_admin" ], "full_name" : "Jack Nicholson", "email" : "jacknich@example.com", "metadata" : { diff --git a/docs/user/monitoring/viewing-metrics.asciidoc b/docs/user/monitoring/viewing-metrics.asciidoc index 61bcb9a49c9012..11516e32400fb9 100644 --- a/docs/user/monitoring/viewing-metrics.asciidoc +++ b/docs/user/monitoring/viewing-metrics.asciidoc @@ -63,7 +63,7 @@ remote monitoring cluster, you must use credentials that are valid on both the -- -.. Create users that have the `monitoring_user` and `kibana_user` +.. Create users that have the `monitoring_user` and `kibana_admin` {ref}/built-in-roles.html[built-in roles]. . Open {kib} in your web browser. diff --git a/docs/user/security/authorization/index.asciidoc b/docs/user/security/authorization/index.asciidoc index 2636b3dfc1bd38..853c735418cea8 100644 --- a/docs/user/security/authorization/index.asciidoc +++ b/docs/user/security/authorization/index.asciidoc @@ -2,11 +2,11 @@ [[xpack-security-authorization]] === Granting access to {kib} -The Elastic Stack comes with the `kibana_user` {ref}/built-in-roles.html[built-in role], which you can use to grant access to all Kibana features in all spaces. To grant users access to a subset of spaces or features, you can create a custom role that grants the desired Kibana privileges. +The Elastic Stack comes with the `kibana_admin` {ref}/built-in-roles.html[built-in role], which you can use to grant access to all Kibana features in all spaces. To grant users access to a subset of spaces or features, you can create a custom role that grants the desired Kibana privileges. -When you assign a user multiple roles, the user receives a union of the roles’ privileges. Therefore, assigning the `kibana_user` role in addition to a custom role that grants Kibana privileges is ineffective because `kibana_user` has access to all the features in all spaces. +When you assign a user multiple roles, the user receives a union of the roles’ privileges. Therefore, assigning the `kibana_admin` role in addition to a custom role that grants Kibana privileges is ineffective because `kibana_admin` has access to all the features in all spaces. -NOTE: When running multiple tenants of Kibana by changing the `kibana.index` in your `kibana.yml`, you cannot use `kibana_user` to grant access. You must create custom roles that authorize the user for that specific tenant. Although multi-tenant installations are supported, the recommended approach to securing access to Kibana segments is to grant users access to specific spaces. +NOTE: When running multiple tenants of Kibana by changing the `kibana.index` in your `kibana.yml`, you cannot use `kibana_admin` to grant access. You must create custom roles that authorize the user for that specific tenant. Although multi-tenant installations are supported, the recommended approach to securing access to Kibana segments is to grant users access to specific spaces. [role="xpack"] === {kib} role management diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index 5f5d85fe8d3beb..825580bdc772ed 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -85,14 +85,14 @@ elasticsearch.username: 'custom_kibana_system' [[reporting-roles-user-api]] ==== With the user API This example uses the {ref}/security-api-put-user.html[user API] to create a user who has the -`reporting_user` role and the `kibana_user` role: +`reporting_user` role and the `kibana_admin` role: [source, sh] --------------------------------------------------------------- POST /_security/user/reporter { "password" : "x-pack-test-password", - "roles" : ["kibana_user", "reporting_user"], + "roles" : ["kibana_admin", "reporting_user"], "full_name" : "Reporting User" } --------------------------------------------------------------- @@ -106,11 +106,11 @@ roles on a per user basis, or assign roles to groups of users. By default, role mappings are configured in {ref}/mapping-roles.html[`config/shield/role_mapping.yml`]. For example, the following snippet assigns the user named Bill Murray the -`kibana_user` and `reporting_user` roles: +`kibana_admin` and `reporting_user` roles: [source,yaml] -------------------------------------------------------------------------------- -kibana_user: +kibana_admin: - "cn=Bill Murray,dc=example,dc=com" reporting_user: - "cn=Bill Murray,dc=example,dc=com" diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index 2d07b57bfabe1e..b6b5248777f6ba 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -104,7 +104,7 @@ You can manage privileges on the *Management / Security / Roles* page in {kib}. If you're using the native realm with Basic Authentication, you can assign roles using the *Management / Security / Users* page in {kib} or the {ref}/security-api.html#security-user-apis[user management APIs]. For example, -the following creates a user named `jacknich` and assigns it the `kibana_user` +the following creates a user named `jacknich` and assigns it the `kibana_admin` role: [source,js] @@ -112,7 +112,7 @@ role: POST /_security/user/jacknich { "password" : "t0pS3cr3t", - "roles" : [ "kibana_user" ] + "roles" : [ "kibana_admin" ] } -------------------------------------------------------------------------------- // CONSOLE diff --git a/docs/user/visualize.asciidoc b/docs/user/visualize.asciidoc index cfd2bac4989c11..5692fe6d1ae01d 100644 --- a/docs/user/visualize.asciidoc +++ b/docs/user/visualize.asciidoc @@ -75,6 +75,53 @@ modifications to the saved search are automatically reflected in the visualization. To disable automatic updates, you can disconnect a visualization from the saved search. +[float] +[[vis-inspector]] +== Inspect visualizations + +Many visualizations allow you to inspect the query and data behind the visualization. + +. In the {kib} toolbar, click *Inspect*. +. To download the data, click *Download CSV*, then choose one of the following options: +* *Formatted CSV* - Downloads the data in table format. +* *Raw CSV* - Downloads the data as provided. +. To view the requests for collecting data, select *Requests* from the *View* +dropdown. + +[float] +[[save-visualize]] +== Save visualizations +To use your visualizations in <>, you must save them. + +. In the {kib} toolbar, click *Save*. +. Enter the visualization *Title* and optional *Description*, then *Save* the visualization. + +To access the saved visualization, go to *Management > {kib} > Saved Objects*. + +[float] +[[save-visualization-read-only-access]] +==== Read only access +When you have insufficient privileges to save visualizations, the following indicator is +displayed and the *Save* button is not visible. + +For more information, refer to <>. + +[role="screenshot"] +image::visualize/images/read-only-badge.png[Example of Visualize's read only access indicator in Kibana's header] + +[float] +[[visualize-share-options]] +== Share visualizations + +When you've finished your visualization, you can share it outside of {kib}. + +From the *Share* menu, you can: + +* Embed the code in a web page. Users must have {kib} access +to view an embedded visualization. +* Share a direct link to a {kib} visualization. +* Generate a PDF report. +* Generate a PNG report. -- include::{kib-repo-dir}/visualize/visualize_rollup_data.asciidoc[] @@ -95,7 +142,3 @@ include::{kib-repo-dir}/visualize/heatmap.asciidoc[] include::{kib-repo-dir}/visualize/for-dashboard.asciidoc[] include::{kib-repo-dir}/visualize/vega.asciidoc[] - -include::{kib-repo-dir}/visualize/saving.asciidoc[] - -include::{kib-repo-dir}/visualize/inspector.asciidoc[] diff --git a/docs/visualize/inspector.asciidoc b/docs/visualize/inspector.asciidoc deleted file mode 100644 index ed98daea211e16..00000000000000 --- a/docs/visualize/inspector.asciidoc +++ /dev/null @@ -1,11 +0,0 @@ -[[vis-inspector]] -== Inspect visualizations - -Many visualizations allow you to inspect the query and data behind the visualization. - -. In the {kib} toolbar, click *Inspect*. -. To download the data, click *Download CSV*, then choose one of the following options: -* *Formatted CSV* - Downloads the data in table format. -* *Raw CSV* - Downloads the data as provided. -. To view the data collection requests, select *Requests* from the *View* -dropdown. diff --git a/docs/visualize/saving.asciidoc b/docs/visualize/saving.asciidoc deleted file mode 100644 index e3330446bfad16..00000000000000 --- a/docs/visualize/saving.asciidoc +++ /dev/null @@ -1,19 +0,0 @@ -[[save-visualize]] -== Save visualizations -To use your visualizations in <>, you must save them. - -. In the {kib} toolbar, click *Save*. -. Enter the visualization *Title* and optional *Description*, then *Save* the visualization. - -To access the saved visualization, go to *Management > {kib} > Saved Objects*. - -[float] -[[save-visualization-read-only-access]] -==== Read only access -When you have insufficient privileges to save visualizations, the following indicator is -displayed and the *Save* button is not visible. - -[role="screenshot"] -image::visualize/images/read-only-badge.png[Example of Visualize's read only access indicator in Kibana's header] - -For more information, see <>. diff --git a/package.json b/package.json index 425527a058e862..e249546e715812 100644 --- a/package.json +++ b/package.json @@ -454,7 +454,7 @@ "murmurhash3js": "3.0.1", "mutation-observer": "^1.0.3", "nock": "10.0.6", - "node-sass": "^4.9.4", + "node-sass": "^4.13.1", "normalize-path": "^3.0.0", "nyc": "^14.1.1", "pixelmatch": "^5.1.0", diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index 68af0aa791c8e0..3b358c03b8053a 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -24,7 +24,7 @@ "gulp-zip": "5.0.1", "inquirer": "^1.2.2", "minimatch": "^3.0.4", - "node-sass": "^4.9.4", + "node-sass": "^4.13.1", "through2": "^2.0.3", "through2-map": "^3.0.0", "vinyl-fs": "^3.0.3" diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts index 72926cae7dbc4b..96fd525efa3ec8 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts @@ -56,7 +56,7 @@ async function getSettingsFromFile(log: ToolingLog, path: string, settingOverrid return transformDeprecations(settingsWithDefaults, logDeprecation); } -export async function readConfigFile(log: ToolingLog, path: string, settingOverrides: any) { +export async function readConfigFile(log: ToolingLog, path: string, settingOverrides: any = {}) { return new Config({ settings: await getSettingsFromFile(log, path, settingOverrides), primary: true, diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index b4d9d3dfee03f6..4bb4c660a01aba 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -53,7 +53,7 @@ "jquery": "^3.4.1", "keymirror": "0.1.1", "moment": "^2.24.0", - "node-sass": "^4.9.4", + "node-sass": "^4.13.1", "postcss": "^7.0.5", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", diff --git a/src/core/public/application/ui/app_container.test.tsx b/src/core/public/application/ui/app_container.test.tsx new file mode 100644 index 00000000000000..a46243a2da4939 --- /dev/null +++ b/src/core/public/application/ui/app_container.test.tsx @@ -0,0 +1,88 @@ +/* + * 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 { mount } from 'enzyme'; + +import { AppContainer } from './app_container'; +import { Mounter, AppMountParameters, AppStatus } from '../types'; + +describe('AppContainer', () => { + const appId = 'someApp'; + const setAppLeaveHandler = jest.fn(); + + const flushPromises = async () => { + await new Promise(async resolve => { + setImmediate(() => resolve()); + }); + }; + + const createResolver = (): [Promise, () => void] => { + let resolve: () => void | undefined; + const promise = new Promise(r => { + resolve = r; + }); + return [promise, resolve!]; + }; + + const createMounter = (promise: Promise): Mounter => ({ + appBasePath: '/base-path', + appRoute: '/some-route', + unmountBeforeMounting: false, + mount: async ({ element }: AppMountParameters) => { + await promise; + const container = document.createElement('div'); + container.innerHTML = 'some-content'; + element.appendChild(container); + return () => container.remove(); + }, + }); + + it('should hide the "not found" page before mounting the route', async () => { + const [waitPromise, resolvePromise] = createResolver(); + const mounter = createMounter(waitPromise); + + const wrapper = mount( + + ); + + expect(wrapper.text()).toContain('Application Not Found'); + + wrapper.setProps({ + appId, + setAppLeaveHandler, + mounter, + appStatus: AppStatus.accessible, + }); + wrapper.update(); + + expect(wrapper.text()).toEqual(''); + + resolvePromise(); + await flushPromises(); + wrapper.update(); + + expect(wrapper.text()).toContain('some-content'); + }); +}); diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index 66c837d238276c..885157843e7df8 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -53,25 +53,27 @@ export const AppContainer: FunctionComponent = ({ unmountRef.current = null; } }; - const mount = async () => { - if (!mounter || appStatus !== AppStatus.accessible) { - return setAppNotFound(true); - } - if (mounter.unmountBeforeMounting) { - unmount(); - } + if (!mounter || appStatus !== AppStatus.accessible) { + return setAppNotFound(true); + } + setAppNotFound(false); + if (mounter.unmountBeforeMounting) { + unmount(); + } + + const mount = async () => { unmountRef.current = (await mounter.mount({ appBasePath: mounter.appBasePath, element: elementRef.current!, onAppLeave: handler => setAppLeaveHandler(appId, handler), })) || null; - setAppNotFound(false); }; mount(); + return unmount; }, [appId, appStatus, mounter, setAppLeaveHandler]); diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index 5f06c51a53d535..50866e5550d8ec 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -103,7 +103,19 @@ const configSchema = schema.object({ ), apiVersion: schema.string({ defaultValue: DEFAULT_API_VERSION }), healthCheck: schema.object({ delay: schema.duration({ defaultValue: 2500 }) }), - ignoreVersionMismatch: schema.boolean({ defaultValue: false }), + ignoreVersionMismatch: schema.conditional( + schema.contextRef('dev'), + false, + schema.boolean({ + validate: rawValue => { + if (rawValue === true) { + return '"ignoreVersionMismatch" can only be set to true in development mode'; + } + }, + defaultValue: false, + }), + schema.boolean({ defaultValue: false }) + ), }); const deprecations: ConfigDeprecationProvider = () => [ diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index a4e51ca55b3e71..b8ad3754965449 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -23,6 +23,7 @@ import { IScopedClusterClient } from './scoped_cluster_client'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; import { InternalElasticsearchServiceSetup, ElasticsearchServiceSetup } from './types'; +import { NodesVersionCompatibility } from './version_check/ensure_es_version'; const createScopedClusterClientMock = (): jest.Mocked => ({ callAsInternalUser: jest.fn(), @@ -71,6 +72,12 @@ type MockedInternalElasticSearchServiceSetup = jest.Mocked< const createInternalSetupContractMock = () => { const setupContract: MockedInternalElasticSearchServiceSetup = { ...createSetupContractMock(), + esNodesCompatibility$: new BehaviorSubject({ + isCompatible: true, + incompatibleNodes: [], + warningNodes: [], + kibanaVersion: '8.0.0', + }), legacy: { config$: new BehaviorSubject({} as ElasticsearchConfig), }, diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 5a7d223fec7ad9..022a03e01d37df 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -31,6 +31,7 @@ import { httpServiceMock } from '../http/http_service.mock'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; import { elasticsearchServiceMock } from './elasticsearch_service.mock'; +import { duration } from 'moment'; let elasticsearchService: ElasticsearchService; const configService = configServiceMock.create(); @@ -41,7 +42,7 @@ configService.atPath.mockReturnValue( new BehaviorSubject({ hosts: ['http://1.2.3.4'], healthCheck: { - delay: 2000, + delay: duration(2000), }, ssl: { verificationMode: 'none', @@ -125,7 +126,7 @@ describe('#setup', () => { const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` Object { - "healthCheckDelay": 2000, + "healthCheckDelay": "PT2S", "hosts": Array [ "http://8.8.8.8", ], @@ -150,7 +151,7 @@ Object { const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` Object { - "healthCheckDelay": 2000, + "healthCheckDelay": "PT2S", "hosts": Array [ "http://1.2.3.4", ], @@ -174,7 +175,7 @@ Object { new BehaviorSubject({ hosts: ['http://1.2.3.4', 'http://9.8.7.6'], healthCheck: { - delay: 2000, + delay: duration(2000), }, ssl: { verificationMode: 'none', @@ -196,7 +197,7 @@ Object { const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` Object { - "healthCheckDelay": 2000, + "healthCheckDelay": "PT2S", "hosts": Array [ "http://8.8.8.8", ], diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index de111e1cb8b9b9..9eaf125cc006fc 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -30,6 +30,7 @@ import { ElasticsearchConfig, ElasticsearchConfigType } from './elasticsearch_co import { InternalHttpServiceSetup, GetAuthHeaders } from '../http/'; import { InternalElasticsearchServiceSetup } from './types'; import { CallAPIOptions } from './api_types'; +import { pollEsNodesVersion } from './version_check/ensure_es_version'; /** @internal */ interface CoreClusterClients { @@ -46,9 +47,17 @@ interface SetupDeps { export class ElasticsearchService implements CoreService { private readonly log: Logger; private readonly config$: Observable; - private subscription?: Subscription; + private subscriptions: { + client?: Subscription; + esNodesCompatibility?: Subscription; + } = { + client: undefined, + esNodesCompatibility: undefined, + }; + private kibanaVersion: string; constructor(private readonly coreContext: CoreContext) { + this.kibanaVersion = coreContext.env.packageInfo.version; this.log = coreContext.logger.get('elasticsearch-service'); this.config$ = coreContext.configService .atPath('elasticsearch') @@ -60,7 +69,7 @@ export class ElasticsearchService implements CoreService { - if (this.subscription !== undefined) { + if (this.subscriptions.client !== undefined) { this.log.error('Clients cannot be changed after they are created'); return false; } @@ -91,7 +100,7 @@ export class ElasticsearchService implements CoreService; - this.subscription = clients$.connect(); + this.subscriptions.client = clients$.connect(); const config = await this.config$.pipe(first()).toPromise(); @@ -149,11 +158,31 @@ export class ElasticsearchService implements CoreService).connect(); + + // TODO: Move to Status Service https://github.com/elastic/kibana/issues/41983 + esNodesCompatibility$.subscribe(({ isCompatible, message }) => { + if (!isCompatible && message) { + this.log.error(message); + } + }); + return { legacy: { config$: clients$.pipe(map(clients => clients.config)) }, adminClient, dataClient, + esNodesCompatibility$, createClient: (type: string, clientConfig: Partial = {}) => { const finalConfig = merge({}, config, clientConfig); @@ -166,11 +195,12 @@ export class ElasticsearchService implements CoreService; }; + esNodesCompatibility$: Observable; } diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts new file mode 100644 index 00000000000000..4989c4a31295cb --- /dev/null +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts @@ -0,0 +1,261 @@ +/* + * 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 { mapNodesVersionCompatibility, pollEsNodesVersion, NodesInfo } from './ensure_es_version'; +import { loggingServiceMock } from '../../logging/logging_service.mock'; +import { take, delay } from 'rxjs/operators'; +import { TestScheduler } from 'rxjs/testing'; +import { of } from 'rxjs'; + +const mockLoggerFactory = loggingServiceMock.create(); +const mockLogger = mockLoggerFactory.get('mock logger'); + +const KIBANA_VERSION = '5.1.0'; + +function createNodes(...versions: string[]): NodesInfo { + const nodes = {} as any; + versions + .map(version => { + return { + version, + http: { + publish_address: 'http_address', + }, + ip: 'ip', + }; + }) + .forEach((node, i) => { + nodes[`node-${i}`] = node; + }); + + return { nodes }; +} + +describe('mapNodesVersionCompatibility', () => { + function createNodesInfoWithoutHTTP(version: string): NodesInfo { + return { nodes: { 'node-without-http': { version, ip: 'ip' } } } as any; + } + + it('returns isCompatible=true with a single node that matches', async () => { + const nodesInfo = createNodes('5.1.0'); + const result = await mapNodesVersionCompatibility(nodesInfo, KIBANA_VERSION, false); + expect(result.isCompatible).toBe(true); + }); + + it('returns isCompatible=true with multiple nodes that satisfy', async () => { + const nodesInfo = createNodes('5.1.0', '5.2.0', '5.1.1-Beta1'); + const result = await mapNodesVersionCompatibility(nodesInfo, KIBANA_VERSION, false); + expect(result.isCompatible).toBe(true); + }); + + it('returns isCompatible=false for a single node that is out of date', () => { + // 5.0.0 ES is too old to work with a 5.1.0 version of Kibana. + const nodesInfo = createNodes('5.1.0', '5.2.0', '5.0.0'); + const result = mapNodesVersionCompatibility(nodesInfo, KIBANA_VERSION, false); + expect(result.isCompatible).toBe(false); + expect(result.message).toMatchInlineSnapshot( + `"This version of Kibana (v5.1.0) is incompatible with the following Elasticsearch nodes in your cluster: v5.0.0 @ http_address (ip)"` + ); + }); + + it('returns isCompatible=false for an incompatible node without http publish address', async () => { + const nodesInfo = createNodesInfoWithoutHTTP('6.1.1'); + const result = mapNodesVersionCompatibility(nodesInfo, KIBANA_VERSION, false); + expect(result.isCompatible).toBe(false); + expect(result.message).toMatchInlineSnapshot( + `"This version of Kibana (v5.1.0) is incompatible with the following Elasticsearch nodes in your cluster: v6.1.1 @ undefined (ip)"` + ); + }); + + it('returns isCompatible=true for outdated nodes when ignoreVersionMismatch=true', async () => { + // 5.0.0 ES is too old to work with a 5.1.0 version of Kibana. + const nodesInfo = createNodes('5.1.0', '5.2.0', '5.0.0'); + const ignoreVersionMismatch = true; + const result = mapNodesVersionCompatibility(nodesInfo, KIBANA_VERSION, ignoreVersionMismatch); + expect(result.isCompatible).toBe(true); + expect(result.message).toMatchInlineSnapshot( + `"Ignoring version incompatibility between Kibana v5.1.0 and the following Elasticsearch nodes: v5.0.0 @ http_address (ip)"` + ); + }); + + it('returns isCompatible=true with a message if a node is only off by a patch version', () => { + const result = mapNodesVersionCompatibility(createNodes('5.1.1'), KIBANA_VERSION, false); + expect(result.isCompatible).toBe(true); + expect(result.message).toMatchInlineSnapshot( + `"You're running Kibana 5.1.0 with some different versions of Elasticsearch. Update Kibana or Elasticsearch to the same version to prevent compatibility issues: v5.1.1 @ http_address (ip)"` + ); + }); + + it('returns isCompatible=true with a message if a node is only off by a patch version and without http publish address', async () => { + const result = mapNodesVersionCompatibility(createNodes('5.1.1'), KIBANA_VERSION, false); + expect(result.isCompatible).toBe(true); + expect(result.message).toMatchInlineSnapshot( + `"You're running Kibana 5.1.0 with some different versions of Elasticsearch. Update Kibana or Elasticsearch to the same version to prevent compatibility issues: v5.1.1 @ http_address (ip)"` + ); + }); +}); + +describe('pollEsNodesVersion', () => { + const callWithInternalUser = jest.fn(); + const getTestScheduler = () => + new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + + beforeEach(() => { + callWithInternalUser.mockClear(); + }); + + it('returns iscCompatible=false and keeps polling when a poll request throws', done => { + expect.assertions(3); + const expectedCompatibilityResults = [false, false, true]; + jest.clearAllMocks(); + callWithInternalUser.mockResolvedValueOnce(createNodes('5.1.0', '5.2.0', '5.0.0')); + callWithInternalUser.mockRejectedValueOnce(new Error('mock request error')); + callWithInternalUser.mockResolvedValueOnce(createNodes('5.1.0', '5.2.0', '5.1.1-Beta1')); + pollEsNodesVersion({ + callWithInternalUser, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(3)) + .subscribe({ + next: result => { + expect(result.isCompatible).toBe(expectedCompatibilityResults.shift()); + }, + complete: done, + error: done, + }); + }); + + it('returns compatibility results', done => { + expect.assertions(1); + const nodes = createNodes('5.1.0', '5.2.0', '5.0.0'); + callWithInternalUser.mockResolvedValueOnce(nodes); + pollEsNodesVersion({ + callWithInternalUser, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(1)) + .subscribe({ + next: result => { + expect(result).toEqual(mapNodesVersionCompatibility(nodes, KIBANA_VERSION, false)); + }, + complete: done, + error: done, + }); + }); + + it('only emits if the node versions changed since the previous poll', done => { + expect.assertions(4); + callWithInternalUser.mockResolvedValueOnce(createNodes('5.1.0', '5.2.0', '5.0.0')); // emit + callWithInternalUser.mockResolvedValueOnce(createNodes('5.0.0', '5.1.0', '5.2.0')); // ignore, same versions, different ordering + callWithInternalUser.mockResolvedValueOnce(createNodes('5.1.1', '5.2.0', '5.0.0')); // emit + callWithInternalUser.mockResolvedValueOnce(createNodes('5.1.1', '5.1.2', '5.1.3')); // emit + callWithInternalUser.mockResolvedValueOnce(createNodes('5.1.1', '5.1.2', '5.1.3')); // ignore + callWithInternalUser.mockResolvedValueOnce(createNodes('5.0.0', '5.1.0', '5.2.0')); // emit, different from previous version + + pollEsNodesVersion({ + callWithInternalUser, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(4)) + .subscribe({ + next: result => expect(result).toBeDefined(), + complete: done, + error: done, + }); + }); + + it('starts polling immediately and then every esVersionCheckInterval', () => { + expect.assertions(1); + callWithInternalUser.mockReturnValueOnce([createNodes('5.1.0', '5.2.0', '5.0.0')]); + callWithInternalUser.mockReturnValueOnce([createNodes('5.1.1', '5.2.0', '5.0.0')]); + + getTestScheduler().run(({ expectObservable }) => { + const expected = 'a 99ms (b|)'; + + const esNodesCompatibility$ = pollEsNodesVersion({ + callWithInternalUser, + esVersionCheckInterval: 100, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }).pipe(take(2)); + + expectObservable(esNodesCompatibility$).toBe(expected, { + a: mapNodesVersionCompatibility( + createNodes('5.1.0', '5.2.0', '5.0.0'), + KIBANA_VERSION, + false + ), + b: mapNodesVersionCompatibility( + createNodes('5.1.1', '5.2.0', '5.0.0'), + KIBANA_VERSION, + false + ), + }); + }); + }); + + it('waits for es version check requests to complete before scheduling the next one', () => { + expect.assertions(2); + + getTestScheduler().run(({ expectObservable }) => { + const expected = '100ms a 99ms (b|)'; + + callWithInternalUser.mockReturnValueOnce( + of(createNodes('5.1.0', '5.2.0', '5.0.0')).pipe(delay(100)) + ); + callWithInternalUser.mockReturnValueOnce( + of(createNodes('5.1.1', '5.2.0', '5.0.0')).pipe(delay(100)) + ); + + const esNodesCompatibility$ = pollEsNodesVersion({ + callWithInternalUser, + esVersionCheckInterval: 10, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }).pipe(take(2)); + + expectObservable(esNodesCompatibility$).toBe(expected, { + a: mapNodesVersionCompatibility( + createNodes('5.1.0', '5.2.0', '5.0.0'), + KIBANA_VERSION, + false + ), + b: mapNodesVersionCompatibility( + createNodes('5.1.1', '5.2.0', '5.0.0'), + KIBANA_VERSION, + false + ), + }); + }); + + expect(callWithInternalUser).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.ts new file mode 100644 index 00000000000000..3e760ec0efabd6 --- /dev/null +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.ts @@ -0,0 +1,164 @@ +/* + * 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. + */ + +/** + * ES and Kibana versions are locked, so Kibana should require that ES has the same version as + * that defined in Kibana's package.json. + */ + +import { timer, of, from, Observable } from 'rxjs'; +import { map, distinctUntilChanged, catchError, exhaustMap } from 'rxjs/operators'; +import { + esVersionCompatibleWithKibana, + esVersionEqualsKibana, +} from './es_kibana_version_compatability'; +import { Logger } from '../../logging'; +import { APICaller } from '..'; + +export interface PollEsNodesVersionOptions { + callWithInternalUser: APICaller; + log: Logger; + kibanaVersion: string; + ignoreVersionMismatch: boolean; + esVersionCheckInterval: number; +} + +interface NodeInfo { + version: string; + ip: string; + http: { + publish_address: string; + }; + name: string; +} + +export interface NodesInfo { + nodes: { + [key: string]: NodeInfo; + }; +} + +export interface NodesVersionCompatibility { + isCompatible: boolean; + message?: string; + incompatibleNodes: NodeInfo[]; + warningNodes: NodeInfo[]; + kibanaVersion: string; +} + +function getHumanizedNodeName(node: NodeInfo) { + const publishAddress = node?.http?.publish_address + ' ' || ''; + return 'v' + node.version + ' @ ' + publishAddress + '(' + node.ip + ')'; +} + +export function mapNodesVersionCompatibility( + nodesInfo: NodesInfo, + kibanaVersion: string, + ignoreVersionMismatch: boolean +): NodesVersionCompatibility { + if (Object.keys(nodesInfo.nodes).length === 0) { + return { + isCompatible: false, + message: 'Unable to retrieve version information from Elasticsearch nodes.', + incompatibleNodes: [], + warningNodes: [], + kibanaVersion, + }; + } + const nodes = Object.keys(nodesInfo.nodes) + .sort() // Sorting ensures a stable node ordering for comparison + .map(key => nodesInfo.nodes[key]) + .map(node => Object.assign({}, node, { name: getHumanizedNodeName(node) })); + + // Aggregate incompatible ES nodes. + const incompatibleNodes = nodes.filter( + node => !esVersionCompatibleWithKibana(node.version, kibanaVersion) + ); + + // Aggregate ES nodes which should prompt a Kibana upgrade. It's acceptable + // if ES and Kibana versions are not the same as long as they are not + // incompatible, but we should warn about it. + // Ignore version qualifiers https://github.com/elastic/elasticsearch/issues/36859 + const warningNodes = nodes.filter(node => !esVersionEqualsKibana(node.version, kibanaVersion)); + + // Note: If incompatible and warning nodes are present `message` only contains + // an incompatibility notice. + let message; + if (incompatibleNodes.length > 0) { + const incompatibleNodeNames = incompatibleNodes.map(node => node.name).join(', '); + if (ignoreVersionMismatch) { + message = `Ignoring version incompatibility between Kibana v${kibanaVersion} and the following Elasticsearch nodes: ${incompatibleNodeNames}`; + } else { + message = `This version of Kibana (v${kibanaVersion}) is incompatible with the following Elasticsearch nodes in your cluster: ${incompatibleNodeNames}`; + } + } else if (warningNodes.length > 0) { + const warningNodeNames = warningNodes.map(node => node.name).join(', '); + message = + `You're running Kibana ${kibanaVersion} with some different versions of ` + + 'Elasticsearch. Update Kibana or Elasticsearch to the same ' + + `version to prevent compatibility issues: ${warningNodeNames}`; + } + + return { + isCompatible: ignoreVersionMismatch || incompatibleNodes.length === 0, + message, + incompatibleNodes, + warningNodes, + kibanaVersion, + }; +} + +// Returns true if two NodesVersionCompatibility entries match +function compareNodes(prev: NodesVersionCompatibility, curr: NodesVersionCompatibility) { + const nodesEqual = (n: NodeInfo, m: NodeInfo) => n.ip === m.ip && n.version === m.version; + return ( + curr.isCompatible === prev.isCompatible && + curr.incompatibleNodes.length === prev.incompatibleNodes.length && + curr.warningNodes.length === prev.warningNodes.length && + curr.incompatibleNodes.every((node, i) => nodesEqual(node, prev.incompatibleNodes[i])) && + curr.warningNodes.every((node, i) => nodesEqual(node, prev.warningNodes[i])) + ); +} + +export const pollEsNodesVersion = ({ + callWithInternalUser, + log, + kibanaVersion, + ignoreVersionMismatch, + esVersionCheckInterval: healthCheckInterval, +}: PollEsNodesVersionOptions): Observable => { + log.debug('Checking Elasticsearch version'); + return timer(0, healthCheckInterval).pipe( + exhaustMap(() => { + return from( + callWithInternalUser('nodes.info', { + filterPath: ['nodes.*.version', 'nodes.*.http.publish_address', 'nodes.*.ip'], + }) + ).pipe( + catchError(_err => { + return of({ nodes: {} }); + }) + ); + }), + map((nodesInfo: NodesInfo) => + mapNodesVersionCompatibility(nodesInfo, kibanaVersion, ignoreVersionMismatch) + ), + distinctUntilChanged(compareNodes) // Only emit if there are new nodes or versions + ); +}; diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/is_es_compatible_with_kibana.js b/src/core/server/elasticsearch/version_check/es_kibana_version_compatability.test.ts similarity index 72% rename from src/legacy/core_plugins/elasticsearch/server/lib/__tests__/is_es_compatible_with_kibana.js rename to src/core/server/elasticsearch/version_check/es_kibana_version_compatability.test.ts index 092c0ecf1071ca..152f25c8138819 100644 --- a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/is_es_compatible_with_kibana.js +++ b/src/core/server/elasticsearch/version_check/es_kibana_version_compatability.test.ts @@ -17,41 +17,39 @@ * under the License. */ -import expect from '@kbn/expect'; - -import isEsCompatibleWithKibana from '../is_es_compatible_with_kibana'; +import { esVersionCompatibleWithKibana } from './es_kibana_version_compatability'; describe('plugins/elasticsearch', () => { describe('lib/is_es_compatible_with_kibana', () => { describe('returns false', () => { it('when ES major is greater than Kibana major', () => { - expect(isEsCompatibleWithKibana('1.0.0', '0.0.0')).to.be(false); + expect(esVersionCompatibleWithKibana('1.0.0', '0.0.0')).toBe(false); }); it('when ES major is less than Kibana major', () => { - expect(isEsCompatibleWithKibana('0.0.0', '1.0.0')).to.be(false); + expect(esVersionCompatibleWithKibana('0.0.0', '1.0.0')).toBe(false); }); it('when majors are equal, but ES minor is less than Kibana minor', () => { - expect(isEsCompatibleWithKibana('1.0.0', '1.1.0')).to.be(false); + expect(esVersionCompatibleWithKibana('1.0.0', '1.1.0')).toBe(false); }); }); describe('returns true', () => { it('when version numbers are the same', () => { - expect(isEsCompatibleWithKibana('1.1.1', '1.1.1')).to.be(true); + expect(esVersionCompatibleWithKibana('1.1.1', '1.1.1')).toBe(true); }); it('when majors are equal, and ES minor is greater than Kibana minor', () => { - expect(isEsCompatibleWithKibana('1.1.0', '1.0.0')).to.be(true); + expect(esVersionCompatibleWithKibana('1.1.0', '1.0.0')).toBe(true); }); it('when majors and minors are equal, and ES patch is greater than Kibana patch', () => { - expect(isEsCompatibleWithKibana('1.1.1', '1.1.0')).to.be(true); + expect(esVersionCompatibleWithKibana('1.1.1', '1.1.0')).toBe(true); }); it('when majors and minors are equal, but ES patch is less than Kibana patch', () => { - expect(isEsCompatibleWithKibana('1.1.0', '1.1.1')).to.be(true); + expect(esVersionCompatibleWithKibana('1.1.0', '1.1.1')).toBe(true); }); }); }); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/is_es_compatible_with_kibana.js b/src/core/server/elasticsearch/version_check/es_kibana_version_compatability.ts similarity index 76% rename from src/legacy/core_plugins/elasticsearch/server/lib/is_es_compatible_with_kibana.js rename to src/core/server/elasticsearch/version_check/es_kibana_version_compatability.ts index 4afbd488d2946f..28b9c0a23e672d 100644 --- a/src/legacy/core_plugins/elasticsearch/server/lib/is_es_compatible_with_kibana.js +++ b/src/core/server/elasticsearch/version_check/es_kibana_version_compatability.ts @@ -17,15 +17,14 @@ * under the License. */ +import semver, { coerce } from 'semver'; + /** - * Let's weed out the ES versions that won't work with a given Kibana version. + * Checks for the compatibilitiy between Elasticsearch and Kibana versions * 1. Major version differences will never work together. * 2. Older versions of ES won't work with newer versions of Kibana. */ - -import semver from 'semver'; - -export default function isEsCompatibleWithKibana(esVersion, kibanaVersion) { +export function esVersionCompatibleWithKibana(esVersion: string, kibanaVersion: string) { const esVersionNumbers = { major: semver.major(esVersion), minor: semver.minor(esVersion), @@ -50,3 +49,9 @@ export default function isEsCompatibleWithKibana(esVersion, kibanaVersion) { return true; } + +export function esVersionEqualsKibana(nodeVersion: string, kibanaVersion: string) { + const nodeSemVer = coerce(nodeVersion); + const kibanaSemver = coerce(kibanaVersion); + return nodeSemVer && kibanaSemver && nodeSemVer.version === kibanaSemver.version; +} diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 65b8ba551cf916..425d8cac1893ea 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -43,7 +43,7 @@ describe('http service', () => { describe('auth', () => { let root: ReturnType; beforeEach(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ migrations: { skip: true } }); }, 30000); afterEach(async () => { @@ -161,7 +161,7 @@ describe('http service', () => { let root: ReturnType; beforeEach(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ migrations: { skip: true } }); }, 30000); afterEach(async () => { @@ -295,7 +295,7 @@ describe('http service', () => { describe('#basePath()', () => { let root: ReturnType; beforeEach(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ migrations: { skip: true } }); }, 30000); afterEach(async () => await root.shutdown()); @@ -324,7 +324,7 @@ describe('http service', () => { describe('elasticsearch', () => { let root: ReturnType; beforeEach(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ migrations: { skip: true } }); }, 30000); afterEach(async () => { diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts index b16352838fad11..6dc7ece1359df7 100644 --- a/src/core/server/http/integration_tests/lifecycle.test.ts +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -721,6 +721,7 @@ describe('Auth', () => { res.ok({ headers: { 'www-authenticate': 'from handler', + 'another-header': 'yet another header', }, }) ); diff --git a/src/core/server/http/lifecycle/on_pre_response.ts b/src/core/server/http/lifecycle/on_pre_response.ts index 45d7478df98051..50d3d7b47bf8d6 100644 --- a/src/core/server/http/lifecycle/on_pre_response.ts +++ b/src/core/server/http/lifecycle/on_pre_response.ts @@ -120,8 +120,8 @@ export function adoptToHapiOnPreResponseFormat(fn: OnPreResponseHandler, log: Lo ...(result.headers as any), // hapi types don't specify string[] as valid value }; } else { + findHeadersIntersection(response.headers, result.headers, log); for (const [headerName, headerValue] of Object.entries(result.headers)) { - findHeadersIntersection(response.headers, result.headers, log); response.header(headerName, headerValue as any); // hapi types don't specify string[] as valid value } } diff --git a/src/core/server/legacy/integration_tests/legacy_service.test.ts b/src/core/server/legacy/integration_tests/legacy_service.test.ts index da2550f2ae799a..e8bcf7a42d192f 100644 --- a/src/core/server/legacy/integration_tests/legacy_service.test.ts +++ b/src/core/server/legacy/integration_tests/legacy_service.test.ts @@ -22,7 +22,7 @@ describe('legacy service', () => { describe('http server', () => { let root: ReturnType; beforeEach(() => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ migrations: { skip: true } }); }, 30000); afterEach(async () => await root.shutdown()); diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index b89abc596ad18f..c6a72eb53d6c4f 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -59,12 +59,6 @@ describe('KibanaMigrator', () => { }); describe('runMigrations', () => { - it('resolves isMigrated if migrations were skipped', async () => { - const skipMigrations = true; - const result = await new KibanaMigrator(mockOptions()).runMigrations(skipMigrations); - expect(result).toEqual([{ status: 'skipped' }, { status: 'skipped' }]); - }); - it('only runs migrations once if called multiple times', async () => { const options = mockOptions(); const clusterStub = jest.fn(() => ({ status: 404 })); diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index c35e8dd90b5b14..747b48a540109e 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -107,24 +107,15 @@ export class KibanaMigrator { * The promise resolves with an array of migration statuses, one for each * elasticsearch index which was migrated. */ - public runMigrations(skipMigrations: boolean = false): Promise> { + public runMigrations(): Promise> { if (this.migrationResult === undefined) { - this.migrationResult = this.runMigrationsInternal(skipMigrations); + this.migrationResult = this.runMigrationsInternal(); } return this.migrationResult; } - private runMigrationsInternal(skipMigrations: boolean) { - if (skipMigrations) { - this.log.warn( - 'Skipping Saved Object migrations on startup. Note: Individual documents will still be migrated when read or written.' - ); - return Promise.resolve( - Object.keys(this.mappingProperties).map(() => ({ status: 'skipped' })) - ); - } - + private runMigrationsInternal() { const kibanaIndexName = this.kibanaConfig.index; const indexMap = createIndexMap({ config: this.config, diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index 6668d57045a957..19798aa68928db 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -31,11 +31,14 @@ import { configServiceMock } from '../mocks'; import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock'; import { legacyServiceMock } from '../legacy/legacy_service.mock'; import { SavedObjectsClientFactoryProvider } from './service/lib'; +import { BehaviorSubject } from 'rxjs'; +import { NodesVersionCompatibility } from '../elasticsearch/version_check/ensure_es_version'; describe('SavedObjectsService', () => { const createSetupDeps = () => { + const elasticsearchMock = elasticsearchServiceMock.createInternalSetup(); return { - elasticsearch: elasticsearchServiceMock.createInternalSetup(), + elasticsearch: elasticsearchMock, legacyPlugins: legacyServiceMock.createDiscoverPlugins(), }; }; @@ -137,7 +140,7 @@ describe('SavedObjectsService', () => { await soService.setup(createSetupDeps()); await soService.start({}); - expect(migratorInstanceMock.runMigrations).toHaveBeenCalledWith(true); + expect(migratorInstanceMock.runMigrations).not.toHaveBeenCalled(); }); it('skips KibanaMigrator migrations when migrations.skip=true', async () => { @@ -146,7 +149,38 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); await soService.setup(createSetupDeps()); await soService.start({}); - expect(migratorInstanceMock.runMigrations).toHaveBeenCalledWith(true); + expect(migratorInstanceMock.runMigrations).not.toHaveBeenCalled(); + }); + + it('waits for all es nodes to be compatible before running migrations', async done => { + expect.assertions(2); + const configService = configServiceMock.create({ atPath: { skip: false } }); + const coreContext = mockCoreContext.create({ configService }); + const soService = new SavedObjectsService(coreContext); + const setupDeps = createSetupDeps(); + // Create an new subject so that we can control when isCompatible=true + // is emitted. + setupDeps.elasticsearch.esNodesCompatibility$ = new BehaviorSubject({ + isCompatible: false, + incompatibleNodes: [], + warningNodes: [], + kibanaVersion: '8.0.0', + }); + await soService.setup(setupDeps); + soService.start({}); + expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(0); + ((setupDeps.elasticsearch.esNodesCompatibility$ as any) as BehaviorSubject< + NodesVersionCompatibility + >).next({ + isCompatible: true, + incompatibleNodes: [], + warningNodes: [], + kibanaVersion: '8.0.0', + }); + setImmediate(() => { + expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(1); + done(); + }); }); it('resolves with KibanaMigrator after waiting for migrations to complete', async () => { @@ -158,7 +192,6 @@ describe('SavedObjectsService', () => { const startContract = await soService.start({}); expect(startContract.migrator).toBe(migratorInstanceMock); - expect(migratorInstanceMock.runMigrations).toHaveBeenCalledWith(false); expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(1); }); }); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index b08033a19242b0..0c985c71c7e2f3 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -18,7 +18,7 @@ */ import { CoreService } from 'src/core/types'; -import { first } from 'rxjs/operators'; +import { first, filter, take } from 'rxjs/operators'; import { SavedObjectsClient, SavedObjectsSchema, @@ -283,9 +283,22 @@ export class SavedObjectsService const cliArgs = this.coreContext.env.cliArgs; const skipMigrations = cliArgs.optimize || savedObjectsConfig.skip; - this.logger.debug('Starting saved objects migration'); - await migrator.runMigrations(skipMigrations); - this.logger.debug('Saved objects migration completed'); + if (skipMigrations) { + this.logger.warn( + 'Skipping Saved Object migrations on startup. Note: Individual documents will still be migrated when read or written.' + ); + } else { + this.logger.info( + 'Waiting until all Elasticsearch nodes are compatible with Kibana before starting saved objects migrations...' + ); + await this.setupDeps!.elasticsearch.esNodesCompatibility$.pipe( + filter(nodes => nodes.isCompatible), + take(1) + ).toPromise(); + + this.logger.info('Starting saved objects migrations'); + await migrator.runMigrations(); + } const createRepository = (callCluster: APICaller, extraTypes: string[] = []) => { return SavedObjectsRepository.createRepository( @@ -343,14 +356,14 @@ export class SavedObjectsService savedObjectMappings: this.mappings, savedObjectMigrations: this.migrations, savedObjectValidations: this.validations, - logger: this.coreContext.logger.get('migrations'), + logger: this.logger, kibanaVersion: this.coreContext.env.packageInfo.version, config: this.setupDeps!.legacyPlugins.pluginExtendedConfig, savedObjectsConfig, kibanaConfig, callCluster: migrationsRetryCallCluster( adminClient.callAsInternalUser, - this.coreContext.logger.get('migrations'), + this.logger, migrationsRetryDelay ), }); diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 5217fdf002be9c..823c70e80fe7cd 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -56,23 +56,23 @@ export KIBANA_PKG_BRANCH="$kbnBranch" ### ### download node ### +nodeVersion="$(cat "$dir/.node-version")" +nodeDir="$cacheDir/node/$nodeVersion" +nodeBin="$nodeDir/bin" +classifier="x64.tar.gz" + UNAME=$(uname) OS="linux" if [[ "$UNAME" = *"MINGW64_NT"* ]]; then OS="win" + nodeBin="$HOME/node" + classifier="x64.zip" +elif [[ "$UNAME" == "Darwin" ]]; then + OS="darwin" fi echo " -- Running on OS: $OS" -nodeVersion="$(cat "$dir/.node-version")" -nodeDir="$cacheDir/node/$nodeVersion" - -if [[ "$OS" == "win" ]]; then - nodeBin="$HOME/node" - nodeUrl="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/dist/v$nodeVersion/node-v$nodeVersion-win-x64.zip" -else - nodeBin="$nodeDir/bin" - nodeUrl="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/dist/v$nodeVersion/node-v$nodeVersion-linux-x64.tar.gz" -fi +nodeUrl="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/dist/v$nodeVersion/node-v$nodeVersion-${OS}-${classifier}" if [[ "$installNode" == "true" ]]; then echo " -- node: version=v${nodeVersion} dir=$nodeDir" diff --git a/src/es_archiver/actions/edit.js b/src/es_archiver/actions/edit.ts similarity index 91% rename from src/es_archiver/actions/edit.js rename to src/es_archiver/actions/edit.ts index 5e3a3490133c71..de63081a1ea1b5 100644 --- a/src/es_archiver/actions/edit.js +++ b/src/es_archiver/actions/edit.ts @@ -22,12 +22,23 @@ import Fs from 'fs'; import { createGunzip, createGzip, Z_BEST_COMPRESSION } from 'zlib'; import { promisify } from 'util'; import globby from 'globby'; +import { ToolingLog } from '@kbn/dev-utils'; import { createPromiseFromStreams } from '../../legacy/utils'; const unlinkAsync = promisify(Fs.unlink); -export async function editAction({ prefix, dataDir, log, handler }) { +export async function editAction({ + prefix, + dataDir, + log, + handler, +}: { + prefix: string; + dataDir: string; + log: ToolingLog; + handler: () => Promise; +}) { const archives = ( await globby('**/*.gz', { cwd: prefix ? resolve(dataDir, prefix) : dataDir, diff --git a/src/es_archiver/actions/empty_kibana_index.js b/src/es_archiver/actions/empty_kibana_index.ts similarity index 73% rename from src/es_archiver/actions/empty_kibana_index.js rename to src/es_archiver/actions/empty_kibana_index.ts index 386863ec18a43d..5f96fbc5f996ca 100644 --- a/src/es_archiver/actions/empty_kibana_index.js +++ b/src/es_archiver/actions/empty_kibana_index.ts @@ -16,13 +16,25 @@ * specific language governing permissions and limitations * under the License. */ + +import { Client } from 'elasticsearch'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; + import { migrateKibanaIndex, deleteKibanaIndices, createStats } from '../lib'; -export async function emptyKibanaIndexAction({ client, log, kbnClient }) { +export async function emptyKibanaIndexAction({ + client, + log, + kbnClient, +}: { + client: Client; + log: ToolingLog; + kbnClient: KbnClient; +}) { const stats = createStats('emptyKibanaIndex', log); const kibanaPluginIds = await kbnClient.plugins.getEnabledIds(); - await deleteKibanaIndices({ client, stats }); - await migrateKibanaIndex({ client, log, stats, kibanaPluginIds }); + await deleteKibanaIndices({ client, stats, log }); + await migrateKibanaIndex({ client, log, kibanaPluginIds }); return stats; } diff --git a/src/es_archiver/actions/index.js b/src/es_archiver/actions/index.ts similarity index 100% rename from src/es_archiver/actions/index.js rename to src/es_archiver/actions/index.ts diff --git a/src/es_archiver/actions/load.js b/src/es_archiver/actions/load.ts similarity index 84% rename from src/es_archiver/actions/load.js rename to src/es_archiver/actions/load.ts index ea02ce9dd3ad33..404fd0daea91d4 100644 --- a/src/es_archiver/actions/load.js +++ b/src/es_archiver/actions/load.ts @@ -19,6 +19,9 @@ import { resolve } from 'path'; import { createReadStream } from 'fs'; +import { Readable } from 'stream'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; +import { Client } from 'elasticsearch'; import { createPromiseFromStreams, concatStreamProviders } from '../../legacy/utils'; @@ -38,12 +41,26 @@ import { // pipe a series of streams into each other so that data and errors // flow from the first stream to the last. Errors from the last stream // are not listened for -const pipeline = (...streams) => +const pipeline = (...streams: Readable[]) => streams.reduce((source, dest) => - source.once('error', error => dest.emit('error', error)).pipe(dest) + source.once('error', error => dest.emit('error', error)).pipe(dest as any) ); -export async function loadAction({ name, skipExisting, client, dataDir, log, kbnClient }) { +export async function loadAction({ + name, + skipExisting, + client, + dataDir, + log, + kbnClient, +}: { + name: string; + skipExisting: boolean; + client: Client; + dataDir: string; + log: ToolingLog; + kbnClient: KbnClient; +}) { const inputDir = resolve(dataDir, name); const stats = createStats(name, log); const files = prioritizeMappings(await readDirectory(inputDir)); @@ -64,12 +81,12 @@ export async function loadAction({ name, skipExisting, client, dataDir, log, kbn { objectMode: true } ); - const progress = new Progress('load progress'); + const progress = new Progress(); progress.activate(log); await createPromiseFromStreams([ recordStream, - createCreateIndexStream({ client, stats, skipExisting, log, kibanaPluginIds }), + createCreateIndexStream({ client, stats, skipExisting, log }), createIndexDocRecordsStream(client, stats, progress), ]); @@ -77,7 +94,7 @@ export async function loadAction({ name, skipExisting, client, dataDir, log, kbn const result = stats.toJSON(); for (const [index, { docs }] of Object.entries(result)) { - if (!docs && docs.indexed > 0) { + if (docs && docs.indexed > 0) { log.info('[%s] Indexed %d docs into %j', name, docs.indexed, index); } } diff --git a/src/es_archiver/actions/rebuild_all.js b/src/es_archiver/actions/rebuild_all.ts similarity index 84% rename from src/es_archiver/actions/rebuild_all.js rename to src/es_archiver/actions/rebuild_all.ts index 9379a29c38130c..1467a1d0430b76 100644 --- a/src/es_archiver/actions/rebuild_all.js +++ b/src/es_archiver/actions/rebuild_all.ts @@ -18,13 +18,12 @@ */ import { resolve, dirname, relative } from 'path'; - import { stat, rename, createReadStream, createWriteStream } from 'fs'; - +import { Readable, Writable } from 'stream'; import { fromNode } from 'bluebird'; +import { ToolingLog } from '@kbn/dev-utils'; import { createPromiseFromStreams } from '../../legacy/utils'; - import { prioritizeMappings, readDirectory, @@ -33,12 +32,20 @@ import { createFormatArchiveStreams, } from '../lib'; -async function isDirectory(path) { +async function isDirectory(path: string): Promise { const stats = await fromNode(cb => stat(path, cb)); return stats.isDirectory(); } -export async function rebuildAllAction({ dataDir, log, rootDir = dataDir }) { +export async function rebuildAllAction({ + dataDir, + log, + rootDir = dataDir, +}: { + dataDir: string; + log: ToolingLog; + rootDir?: string; +}) { const childNames = prioritizeMappings(await readDirectory(dataDir)); for (const childName of childNames) { const childPath = resolve(dataDir, childName); @@ -58,11 +65,11 @@ export async function rebuildAllAction({ dataDir, log, rootDir = dataDir }) { const tempFile = childPath + (gzip ? '.rebuilding.gz' : '.rebuilding'); await createPromiseFromStreams([ - createReadStream(childPath), + createReadStream(childPath) as Readable, ...createParseArchiveStreams({ gzip }), ...createFormatArchiveStreams({ gzip }), createWriteStream(tempFile), - ]); + ] as [Readable, ...Writable[]]); await fromNode(cb => rename(tempFile, childPath, cb)); log.info(`${archiveName} Rebuilt ${childName}`); diff --git a/src/es_archiver/actions/save.js b/src/es_archiver/actions/save.ts similarity index 83% rename from src/es_archiver/actions/save.js rename to src/es_archiver/actions/save.ts index 2c264ed2ee3a9e..7a3a9dd97c0ab1 100644 --- a/src/es_archiver/actions/save.js +++ b/src/es_archiver/actions/save.ts @@ -19,9 +19,11 @@ import { resolve } from 'path'; import { createWriteStream, mkdirSync } from 'fs'; +import { Readable, Writable } from 'stream'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; import { createListStream, createPromiseFromStreams } from '../../legacy/utils'; - import { createStats, createGenerateIndexRecordsStream, @@ -30,7 +32,21 @@ import { Progress, } from '../lib'; -export async function saveAction({ name, indices, client, dataDir, log, raw }) { +export async function saveAction({ + name, + indices, + client, + dataDir, + log, + raw, +}: { + name: string; + indices: string | string[]; + client: Client; + dataDir: string; + log: ToolingLog; + raw: boolean; +}) { const outputDir = resolve(dataDir, name); const stats = createStats(name, log); @@ -48,7 +64,7 @@ export async function saveAction({ name, indices, client, dataDir, log, raw }) { createGenerateIndexRecordsStream(client, stats), ...createFormatArchiveStreams(), createWriteStream(resolve(outputDir, 'mappings.json')), - ]), + ] as [Readable, ...Writable[]]), // export all documents from matching indexes into data.json.gz createPromiseFromStreams([ @@ -56,7 +72,7 @@ export async function saveAction({ name, indices, client, dataDir, log, raw }) { createGenerateDocRecordsStream(client, stats, progress), ...createFormatArchiveStreams({ gzip: !raw }), createWriteStream(resolve(outputDir, `data.json${raw ? '' : '.gz'}`)), - ]), + ] as [Readable, ...Writable[]]), ]); progress.deactivate(); diff --git a/src/es_archiver/actions/unload.js b/src/es_archiver/actions/unload.ts similarity index 79% rename from src/es_archiver/actions/unload.js rename to src/es_archiver/actions/unload.ts index 2acf8d2d719865..130a6b542b218a 100644 --- a/src/es_archiver/actions/unload.js +++ b/src/es_archiver/actions/unload.ts @@ -19,9 +19,11 @@ import { resolve } from 'path'; import { createReadStream } from 'fs'; +import { Readable, Writable } from 'stream'; +import { Client } from 'elasticsearch'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; import { createPromiseFromStreams } from '../../legacy/utils'; - import { isGzip, createStats, @@ -32,7 +34,19 @@ import { createDeleteIndexStream, } from '../lib'; -export async function unloadAction({ name, client, dataDir, log, kbnClient }) { +export async function unloadAction({ + name, + client, + dataDir, + log, + kbnClient, +}: { + name: string; + client: Client; + dataDir: string; + log: ToolingLog; + kbnClient: KbnClient; +}) { const inputDir = resolve(dataDir, name); const stats = createStats(name, log); const kibanaPluginIds = await kbnClient.plugins.getEnabledIds(); @@ -42,11 +56,11 @@ export async function unloadAction({ name, client, dataDir, log, kbnClient }) { log.info('[%s] Unloading indices from %j', name, filename); await createPromiseFromStreams([ - createReadStream(resolve(inputDir, filename)), + createReadStream(resolve(inputDir, filename)) as Readable, ...createParseArchiveStreams({ gzip: isGzip(filename) }), createFilterRecordsStream('index'), createDeleteIndexStream(client, stats, log, kibanaPluginIds), - ]); + ] as [Readable, ...Writable[]]); } return stats.toJSON(); diff --git a/src/es_archiver/cli.js b/src/es_archiver/cli.ts similarity index 90% rename from src/es_archiver/cli.js rename to src/es_archiver/cli.ts index 56d1fdca89780c..252f99f8f47afb 100644 --- a/src/es_archiver/cli.js +++ b/src/es_archiver/cli.ts @@ -17,7 +17,7 @@ * under the License. */ -/************************************************************* +/** *********************************************************** * * Run `node scripts/es_archiver --help` for usage information * @@ -27,17 +27,17 @@ import { resolve } from 'path'; import { readFileSync } from 'fs'; import { format as formatUrl } from 'url'; import readline from 'readline'; - import { Command } from 'commander'; import * as legacyElasticsearch from 'elasticsearch'; -import { EsArchiver } from './es_archiver'; import { ToolingLog } from '@kbn/dev-utils'; import { readConfigFile } from '@kbn/test'; +import { EsArchiver } from './es_archiver'; + const cmd = new Command('node scripts/es_archiver'); -const resolveConfigPath = v => resolve(process.cwd(), v); +const resolveConfigPath = (v: string) => resolve(process.cwd(), v); const defaultConfigPath = resolveConfigPath('test/functional/config.js'); cmd @@ -56,6 +56,7 @@ cmd defaultConfigPath ) .on('--help', () => { + // eslint-disable-next-line no-console console.log(readFileSync(resolve(__dirname, './cli_help.txt'), 'utf8')); }); @@ -95,10 +96,10 @@ cmd output: process.stdout, }); - await new Promise(resolve => { + await new Promise(resolveInput => { rl.question(`Press enter when you're done`, () => { rl.close(); - resolve(); + resolveInput(); }); }); }) @@ -112,12 +113,12 @@ cmd cmd.parse(process.argv); -const missingCommand = cmd.args.every(a => !(a instanceof Command)); +const missingCommand = cmd.args.every(a => !((a as any) instanceof Command)); if (missingCommand) { execute(); } -async function execute(fn) { +async function execute(fn?: (esArchiver: EsArchiver, command: Command) => void): Promise { try { const log = new ToolingLog({ level: cmd.verbose ? 'debug' : 'info', @@ -134,7 +135,7 @@ async function execute(fn) { // log and count all validation errors let errorCount = 0; - const error = msg => { + const error = (msg: string) => { errorCount++; log.error(msg); }; @@ -170,11 +171,12 @@ async function execute(fn) { dataDir: resolve(cmd.dir), kibanaUrl: cmd.kibanaUrl, }); - await fn(esArchiver, cmd); + await fn!(esArchiver, cmd); } finally { await client.close(); } } catch (err) { + // eslint-disable-next-line no-console console.log('FATAL ERROR', err.stack); } } diff --git a/src/es_archiver/es_archiver.d.ts b/src/es_archiver/es_archiver.d.ts deleted file mode 100644 index c50ae19d99cbf3..00000000000000 --- a/src/es_archiver/es_archiver.d.ts +++ /dev/null @@ -1,39 +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 { ToolingLog } from '@kbn/dev-utils'; -import { Client } from 'elasticsearch'; -import { createStats } from './lib/stats'; - -export type JsonStats = ReturnType['toJSON']>; - -export class EsArchiver { - constructor(options: { client: Client; dataDir: string; log: ToolingLog; kibanaUrl: string }); - public save( - name: string, - indices: string | string[], - options?: { raw?: boolean } - ): Promise; - public load(name: string, options?: { skipExisting?: boolean }): Promise; - public unload(name: string): Promise; - public rebuildAll(): Promise; - public edit(prefix: string, handler: () => Promise): Promise; - public loadIfNeeded(name: string): Promise; - public emptyKibanaIndex(): Promise; -} diff --git a/src/es_archiver/es_archiver.js b/src/es_archiver/es_archiver.ts similarity index 83% rename from src/es_archiver/es_archiver.js rename to src/es_archiver/es_archiver.ts index 705706d0e58778..5614dfd842087b 100644 --- a/src/es_archiver/es_archiver.js +++ b/src/es_archiver/es_archiver.ts @@ -17,7 +17,8 @@ * under the License. */ -import { KbnClient } from '@kbn/dev-utils'; +import { Client } from 'elasticsearch'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; import { saveAction, @@ -29,7 +30,22 @@ import { } from './actions'; export class EsArchiver { - constructor({ client, dataDir, log, kibanaUrl }) { + private readonly client: Client; + private readonly dataDir: string; + private readonly log: ToolingLog; + private readonly kbnClient: KbnClient; + + constructor({ + client, + dataDir, + log, + kibanaUrl, + }: { + client: Client; + dataDir: string; + log: ToolingLog; + kibanaUrl: string; + }) { this.client = client; this.dataDir = dataDir; this.log = log; @@ -46,7 +62,7 @@ export class EsArchiver { * @property {Boolean} options.raw - should the archive be raw (unzipped) or not * @return Promise */ - async save(name, indices, { raw = false } = {}) { + async save(name: string, indices: string | string[], { raw = false }: { raw?: boolean } = {}) { return await saveAction({ name, indices, @@ -66,9 +82,7 @@ export class EsArchiver { * be ignored or overwritten * @return Promise */ - async load(name, options = {}) { - const { skipExisting } = options; - + async load(name: string, { skipExisting = false }: { skipExisting?: boolean } = {}) { return await loadAction({ name, skipExisting: !!skipExisting, @@ -85,7 +99,7 @@ export class EsArchiver { * @param {String} name * @return Promise */ - async unload(name) { + async unload(name: string) { return await unloadAction({ name, client: this.client, @@ -103,7 +117,6 @@ export class EsArchiver { */ async rebuildAll() { return await rebuildAllAction({ - client: this.client, dataDir: this.dataDir, log: this.log, }); @@ -117,7 +130,7 @@ export class EsArchiver { * @param {() => Promise} handler * @return Promise */ - async edit(prefix, handler) { + async edit(prefix: string, handler: () => Promise) { return await editAction({ prefix, log: this.log, @@ -132,7 +145,7 @@ export class EsArchiver { * @param {String} name * @return Promise */ - async loadIfNeeded(name) { + async loadIfNeeded(name: string) { return await this.load(name, { skipExisting: true }); } diff --git a/src/es_archiver/index.js b/src/es_archiver/index.js deleted file mode 100644 index f7a579a98a42d9..00000000000000 --- a/src/es_archiver/index.js +++ /dev/null @@ -1,20 +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. - */ - -export { EsArchiver } from './es_archiver'; diff --git a/src/es_archiver/index.d.ts b/src/es_archiver/index.ts similarity index 100% rename from src/es_archiver/index.d.ts rename to src/es_archiver/index.ts diff --git a/src/es_archiver/lib/__tests__/stats.js b/src/es_archiver/lib/__tests__/stats.ts similarity index 95% rename from src/es_archiver/lib/__tests__/stats.js rename to src/es_archiver/lib/__tests__/stats.ts index ccc24c25fb8601..28e337b3da5298 100644 --- a/src/es_archiver/lib/__tests__/stats.js +++ b/src/es_archiver/lib/__tests__/stats.ts @@ -17,26 +17,26 @@ * under the License. */ -import expect from '@kbn/expect'; import { uniq } from 'lodash'; import sinon from 'sinon'; +import expect from '@kbn/expect'; +import { ToolingLog } from '@kbn/dev-utils'; import { createStats } from '../'; -import { ToolingLog } from '@kbn/dev-utils'; -function createBufferedLog() { - const log = new ToolingLog({ +function createBufferedLog(): ToolingLog & { buffer: string } { + const log: ToolingLog = new ToolingLog({ level: 'debug', writeTo: { - write: chunk => (log.buffer += chunk), + write: chunk => ((log as any).buffer += chunk), }, }); - log.buffer = ''; - return log; + (log as any).buffer = ''; + return log as ToolingLog & { buffer: string }; } -function assertDeepClones(a, b) { - const path = []; +function assertDeepClones(a: any, b: any) { + const path: string[] = []; try { (function recurse(one, two) { if (typeof one !== 'object' || typeof two !== 'object') { diff --git a/src/es_archiver/lib/archives/__tests__/format.js b/src/es_archiver/lib/archives/__tests__/format.ts similarity index 89% rename from src/es_archiver/lib/archives/__tests__/format.js rename to src/es_archiver/lib/archives/__tests__/format.ts index 20ead30824d064..f472f094134d7b 100644 --- a/src/es_archiver/lib/archives/__tests__/format.js +++ b/src/es_archiver/lib/archives/__tests__/format.ts @@ -17,7 +17,7 @@ * under the License. */ -import Stream from 'stream'; +import Stream, { Readable, Writable } from 'stream'; import { createGunzip } from 'zlib'; import expect from '@kbn/expect'; @@ -43,11 +43,11 @@ describe('esArchiver createFormatArchiveStreams', () => { }); it('streams consume js values and produces buffers', async () => { - const output = await createPromiseFromStreams([ + const output = await createPromiseFromStreams([ createListStream(INPUTS), ...createFormatArchiveStreams({ gzip: false }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output.length).to.be.greaterThan(0); output.forEach(b => expect(b).to.be.a(Buffer)); @@ -58,7 +58,7 @@ describe('esArchiver createFormatArchiveStreams', () => { createListStream(INPUTS), ...createFormatArchiveStreams({ gzip: false }), createConcatStream(''), - ]); + ] as [Readable, ...Writable[]]); expect(json).to.be(INPUT_JSON); }); @@ -73,11 +73,11 @@ describe('esArchiver createFormatArchiveStreams', () => { }); it('streams consume js values and produces buffers', async () => { - const output = await createPromiseFromStreams([ + const output = await createPromiseFromStreams([ createListStream([1, 2, { foo: 'bar' }, [1, 2]]), ...createFormatArchiveStreams({ gzip: true }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output.length).to.be.greaterThan(0); output.forEach(b => expect(b).to.be.a(Buffer)); @@ -89,7 +89,7 @@ describe('esArchiver createFormatArchiveStreams', () => { ...createFormatArchiveStreams({ gzip: true }), createGunzip(), createConcatStream(''), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.be(INPUT_JSON); }); }); @@ -100,7 +100,7 @@ describe('esArchiver createFormatArchiveStreams', () => { createListStream(INPUTS), ...createFormatArchiveStreams(), createConcatStream(''), - ]); + ] as [Readable, ...Writable[]]); expect(json).to.be(INPUT_JSON); }); diff --git a/src/es_archiver/lib/archives/__tests__/parse.js b/src/es_archiver/lib/archives/__tests__/parse.ts similarity index 93% rename from src/es_archiver/lib/archives/__tests__/parse.js rename to src/es_archiver/lib/archives/__tests__/parse.ts index 2e1506e543a358..ba30156b5af393 100644 --- a/src/es_archiver/lib/archives/__tests__/parse.js +++ b/src/es_archiver/lib/archives/__tests__/parse.ts @@ -17,7 +17,7 @@ * under the License. */ -import Stream, { PassThrough, Transform } from 'stream'; +import Stream, { PassThrough, Readable, Writable, Transform } from 'stream'; import { createGzip } from 'zlib'; import expect from '@kbn/expect'; @@ -66,13 +66,13 @@ describe('esArchiver createParseArchiveStreams', () => { ]), ...createParseArchiveStreams({ gzip: false }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.eql([{ a: 1 }, 1]); }); it('provides each JSON object as soon as it is parsed', async () => { - let onReceived; + let onReceived: (resolved: any) => void; const receivedPromise = new Promise(resolve => (onReceived = resolve)); const input = new PassThrough(); const check = new Transform({ @@ -80,16 +80,16 @@ describe('esArchiver createParseArchiveStreams', () => { readableObjectMode: true, transform(chunk, env, callback) { onReceived(chunk); - callback(null, chunk); + callback(undefined, chunk); }, }); const finalPromise = createPromiseFromStreams([ - input, + input as Readable, ...createParseArchiveStreams(), check, createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); input.write(Buffer.from('{"a": 1}\n\n{"a":')); expect(await receivedPromise).to.eql({ a: 1 }); @@ -110,7 +110,7 @@ describe('esArchiver createParseArchiveStreams', () => { ]), ...createParseArchiveStreams({ gzip: false }), createConcatStream(), - ]); + ] as [Readable, ...Writable[]]); throw new Error('should have failed'); } catch (err) { expect(err.message).to.contain('Unexpected number'); @@ -149,7 +149,7 @@ describe('esArchiver createParseArchiveStreams', () => { createGzip(), ...createParseArchiveStreams({ gzip: true }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.eql([{ a: 1 }, { a: 2 }]); }); @@ -161,7 +161,7 @@ describe('esArchiver createParseArchiveStreams', () => { createGzip(), ...createParseArchiveStreams({ gzip: true }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.eql([]); }); @@ -173,7 +173,7 @@ describe('esArchiver createParseArchiveStreams', () => { createListStream([Buffer.from('{"a": 1}')]), ...createParseArchiveStreams({ gzip: true }), createConcatStream(), - ]); + ] as [Readable, ...Writable[]]); throw new Error('should have failed'); } catch (err) { expect(err.message).to.contain('incorrect header check'); diff --git a/src/es_archiver/lib/archives/constants.js b/src/es_archiver/lib/archives/constants.ts similarity index 100% rename from src/es_archiver/lib/archives/constants.js rename to src/es_archiver/lib/archives/constants.ts diff --git a/src/es_archiver/lib/archives/filenames.js b/src/es_archiver/lib/archives/filenames.ts similarity index 91% rename from src/es_archiver/lib/archives/filenames.js rename to src/es_archiver/lib/archives/filenames.ts index 4ced04401d28d5..24c355edda278a 100644 --- a/src/es_archiver/lib/archives/filenames.js +++ b/src/es_archiver/lib/archives/filenames.ts @@ -19,7 +19,7 @@ import { basename, extname } from 'path'; -export function isGzip(path) { +export function isGzip(path: string) { return extname(path) === '.gz'; } @@ -28,7 +28,7 @@ export function isGzip(path) { * @param {String} path * @return {Boolean} */ -export function isMappingFile(path) { +export function isMappingFile(path: string) { return basename(path, '.gz') === 'mappings.json'; } @@ -41,7 +41,7 @@ export function isMappingFile(path) { * @param {Array} filenames * @return {Array} */ -export function prioritizeMappings(filenames) { +export function prioritizeMappings(filenames: string[]) { return filenames.slice().sort((fa, fb) => { if (isMappingFile(fa) === isMappingFile(fb)) return 0; return isMappingFile(fb) ? 1 : -1; diff --git a/src/es_archiver/lib/archives/format.js b/src/es_archiver/lib/archives/format.ts similarity index 93% rename from src/es_archiver/lib/archives/format.js rename to src/es_archiver/lib/archives/format.ts index 01fca87e7ba988..9bef4c9adbf059 100644 --- a/src/es_archiver/lib/archives/format.js +++ b/src/es_archiver/lib/archives/format.ts @@ -19,14 +19,12 @@ import { createGzip, Z_BEST_COMPRESSION } from 'zlib'; import { PassThrough } from 'stream'; - import stringify from 'json-stable-stringify'; import { createMapStream, createIntersperseStream } from '../../../legacy/utils'; - import { RECORD_SEPARATOR } from './constants'; -export function createFormatArchiveStreams({ gzip = false } = {}) { +export function createFormatArchiveStreams({ gzip = false }: { gzip?: boolean } = {}) { return [ createMapStream(record => stringify(record, { space: ' ' })), createIntersperseStream(RECORD_SEPARATOR), diff --git a/src/es_archiver/lib/archives/index.js b/src/es_archiver/lib/archives/index.ts similarity index 99% rename from src/es_archiver/lib/archives/index.js rename to src/es_archiver/lib/archives/index.ts index 4020f52e45a35b..6aa489ea5a46de 100644 --- a/src/es_archiver/lib/archives/index.js +++ b/src/es_archiver/lib/archives/index.ts @@ -18,7 +18,5 @@ */ export { isGzip, prioritizeMappings } from './filenames'; - export { createParseArchiveStreams } from './parse'; - export { createFormatArchiveStreams } from './format'; diff --git a/src/es_archiver/lib/archives/parse.js b/src/es_archiver/lib/archives/parse.ts similarity index 91% rename from src/es_archiver/lib/archives/parse.js rename to src/es_archiver/lib/archives/parse.ts index 4fe1df72592292..0f4460c925019d 100644 --- a/src/es_archiver/lib/archives/parse.js +++ b/src/es_archiver/lib/archives/parse.ts @@ -29,7 +29,7 @@ export function createParseArchiveStreams({ gzip = false } = {}) { gzip ? createGunzip() : new PassThrough(), createReplaceStream('\r\n', '\n'), createSplitStream(RECORD_SEPARATOR), - createFilterStream(l => l.match(/[^\s]/)), - createMapStream(json => JSON.parse(json.trim())), + createFilterStream(l => !!l.match(/[^\s]/)), + createMapStream(json => JSON.parse(json.trim())), ]; } diff --git a/src/es_archiver/lib/directory.js b/src/es_archiver/lib/directory.ts similarity index 88% rename from src/es_archiver/lib/directory.js rename to src/es_archiver/lib/directory.ts index 5aee10cfea65d9..8581207fa795d1 100644 --- a/src/es_archiver/lib/directory.js +++ b/src/es_archiver/lib/directory.ts @@ -18,10 +18,9 @@ */ import { readdir } from 'fs'; - import { fromNode } from 'bluebird'; -export async function readDirectory(path) { - const allNames = await fromNode(cb => readdir(path, cb)); +export async function readDirectory(path: string) { + const allNames = await fromNode(cb => readdir(path, cb)); return allNames.filter(name => !name.startsWith('.')); } diff --git a/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js b/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.ts similarity index 98% rename from src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js rename to src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.ts index bf4aab208127f4..03599cdc9fbcfe 100644 --- a/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js +++ b/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.ts @@ -143,7 +143,7 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { }, }, ]); - sinon.assert.calledTwice(stats.archivedDoc); + sinon.assert.calledTwice(stats.archivedDoc as any); expect(progress.getTotal()).to.be(2); expect(progress.getComplete()).to.be(2); }); diff --git a/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js b/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.ts similarity index 98% rename from src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js rename to src/es_archiver/lib/docs/__tests__/index_doc_records_stream.ts index 2535642c27cc95..35b068a6910907 100644 --- a/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js +++ b/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.ts @@ -26,12 +26,12 @@ import { Progress } from '../../progress'; import { createIndexDocRecordsStream } from '../index_doc_records_stream'; import { createStubStats, createStubClient, createPersonDocRecords } from './stubs'; -const recordsToBulkBody = records => { +const recordsToBulkBody = (records: any[]) => { return records.reduce((acc, record) => { const { index, id, source } = record.value; return [...acc, { index: { _index: index, _id: id } }, source]; - }, []); + }, [] as any[]); }; describe('esArchiver: createIndexDocRecordsStream()', () => { diff --git a/src/es_archiver/lib/docs/__tests__/stubs.js b/src/es_archiver/lib/docs/__tests__/stubs.ts similarity index 74% rename from src/es_archiver/lib/docs/__tests__/stubs.js rename to src/es_archiver/lib/docs/__tests__/stubs.ts index 9ed48efa7d03ac..698d62e450cb46 100644 --- a/src/es_archiver/lib/docs/__tests__/stubs.js +++ b/src/es_archiver/lib/docs/__tests__/stubs.ts @@ -17,17 +17,22 @@ * under the License. */ +import { Client } from 'elasticsearch'; import sinon from 'sinon'; import Chance from 'chance'; import { times } from 'lodash'; + +import { Stats } from '../../stats'; + const chance = new Chance(); -export const createStubStats = () => ({ - indexedDoc: sinon.stub(), - archivedDoc: sinon.stub(), -}); +export const createStubStats = (): Stats => + ({ + indexedDoc: sinon.stub(), + archivedDoc: sinon.stub(), + } as any); -export const createPersonDocRecords = n => +export const createPersonDocRecords = (n: number) => times(n, () => ({ type: 'doc', value: { @@ -42,15 +47,21 @@ export const createPersonDocRecords = n => }, })); -export const createStubClient = (responses = []) => { - const createStubClientMethod = name => +type MockClient = Client & { + assertNoPendingResponses: () => void; +}; + +export const createStubClient = ( + responses: Array<(name: string, params: any) => any | Promise> = [] +): MockClient => { + const createStubClientMethod = (name: string) => sinon.spy(async params => { if (responses.length === 0) { throw new Error(`unexpected client.${name} call`); } const response = responses.shift(); - return await response(name, params); + return await response!(name, params); }); return { @@ -63,5 +74,5 @@ export const createStubClient = (responses = []) => { throw new Error(`There are ${responses.length} unsent responses.`); } }, - }; + } as any; }; diff --git a/src/es_archiver/lib/docs/generate_doc_records_stream.js b/src/es_archiver/lib/docs/generate_doc_records_stream.ts similarity index 80% rename from src/es_archiver/lib/docs/generate_doc_records_stream.js rename to src/es_archiver/lib/docs/generate_doc_records_stream.ts index be8b0351d95c8e..e255a0abc36c5f 100644 --- a/src/es_archiver/lib/docs/generate_doc_records_stream.js +++ b/src/es_archiver/lib/docs/generate_doc_records_stream.ts @@ -18,33 +18,36 @@ */ import { Transform } from 'stream'; +import { Client, SearchParams, SearchResponse } from 'elasticsearch'; +import { Stats } from '../stats'; +import { Progress } from '../progress'; const SCROLL_SIZE = 1000; const SCROLL_TIMEOUT = '1m'; -export function createGenerateDocRecordsStream(client, stats, progress) { +export function createGenerateDocRecordsStream(client: Client, stats: Stats, progress: Progress) { return new Transform({ writableObjectMode: true, readableObjectMode: true, async transform(index, enc, callback) { try { - let remainingHits = null; - let resp = null; + let remainingHits = 0; + let resp: SearchResponse | null = null; while (!resp || remainingHits > 0) { if (!resp) { resp = await client.search({ - index: index, + index, scroll: SCROLL_TIMEOUT, size: SCROLL_SIZE, _source: true, - rest_total_hits_as_int: true, - }); + rest_total_hits_as_int: true, // not declared on SearchParams type + } as SearchParams); remainingHits = resp.hits.total; progress.addToTotal(remainingHits); } else { resp = await client.scroll({ - scrollId: resp._scroll_id, + scrollId: resp._scroll_id!, scroll: SCROLL_TIMEOUT, }); } @@ -68,7 +71,7 @@ export function createGenerateDocRecordsStream(client, stats, progress) { progress.addToComplete(resp.hits.hits.length); } - callback(null); + callback(undefined); } catch (err) { callback(err); } diff --git a/src/es_archiver/lib/docs/index.js b/src/es_archiver/lib/docs/index.ts similarity index 100% rename from src/es_archiver/lib/docs/index.js rename to src/es_archiver/lib/docs/index.ts diff --git a/src/es_archiver/lib/docs/index_doc_records_stream.js b/src/es_archiver/lib/docs/index_doc_records_stream.ts similarity index 86% rename from src/es_archiver/lib/docs/index_doc_records_stream.js rename to src/es_archiver/lib/docs/index_doc_records_stream.ts index 73fb75c52ff0a6..8236ae8adb6db7 100644 --- a/src/es_archiver/lib/docs/index_doc_records_stream.js +++ b/src/es_archiver/lib/docs/index_doc_records_stream.ts @@ -17,11 +17,14 @@ * under the License. */ +import { Client } from 'elasticsearch'; import { Writable } from 'stream'; +import { Stats } from '../stats'; +import { Progress } from '../progress'; -export function createIndexDocRecordsStream(client, stats, progress) { - async function indexDocs(docs) { - const body = []; +export function createIndexDocRecordsStream(client: Client, stats: Stats, progress: Progress) { + async function indexDocs(docs: any[]) { + const body: any[] = []; docs.forEach(doc => { stats.indexedDoc(doc.index); diff --git a/src/es_archiver/lib/index.js b/src/es_archiver/lib/index.ts similarity index 96% rename from src/es_archiver/lib/index.js rename to src/es_archiver/lib/index.ts index 246dd8169cd6b0..960d51e411859e 100644 --- a/src/es_archiver/lib/index.js +++ b/src/es_archiver/lib/index.ts @@ -30,7 +30,7 @@ export { export { createFilterRecordsStream } from './records'; -export { createStats } from './stats'; +export { createStats, Stats } from './stats'; export { isGzip, diff --git a/src/es_archiver/lib/indices/__tests__/create_index_stream.js b/src/es_archiver/lib/indices/__tests__/create_index_stream.ts similarity index 76% rename from src/es_archiver/lib/indices/__tests__/create_index_stream.js rename to src/es_archiver/lib/indices/__tests__/create_index_stream.ts index 9e0f83c9f7eb96..c90497eded88ce 100644 --- a/src/es_archiver/lib/indices/__tests__/create_index_stream.js +++ b/src/es_archiver/lib/indices/__tests__/create_index_stream.ts @@ -34,10 +34,13 @@ import { createStubIndexRecord, createStubDocRecord, createStubClient, + createStubLogger, } from './stubs'; const chance = new Chance(); +const log = createStubLogger(); + describe('esArchiver: createCreateIndexStream()', () => { describe('defaults', () => { it('deletes existing indices, creates all', async () => { @@ -48,15 +51,15 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubIndexRecord('existing-index'), createStubIndexRecord('new-index'), ]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), ]); expect(stats.getTestSummary()).to.eql({ deletedIndex: 1, createdIndex: 2, }); - sinon.assert.callCount(client.indices.delete, 1); - sinon.assert.callCount(client.indices.create, 3); // one failed create because of existing + sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 1); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 3); // one failed create because of existing }); it('deletes existing aliases, creates all', async () => { @@ -67,14 +70,19 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubIndexRecord('existing-index'), createStubIndexRecord('new-index'), ]), - createCreateIndexStream({ client, stats, log: { debug: () => {} } }), + createCreateIndexStream({ client, stats, log }), ]); - expect(client.indices.getAlias.calledOnce).to.be.ok(); - expect(client.indices.getAlias.args[0][0]).to.eql({ name: 'existing-index', ignore: [404] }); - expect(client.indices.delete.calledOnce).to.be.ok(); - expect(client.indices.delete.args[0][0]).to.eql({ index: ['actual-index'] }); - sinon.assert.callCount(client.indices.create, 3); // one failed create because of existing + expect((client.indices.getAlias as sinon.SinonSpy).calledOnce).to.be.ok(); + expect((client.indices.getAlias as sinon.SinonSpy).args[0][0]).to.eql({ + name: 'existing-index', + ignore: [404], + }); + expect((client.indices.delete as sinon.SinonSpy).calledOnce).to.be.ok(); + expect((client.indices.delete as sinon.SinonSpy).args[0][0]).to.eql({ + index: ['actual-index'], + }); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 3); // one failed create because of existing }); it('passes through "hit" records', async () => { @@ -86,7 +94,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubDocRecord('index', 1), createStubDocRecord('index', 2), ]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); @@ -101,11 +109,11 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubIndexRecord('index', { foo: {} }), createStubDocRecord('index', 1), ]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); - sinon.assert.calledWith(client.indices.create, { + sinon.assert.calledWith(client.indices.create as sinon.SinonSpy, { method: 'PUT', index: 'index', body: { @@ -126,7 +134,7 @@ describe('esArchiver: createCreateIndexStream()', () => { const output = await createPromiseFromStreams([ createListStream([createStubIndexRecord('index'), ...randoms]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); @@ -140,7 +148,7 @@ describe('esArchiver: createCreateIndexStream()', () => { const output = await createPromiseFromStreams([ createListStream(nonRecordValues), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); @@ -161,6 +169,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createCreateIndexStream({ client, stats, + log, skipExisting: true, }), ]); @@ -169,9 +178,12 @@ describe('esArchiver: createCreateIndexStream()', () => { skippedIndex: 1, createdIndex: 1, }); - sinon.assert.callCount(client.indices.delete, 0); - sinon.assert.callCount(client.indices.create, 2); // one failed create because of existing - expect(client.indices.create.args[0][0]).to.have.property('index', 'new-index'); + sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 0); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 2); // one failed create because of existing + expect((client.indices.create as sinon.SinonSpy).args[0][0]).to.have.property( + 'index', + 'new-index' + ); }); it('filters documents for skipped indices', async () => { @@ -190,6 +202,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createCreateIndexStream({ client, stats, + log, skipExisting: true, }), createConcatStream([]), @@ -199,8 +212,8 @@ describe('esArchiver: createCreateIndexStream()', () => { skippedIndex: 1, createdIndex: 1, }); - sinon.assert.callCount(client.indices.delete, 0); - sinon.assert.callCount(client.indices.create, 2); // one failed create because of existing + sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 0); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 2); // one failed create because of existing expect(output).to.have.length(2); expect(output).to.eql([ diff --git a/src/es_archiver/lib/indices/__tests__/delete_index_stream.js b/src/es_archiver/lib/indices/__tests__/delete_index_stream.ts similarity index 66% rename from src/es_archiver/lib/indices/__tests__/delete_index_stream.js rename to src/es_archiver/lib/indices/__tests__/delete_index_stream.ts index 955d1fff8779ed..1c989ba158a29d 100644 --- a/src/es_archiver/lib/indices/__tests__/delete_index_stream.js +++ b/src/es_archiver/lib/indices/__tests__/delete_index_stream.ts @@ -23,7 +23,14 @@ import { createListStream, createPromiseFromStreams } from '../../../../legacy/u import { createDeleteIndexStream } from '../delete_index_stream'; -import { createStubStats, createStubClient, createStubIndexRecord } from './stubs'; +import { + createStubStats, + createStubClient, + createStubIndexRecord, + createStubLogger, +} from './stubs'; + +const log = createStubLogger(); describe('esArchiver: createDeleteIndexStream()', () => { it('deletes the index without checking if it exists', async () => { @@ -32,13 +39,13 @@ describe('esArchiver: createDeleteIndexStream()', () => { await createPromiseFromStreams([ createListStream([createStubIndexRecord('index1')]), - createDeleteIndexStream(client, stats), + createDeleteIndexStream(client, stats, log, []), ]); - sinon.assert.notCalled(stats.deletedIndex); - sinon.assert.notCalled(client.indices.create); - sinon.assert.calledOnce(client.indices.delete); - sinon.assert.notCalled(client.indices.exists); + sinon.assert.notCalled(stats.deletedIndex as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.create as sinon.SinonSpy); + sinon.assert.calledOnce(client.indices.delete as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.exists as sinon.SinonSpy); }); it('reports the delete when the index existed', async () => { @@ -47,12 +54,12 @@ describe('esArchiver: createDeleteIndexStream()', () => { await createPromiseFromStreams([ createListStream([createStubIndexRecord('index1')]), - createDeleteIndexStream(client, stats), + createDeleteIndexStream(client, stats, log, []), ]); - sinon.assert.calledOnce(stats.deletedIndex); - sinon.assert.notCalled(client.indices.create); - sinon.assert.calledOnce(client.indices.delete); - sinon.assert.notCalled(client.indices.exists); + sinon.assert.calledOnce(stats.deletedIndex as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.create as sinon.SinonSpy); + sinon.assert.calledOnce(client.indices.delete as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.exists as sinon.SinonSpy); }); }); diff --git a/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.js b/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.ts similarity index 89% rename from src/es_archiver/lib/indices/__tests__/generate_index_records_stream.js rename to src/es_archiver/lib/indices/__tests__/generate_index_records_stream.ts index 3523e9e82b153f..7a3712ca1a336f 100644 --- a/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.js +++ b/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.ts @@ -45,10 +45,10 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { archivedIndex: 4, }); - sinon.assert.callCount(client.indices.get, 4); - sinon.assert.notCalled(client.indices.create); - sinon.assert.notCalled(client.indices.delete); - sinon.assert.notCalled(client.indices.exists); + sinon.assert.callCount(client.indices.get as sinon.SinonSpy, 4); + sinon.assert.notCalled(client.indices.create as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.delete as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.exists as sinon.SinonSpy); }); it('filters index metadata from settings', async () => { @@ -60,9 +60,9 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { createGenerateIndexRecordsStream(client, stats), ]); - const params = client.indices.get.args[0][0]; + const params = (client.indices.get as sinon.SinonSpy).args[0][0]; expect(params).to.have.property('filterPath'); - const filters = params.filterPath; + const filters: string[] = params.filterPath; expect(filters.some(path => path.includes('index.creation_date'))).to.be(true); expect(filters.some(path => path.includes('index.uuid'))).to.be(true); expect(filters.some(path => path.includes('index.version'))).to.be(true); @@ -73,7 +73,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { const stats = createStubStats(); const client = createStubClient(['index1', 'index2', 'index3']); - const indexRecords = await createPromiseFromStreams([ + const indexRecords = await createPromiseFromStreams([ createListStream(['index1', 'index2', 'index3']), createGenerateIndexRecordsStream(client, stats), createConcatStream([]), diff --git a/src/es_archiver/lib/indices/__tests__/stubs.js b/src/es_archiver/lib/indices/__tests__/stubs.js deleted file mode 100644 index 00649a06f9efef..00000000000000 --- a/src/es_archiver/lib/indices/__tests__/stubs.js +++ /dev/null @@ -1,128 +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 sinon from 'sinon'; - -export const createStubStats = () => ({ - createdIndex: sinon.stub(), - createdAliases: sinon.stub(), - deletedIndex: sinon.stub(), - skippedIndex: sinon.stub(), - archivedIndex: sinon.stub(), - getTestSummary() { - const summary = {}; - Object.keys(this).forEach(key => { - if (this[key].callCount) { - summary[key] = this[key].callCount; - } - }); - return summary; - }, -}); - -export const createStubIndexRecord = (index, aliases = {}) => ({ - type: 'index', - value: { index, aliases }, -}); - -export const createStubDocRecord = (index, id) => ({ - type: 'doc', - value: { index, id }, -}); - -const createEsClientError = errorType => { - const err = new Error(`ES Client Error Stub "${errorType}"`); - err.body = { - error: { - type: errorType, - }, - }; - return err; -}; - -const indexAlias = (aliases, index) => Object.keys(aliases).find(k => aliases[k] === index); - -export const createStubClient = (existingIndices = [], aliases = {}) => ({ - indices: { - get: sinon.spy(async ({ index }) => { - if (!existingIndices.includes(index)) { - throw createEsClientError('index_not_found_exception'); - } - - return { - [index]: { - mappings: {}, - settings: {}, - }, - }; - }), - existsAlias: sinon.spy(({ name }) => { - return Promise.resolve(aliases.hasOwnProperty(name)); - }), - getAlias: sinon.spy(async ({ index, name }) => { - if (index && existingIndices.indexOf(index) >= 0) { - const result = indexAlias(aliases, index); - return { [index]: { aliases: result ? { [result]: {} } : {} } }; - } - - if (name && aliases[name]) { - return { [aliases[name]]: { aliases: { [name]: {} } } }; - } - - return { status: 404 }; - }), - updateAliases: sinon.spy(async ({ body }) => { - body.actions.forEach(({ add: { index, alias } }) => { - if (!existingIndices.includes(index)) { - throw createEsClientError('index_not_found_exception'); - } - existingIndices.push({ index, alias }); - }); - - return { ok: true }; - }), - create: sinon.spy(async ({ index }) => { - if (existingIndices.includes(index) || aliases.hasOwnProperty(index)) { - throw createEsClientError('resource_already_exists_exception'); - } else { - existingIndices.push(index); - return { ok: true }; - } - }), - delete: sinon.spy(async ({ index }) => { - const indices = Array.isArray(index) ? index : [index]; - if (indices.every(ix => existingIndices.includes(ix))) { - // Delete aliases associated with our indices - indices.forEach(ix => { - const alias = Object.keys(aliases).find(k => aliases[k] === ix); - if (alias) { - delete aliases[alias]; - } - }); - indices.forEach(ix => existingIndices.splice(existingIndices.indexOf(ix), 1)); - return { ok: true }; - } else { - throw createEsClientError('index_not_found_exception'); - } - }), - exists: sinon.spy(async () => { - throw new Error('Do not use indices.exists(). React to errors instead.'); - }), - }, -}); diff --git a/src/es_archiver/lib/indices/__tests__/stubs.ts b/src/es_archiver/lib/indices/__tests__/stubs.ts new file mode 100644 index 00000000000000..3f4682299c38d8 --- /dev/null +++ b/src/es_archiver/lib/indices/__tests__/stubs.ts @@ -0,0 +1,154 @@ +/* + * 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 { Client } from 'elasticsearch'; +import sinon from 'sinon'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../../stats'; + +type StubStats = Stats & { + getTestSummary: () => Record; +}; + +export const createStubStats = (): StubStats => + ({ + createdIndex: sinon.stub(), + createdAliases: sinon.stub(), + deletedIndex: sinon.stub(), + skippedIndex: sinon.stub(), + archivedIndex: sinon.stub(), + getTestSummary() { + const summary: Record = {}; + Object.keys(this).forEach(key => { + if (this[key].callCount) { + summary[key] = this[key].callCount; + } + }); + return summary; + }, + } as any); + +export const createStubLogger = (): ToolingLog => + ({ + debug: sinon.stub(), + info: sinon.stub(), + success: sinon.stub(), + warning: sinon.stub(), + error: sinon.stub(), + } as any); + +export const createStubIndexRecord = (index: string, aliases = {}) => ({ + type: 'index', + value: { index, aliases }, +}); + +export const createStubDocRecord = (index: string, id: number) => ({ + type: 'doc', + value: { index, id }, +}); + +const createEsClientError = (errorType: string) => { + const err = new Error(`ES Client Error Stub "${errorType}"`); + (err as any).body = { + error: { + type: errorType, + }, + }; + return err; +}; + +const indexAlias = (aliases: Record, index: string) => + Object.keys(aliases).find(k => aliases[k] === index); + +type StubClient = Client; + +export const createStubClient = ( + existingIndices: string[] = [], + aliases: Record = {} +): StubClient => + ({ + indices: { + get: sinon.spy(async ({ index }) => { + if (!existingIndices.includes(index)) { + throw createEsClientError('index_not_found_exception'); + } + + return { + [index]: { + mappings: {}, + settings: {}, + }, + }; + }), + existsAlias: sinon.spy(({ name }) => { + return Promise.resolve(aliases.hasOwnProperty(name)); + }), + getAlias: sinon.spy(async ({ index, name }) => { + if (index && existingIndices.indexOf(index) >= 0) { + const result = indexAlias(aliases, index); + return { [index]: { aliases: result ? { [result]: {} } : {} } }; + } + + if (name && aliases[name]) { + return { [aliases[name]]: { aliases: { [name]: {} } } }; + } + + return { status: 404 }; + }), + updateAliases: sinon.spy(async ({ body }) => { + body.actions.forEach( + ({ add: { index, alias } }: { add: { index: string; alias: string } }) => { + if (!existingIndices.includes(index)) { + throw createEsClientError('index_not_found_exception'); + } + existingIndices.push({ index, alias } as any); + } + ); + + return { ok: true }; + }), + create: sinon.spy(async ({ index }) => { + if (existingIndices.includes(index) || aliases.hasOwnProperty(index)) { + throw createEsClientError('resource_already_exists_exception'); + } else { + existingIndices.push(index); + return { ok: true }; + } + }), + delete: sinon.spy(async ({ index }) => { + const indices = Array.isArray(index) ? index : [index]; + if (indices.every(ix => existingIndices.includes(ix))) { + // Delete aliases associated with our indices + indices.forEach(ix => { + const alias = Object.keys(aliases).find(k => aliases[k] === ix); + if (alias) { + delete aliases[alias]; + } + }); + indices.forEach(ix => existingIndices.splice(existingIndices.indexOf(ix), 1)); + return { ok: true }; + } else { + throw createEsClientError('index_not_found_exception'); + } + }), + exists: sinon.spy(async () => { + throw new Error('Do not use indices.exists(). React to errors instead.'); + }), + }, + } as any); diff --git a/src/es_archiver/lib/indices/create_index_stream.js b/src/es_archiver/lib/indices/create_index_stream.ts similarity index 81% rename from src/es_archiver/lib/indices/create_index_stream.js rename to src/es_archiver/lib/indices/create_index_stream.ts index 8fe4bc568cd231..df9d3bb623ad65 100644 --- a/src/es_archiver/lib/indices/create_index_stream.js +++ b/src/es_archiver/lib/indices/create_index_stream.ts @@ -17,13 +17,36 @@ * under the License. */ -import { Transform } from 'stream'; - +import { Transform, Readable } from 'stream'; import { get, once } from 'lodash'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; + +import { Stats } from '../stats'; import { deleteKibanaIndices } from './kibana_index'; import { deleteIndex } from './delete_index'; -export function createCreateIndexStream({ client, stats, skipExisting, log }) { +interface DocRecord { + value: { + index: string; + type: string; + settings: Record; + mappings: Record; + aliases: Record; + }; +} + +export function createCreateIndexStream({ + client, + stats, + skipExisting = false, + log, +}: { + client: Client; + stats: Stats; + skipExisting?: boolean; + log: ToolingLog; +}) { const skipDocsFromIndices = new Set(); // If we're trying to import Kibana index docs, we need to ensure that @@ -31,7 +54,7 @@ export function createCreateIndexStream({ client, stats, skipExisting, log }) { // migrations. This only needs to be done once per archive load operation. const deleteKibanaIndicesOnce = once(deleteKibanaIndices); - async function handleDoc(stream, record) { + async function handleDoc(stream: Readable, record: DocRecord) { if (skipDocsFromIndices.has(record.value.index)) { return; } @@ -39,7 +62,7 @@ export function createCreateIndexStream({ client, stats, skipExisting, log }) { stream.push(record); } - async function handleIndex(record) { + async function handleIndex(record: DocRecord) { const { index, settings, mappings, aliases } = record.value; const isKibana = index.startsWith('.kibana'); @@ -102,7 +125,7 @@ export function createCreateIndexStream({ client, stats, skipExisting, log }) { break; } - callback(null); + callback(); } catch (err) { callback(err); } diff --git a/src/es_archiver/lib/indices/delete_index.js b/src/es_archiver/lib/indices/delete_index.ts similarity index 76% rename from src/es_archiver/lib/indices/delete_index.js rename to src/es_archiver/lib/indices/delete_index.ts index 6f60d9533a36b3..e3fca587fbc3d3 100644 --- a/src/es_archiver/lib/indices/delete_index.js +++ b/src/es_archiver/lib/indices/delete_index.ts @@ -18,22 +18,34 @@ */ import { get } from 'lodash'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../stats'; // see https://github.com/elastic/elasticsearch/blob/99f88f15c5febbca2d13b5b5fda27b844153bf1a/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java#L313-L319 const PENDING_SNAPSHOT_STATUSES = ['INIT', 'STARTED', 'WAITING']; -export async function deleteIndex(options) { +export async function deleteIndex(options: { + client: Client; + stats: Stats; + index: string; + log: ToolingLog; + retryIfSnapshottingCount?: number; +}): Promise { const { client, stats, index, log, retryIfSnapshottingCount = 10 } = options; const getIndicesToDelete = async () => { const aliasInfo = await client.indices.getAlias({ name: index, ignore: [404] }); - return aliasInfo.status === 404 ? index : Object.keys(aliasInfo); + return aliasInfo.status === 404 ? [index] : Object.keys(aliasInfo); }; try { const indicesToDelete = await getIndicesToDelete(); await client.indices.delete({ index: indicesToDelete }); - stats.deletedIndex(indicesToDelete); + for (let i = 0; i < indicesToDelete.length; i++) { + const indexToDelete = indicesToDelete[i]; + stats.deletedIndex(indexToDelete); + } } catch (error) { if (retryIfSnapshottingCount > 0 && isDeleteWhileSnapshotInProgressError(error)) { stats.waitingForInProgressSnapshot(index); @@ -56,7 +68,7 @@ export async function deleteIndex(options) { * @param {Error} error * @return {Boolean} */ -export function isDeleteWhileSnapshotInProgressError(error) { +export function isDeleteWhileSnapshotInProgressError(error: object) { return get(error, 'body.error.reason', '').startsWith( 'Cannot delete indices that are being snapshotted' ); @@ -65,13 +77,9 @@ export function isDeleteWhileSnapshotInProgressError(error) { /** * Wait for the any snapshot in any repository that is * snapshotting this index to complete. - * - * @param {EsClient} client - * @param {string} index the name of the index to look for - * @return {Promise} */ -export async function waitForSnapshotCompletion(client, index, log) { - const isSnapshotPending = async (repository, snapshot) => { +export async function waitForSnapshotCompletion(client: Client, index: string, log: ToolingLog) { + const isSnapshotPending = async (repository: string, snapshot: string) => { const { snapshots: [status], } = await client.snapshot.status({ @@ -83,7 +91,7 @@ export async function waitForSnapshotCompletion(client, index, log) { return PENDING_SNAPSHOT_STATUSES.includes(status.state); }; - const getInProgressSnapshots = async repository => { + const getInProgressSnapshots = async (repository: string) => { const { snapshots: inProgressSnapshots } = await client.snapshot.get({ repository, snapshot: '_current', @@ -91,9 +99,9 @@ export async function waitForSnapshotCompletion(client, index, log) { return inProgressSnapshots; }; - for (const repository of Object.keys(await client.snapshot.getRepository())) { + for (const repository of Object.keys(await client.snapshot.getRepository({} as any))) { const allInProgress = await getInProgressSnapshots(repository); - const found = allInProgress.find(s => s.indices.includes(index)); + const found = allInProgress.find((s: any) => s.indices.includes(index)); if (!found) { continue; diff --git a/src/es_archiver/lib/indices/delete_index_stream.js b/src/es_archiver/lib/indices/delete_index_stream.ts similarity index 86% rename from src/es_archiver/lib/indices/delete_index_stream.js rename to src/es_archiver/lib/indices/delete_index_stream.ts index 31a49ed30a124d..b4e1e530e1f84e 100644 --- a/src/es_archiver/lib/indices/delete_index_stream.js +++ b/src/es_archiver/lib/indices/delete_index_stream.ts @@ -18,11 +18,19 @@ */ import { Transform } from 'stream'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../stats'; import { deleteIndex } from './delete_index'; import { cleanKibanaIndices } from './kibana_index'; -export function createDeleteIndexStream(client, stats, log, kibanaPluginIds) { +export function createDeleteIndexStream( + client: Client, + stats: Stats, + log: ToolingLog, + kibanaPluginIds: string[] +) { return new Transform({ readableObjectMode: true, writableObjectMode: true, diff --git a/src/es_archiver/lib/indices/generate_index_records_stream.js b/src/es_archiver/lib/indices/generate_index_records_stream.ts similarity index 89% rename from src/es_archiver/lib/indices/generate_index_records_stream.js rename to src/es_archiver/lib/indices/generate_index_records_stream.ts index 1d1a44aa634c24..b4b98f8ae262c7 100644 --- a/src/es_archiver/lib/indices/generate_index_records_stream.js +++ b/src/es_archiver/lib/indices/generate_index_records_stream.ts @@ -18,14 +18,16 @@ */ import { Transform } from 'stream'; +import { Client } from 'elasticsearch'; +import { Stats } from '../stats'; -export function createGenerateIndexRecordsStream(client, stats) { +export function createGenerateIndexRecordsStream(client: Client, stats: Stats) { return new Transform({ writableObjectMode: true, readableObjectMode: true, async transform(indexOrAlias, enc, callback) { try { - const resp = await client.indices.get({ + const resp = (await client.indices.get({ index: indexOrAlias, filterPath: [ '*.settings', @@ -36,7 +38,7 @@ export function createGenerateIndexRecordsStream(client, stats) { '-*.settings.index.version', '-*.settings.index.provided_name', ], - }); + })) as Record; for (const [index, { settings, mappings }] of Object.entries(resp)) { const { diff --git a/src/es_archiver/lib/indices/index.js b/src/es_archiver/lib/indices/index.ts similarity index 100% rename from src/es_archiver/lib/indices/index.js rename to src/es_archiver/lib/indices/index.ts diff --git a/src/es_archiver/lib/indices/kibana_index.js b/src/es_archiver/lib/indices/kibana_index.ts similarity index 70% rename from src/es_archiver/lib/indices/kibana_index.js rename to src/es_archiver/lib/indices/kibana_index.ts index 744132bdcef69e..de67ba7c4e31ec 100644 --- a/src/es_archiver/lib/indices/kibana_index.js +++ b/src/es_archiver/lib/indices/kibana_index.ts @@ -17,29 +17,34 @@ * under the License. */ -import _ from 'lodash'; +import { get } from 'lodash'; import fs from 'fs'; -import path from 'path'; +import Path from 'path'; import { promisify } from 'util'; import { toArray } from 'rxjs/operators'; +import { Client, CreateDocumentParams } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../stats'; import { deleteIndex } from './delete_index'; -import { collectUiExports } from '../../../legacy/ui/ui_exports'; import { KibanaMigrator } from '../../../core/server/saved_objects/migrations'; import { SavedObjectsSchema } from '../../../core/server/saved_objects'; +// @ts-ignore +import { collectUiExports } from '../../../legacy/ui/ui_exports'; +// @ts-ignore import { findPluginSpecs } from '../../../legacy/plugin_discovery'; /** * Load the uiExports for a Kibana instance, only load uiExports from xpack if * it is enabled in the Kibana server. */ -const getUiExports = async kibanaPluginIds => { +const getUiExports = async (kibanaPluginIds: string[]) => { const xpackEnabled = kibanaPluginIds.includes('xpack_main'); const { spec$ } = await findPluginSpecs({ plugins: { - scanDirs: [path.resolve(__dirname, '../../../legacy/core_plugins')], - paths: xpackEnabled ? [path.resolve(__dirname, '../../../../x-pack')] : [], + scanDirs: [Path.resolve(__dirname, '../../../legacy/core_plugins')], + paths: xpackEnabled ? [Path.resolve(__dirname, '../../../../x-pack')] : [], }, }); @@ -50,7 +55,15 @@ const getUiExports = async kibanaPluginIds => { /** * Deletes all indices that start with `.kibana` */ -export async function deleteKibanaIndices({ client, stats, log }) { +export async function deleteKibanaIndices({ + client, + stats, + log, +}: { + client: Client; + stats: Stats; + log: ToolingLog; +}) { const indexNames = await fetchKibanaIndices(client); if (!indexNames.length) { return; @@ -76,37 +89,52 @@ export async function deleteKibanaIndices({ client, stats, log }) { * builds up an object that implements just enough of the kbnMigrations interface * as is required by migrations. */ -export async function migrateKibanaIndex({ client, log, kibanaPluginIds }) { +export async function migrateKibanaIndex({ + client, + log, + kibanaPluginIds, +}: { + client: Client; + log: ToolingLog; + kibanaPluginIds: string[]; +}) { const uiExports = await getUiExports(kibanaPluginIds); const kibanaVersion = await loadKibanaVersion(); - const config = { + const config: Record = { 'xpack.task_manager.index': '.kibana_task_manager', }; + const logger = { + trace: log.verbose.bind(log), + debug: log.debug.bind(log), + info: log.info.bind(log), + warn: log.warning.bind(log), + error: log.error.bind(log), + fatal: log.error.bind(log), + log: (entry: any) => log.info(entry.message), + get: () => logger, + }; + const migratorOptions = { - config: { get: path => config[path] }, + config: { get: (path: string) => config[path] } as any, savedObjectsConfig: { scrollDuration: '5m', batchSize: 100, pollInterval: 100, + skip: false, }, kibanaConfig: { index: '.kibana', - }, - logger: { - trace: log.verbose.bind(log), - debug: log.debug.bind(log), - info: log.info.bind(log), - warn: log.warning.bind(log), - error: log.error.bind(log), - }, - version: kibanaVersion, + } as any, + logger, + kibanaVersion, savedObjectSchemas: new SavedObjectsSchema(uiExports.savedObjectSchemas), savedObjectMappings: uiExports.savedObjectMappings, savedObjectMigrations: uiExports.savedObjectMigrations, savedObjectValidations: uiExports.savedObjectValidations, - callCluster: (path, ...args) => _.get(client, path).call(client, ...args), + callCluster: (path: string, ...args: any[]) => + (get(client, path) as Function).call(client, ...args), }; return await new KibanaMigrator(migratorOptions).runMigrations(); @@ -114,8 +142,8 @@ export async function migrateKibanaIndex({ client, log, kibanaPluginIds }) { async function loadKibanaVersion() { const readFile = promisify(fs.readFile); - const packageJson = await readFile(path.join(__dirname, '../../../../package.json')); - return JSON.parse(packageJson).version; + const packageJson = await readFile(Path.join(__dirname, '../../../../package.json')); + return JSON.parse(packageJson.toString('utf-8')).version; } /** @@ -123,16 +151,24 @@ async function loadKibanaVersion() { * .kibana, .kibana_1, .kibana_323, etc. This finds all indices starting * with .kibana, then filters out any that aren't actually Kibana's core * index (e.g. we don't want to remove .kibana_task_manager or the like). - * - * @param {string} index */ -async function fetchKibanaIndices(client) { +async function fetchKibanaIndices(client: Client) { const kibanaIndices = await client.cat.indices({ index: '.kibana*', format: 'json' }); - const isKibanaIndex = index => /^\.kibana(:?_\d*)?$/.test(index); - return kibanaIndices.map(x => x.index).filter(isKibanaIndex); + const isKibanaIndex = (index: string) => /^\.kibana(:?_\d*)?$/.test(index); + return kibanaIndices.map((x: { index: string }) => x.index).filter(isKibanaIndex); } -export async function cleanKibanaIndices({ client, stats, log, kibanaPluginIds }) { +export async function cleanKibanaIndices({ + client, + stats, + log, + kibanaPluginIds, +}: { + client: Client; + stats: Stats; + log: ToolingLog; + kibanaPluginIds: string[]; +}) { if (!kibanaPluginIds.includes('spaces')) { return await deleteKibanaIndices({ client, @@ -178,7 +214,7 @@ export async function cleanKibanaIndices({ client, stats, log, kibanaPluginIds } stats.deletedIndex('.kibana'); } -export async function createDefaultSpace({ index, client }) { +export async function createDefaultSpace({ index, client }: { index: string; client: Client }) { await client.create({ index, id: 'space:default', @@ -193,5 +229,5 @@ export async function createDefaultSpace({ index, client }) { _reserved: true, }, }, - }); + } as CreateDocumentParams); } diff --git a/src/es_archiver/lib/records/__tests__/filter_records_stream.js b/src/es_archiver/lib/records/__tests__/filter_records_stream.ts similarity index 97% rename from src/es_archiver/lib/records/__tests__/filter_records_stream.js rename to src/es_archiver/lib/records/__tests__/filter_records_stream.ts index fd35575ca59bae..d5830478decbae 100644 --- a/src/es_archiver/lib/records/__tests__/filter_records_stream.js +++ b/src/es_archiver/lib/records/__tests__/filter_records_stream.ts @@ -51,7 +51,7 @@ describe('esArchiver: createFilterRecordsStream()', () => { it('produces record values that have a matching type', async () => { const type1 = chance.word({ length: 5 }); - const output = await createPromiseFromStreams([ + const output = await createPromiseFromStreams([ createListStream([ { type: type1, value: {} }, { type: type1, value: {} }, diff --git a/src/es_archiver/lib/records/filter_records_stream.js b/src/es_archiver/lib/records/filter_records_stream.ts similarity index 91% rename from src/es_archiver/lib/records/filter_records_stream.js rename to src/es_archiver/lib/records/filter_records_stream.ts index 5a835ffe8e84d5..191cbd3b921e3b 100644 --- a/src/es_archiver/lib/records/filter_records_stream.js +++ b/src/es_archiver/lib/records/filter_records_stream.ts @@ -19,14 +19,14 @@ import { Transform } from 'stream'; -export function createFilterRecordsStream(type) { +export function createFilterRecordsStream(type: string) { return new Transform({ writableObjectMode: true, readableObjectMode: true, transform(record, enc, callback) { if (record && record.type === type) { - callback(null, record); + callback(undefined, record); } else { callback(); } diff --git a/src/es_archiver/lib/records/index.js b/src/es_archiver/lib/records/index.ts similarity index 100% rename from src/es_archiver/lib/records/index.js rename to src/es_archiver/lib/records/index.ts diff --git a/src/es_archiver/lib/stats.ts b/src/es_archiver/lib/stats.ts index 5f73304abf9a8b..c69b764fc72908 100644 --- a/src/es_archiver/lib/stats.ts +++ b/src/es_archiver/lib/stats.ts @@ -37,6 +37,8 @@ export interface IndexStats { }; } +export type Stats = ReturnType; + export function createStats(name: string, log: ToolingLog) { const info = (msg: string, ...args: any[]) => log.info(`[${name}] ${msg}`, ...args); const debug = (msg: string, ...args: any[]) => log.debug(`[${name}] ${msg}`, ...args); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.js b/src/legacy/core_plugins/data/public/actions/filters/brush_event.js similarity index 81% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.js rename to src/legacy/core_plugins/data/public/actions/filters/brush_event.js index e0854205b132ea..67711bd4599a2d 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.js +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.js @@ -19,9 +19,10 @@ import _ from 'lodash'; import moment from 'moment'; -import { esFilters } from '../../../../../../../plugins/data/public'; +import { esFilters } from '../../../../../../plugins/data/public'; +import { deserializeAggConfig } from '../../search/expressions/utils'; -export function onBrushEvent(event) { +export async function onBrushEvent(event, getIndexPatterns) { const isNumber = event.data.ordered; const isDate = isNumber && event.data.ordered.date; @@ -29,9 +30,12 @@ export function onBrushEvent(event) { if (!xRaw) return []; const column = xRaw.table.columns[xRaw.column]; if (!column) return []; - const aggConfig = event.aggConfigs[xRaw.column]; - if (!aggConfig) return []; - const indexPattern = aggConfig.getIndexPattern(); + if (!column.meta) return []; + const indexPattern = await getIndexPatterns().get(column.meta.indexPatternId); + const aggConfig = deserializeAggConfig({ + ...column.meta, + indexPattern, + }); const field = aggConfig.params.field; if (!field) return []; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.js b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.js similarity index 71% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.js rename to src/legacy/core_plugins/data/public/actions/filters/brush_event.test.js index 215d440edd9d02..a6fe58503cd021 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.js +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.js @@ -20,21 +20,36 @@ import _ from 'lodash'; import moment from 'moment'; import expect from '@kbn/expect'; + +jest.mock('../../../../../ui/public/agg_types/agg_configs', () => ({ + AggConfigs: function AggConfigs() { + return { + createAggConfig: ({ params }) => ({ + params, + getIndexPattern: () => ({ + timeFieldName: 'time', + }), + }), + }; + }, +})); + import { onBrushEvent } from './brush_event'; describe('brushEvent', () => { const DAY_IN_MS = 24 * 60 * 60 * 1000; const JAN_01_2014 = 1388559600000; + const aggConfigs = [ + { + params: {}, + getIndexPattern: () => ({ + timeFieldName: 'time', + }), + }, + ]; + const baseEvent = { - aggConfigs: [ - { - params: {}, - getIndexPattern: () => ({ - timeFieldName: 'time', - }), - }, - ], data: { fieldFormatter: _.constant({}), series: [ @@ -47,6 +62,11 @@ describe('brushEvent', () => { columns: [ { id: '1', + meta: { + type: 'histogram', + indexPatternId: 'indexPatternId', + aggConfigParams: aggConfigs[0].params, + }, }, ], }, @@ -69,9 +89,11 @@ describe('brushEvent', () => { expect(onBrushEvent).to.be.a(Function); }); - test('ignores event when data.xAxisField not provided', () => { + test('ignores event when data.xAxisField not provided', async () => { const event = _.cloneDeep(baseEvent); - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => baseEvent.data.indexPattern, + })); expect(filters.length).to.equal(0); }); @@ -84,22 +106,26 @@ describe('brushEvent', () => { }; beforeEach(() => { + aggConfigs[0].params.field = dateField; dateEvent = _.cloneDeep(baseEvent); - dateEvent.aggConfigs[0].params.field = dateField; dateEvent.data.ordered = { date: true }; }); - test('by ignoring the event when range spans zero time', () => { + test('by ignoring the event when range spans zero time', async () => { const event = _.cloneDeep(dateEvent); event.range = [JAN_01_2014, JAN_01_2014]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => dateEvent.data.indexPattern, + })); expect(filters.length).to.equal(0); }); - test('by updating the timefilter', () => { + test('by updating the timefilter', async () => { const event = _.cloneDeep(dateEvent); event.range = [JAN_01_2014, JAN_01_2014 + DAY_IN_MS]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: async () => dateEvent.data.indexPattern, + })); expect(filters[0].range.time.gte).to.be(new Date(JAN_01_2014).toISOString()); // Set to a baseline timezone for comparison. expect(filters[0].range.time.lt).to.be(new Date(JAN_01_2014 + DAY_IN_MS).toISOString()); @@ -114,17 +140,19 @@ describe('brushEvent', () => { }; beforeEach(() => { + aggConfigs[0].params.field = dateField; dateEvent = _.cloneDeep(baseEvent); - dateEvent.aggConfigs[0].params.field = dateField; dateEvent.data.ordered = { date: true }; }); - test('creates a new range filter', () => { + test('creates a new range filter', async () => { const event = _.cloneDeep(dateEvent); const rangeBegin = JAN_01_2014; const rangeEnd = rangeBegin + DAY_IN_MS; event.range = [rangeBegin, rangeEnd]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => dateEvent.data.indexPattern, + })); expect(filters.length).to.equal(1); expect(filters[0].range.anotherTimeField.gte).to.equal(moment(rangeBegin).toISOString()); expect(filters[0].range.anotherTimeField.lt).to.equal(moment(rangeEnd).toISOString()); @@ -142,22 +170,26 @@ describe('brushEvent', () => { }; beforeEach(() => { + aggConfigs[0].params.field = numberField; numberEvent = _.cloneDeep(baseEvent); - numberEvent.aggConfigs[0].params.field = numberField; numberEvent.data.ordered = { date: false }; }); - test('by ignoring the event when range does not span at least 2 values', () => { + test('by ignoring the event when range does not span at least 2 values', async () => { const event = _.cloneDeep(numberEvent); event.range = [1]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => numberEvent.data.indexPattern, + })); expect(filters.length).to.equal(0); }); - test('by creating a new filter', () => { + test('by creating a new filter', async () => { const event = _.cloneDeep(numberEvent); event.range = [1, 2, 3, 4]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => numberEvent.data.indexPattern, + })); expect(filters.length).to.equal(1); expect(filters[0].range.numberField.gte).to.equal(1); expect(filters[0].range.numberField.lt).to.equal(4); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.mocks.ts b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts similarity index 92% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.mocks.ts rename to src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts index f0de2f88dcb82c..2cecfd0fe8b767 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.mocks.ts +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts @@ -17,7 +17,7 @@ * under the License. */ -import { chromeServiceMock } from '../../../../../../../core/public/mocks'; +import { chromeServiceMock } from '../../../../../../core/public/mocks'; jest.doMock('ui/new_platform', () => ({ npStart: { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/vis_filters.js b/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js similarity index 72% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/vis_filters.js rename to src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js index 303dec690e62bb..1037c718d0003f 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/vis_filters.js +++ b/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js @@ -17,8 +17,10 @@ * under the License. */ -import { onBrushEvent } from './brush_event'; -import { esFilters } from '../../../../../../../plugins/data/public'; +import { esFilters } from '../../../../../../plugins/data/public'; +import { deserializeAggConfig } from '../../search/expressions/utils'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getIndexPatterns } from '../../../../../../plugins/data/public/services'; /** * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter @@ -63,11 +65,16 @@ const getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { * @param {string} cellValue - value of the current cell * @return {array|string} - filter or list of filters to provide to queryFilter.addFilters() */ -const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { +const createFilter = async (table, columnIndex, rowIndex) => { + if (!table || !table.columns || !table.columns[columnIndex]) return; const column = table.columns[columnIndex]; - const aggConfig = aggConfigs[columnIndex]; + const aggConfig = deserializeAggConfig({ + type: column.meta.type, + aggConfigParams: column.meta.aggConfigParams, + indexPattern: await getIndexPatterns().get(column.meta.indexPatternId), + }); let filter = []; - const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : cellValue; + const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : null; if (value === null || value === undefined || !aggConfig.isFilterable()) { return; } @@ -85,26 +92,28 @@ const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { return filter; }; -const createFiltersFromEvent = event => { +const createFiltersFromEvent = async event => { const filters = []; const dataPoints = event.data || [event]; - dataPoints - .filter(point => point) - .forEach(val => { - const { table, column, row, value } = val; - const filter = createFilter(event.aggConfigs, table, column, row, value); - if (filter) { - filter.forEach(f => { - if (event.negate) { - f = esFilters.toggleFilterNegated(f); - } - filters.push(f); - }); - } - }); + await Promise.all( + dataPoints + .filter(point => point) + .map(async val => { + const { table, column, row } = val; + const filter = await createFilter(table, column, row); + if (filter) { + filter.forEach(f => { + if (event.negate) { + f = esFilters.toggleFilterNegated(f); + } + filters.push(f); + }); + } + }) + ); return filters; }; -export { createFilter, createFiltersFromEvent, onBrushEvent }; +export { createFilter, createFiltersFromEvent }; diff --git a/src/legacy/core_plugins/data/public/actions/select_range_action.ts b/src/legacy/core_plugins/data/public/actions/select_range_action.ts new file mode 100644 index 00000000000000..4ea5c78a9fd2b4 --- /dev/null +++ b/src/legacy/core_plugins/data/public/actions/select_range_action.ts @@ -0,0 +1,91 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + IAction, + createAction, + IncompatibleActionError, +} from '../../../../../plugins/ui_actions/public'; +// @ts-ignore +import { onBrushEvent } from './filters/brush_event'; +import { + esFilters, + FilterManager, + TimefilterContract, + changeTimeFilter, + extractTimeFilter, + mapAndFlattenFilters, +} from '../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getIndexPatterns } from '../../../../../plugins/data/public/services'; + +export const SELECT_RANGE_ACTION = 'SELECT_RANGE_ACTION'; + +interface ActionContext { + data: any; + timeFieldName: string; +} + +async function isCompatible(context: ActionContext) { + try { + const filters: esFilters.Filter[] = (await onBrushEvent(context.data, getIndexPatterns)) || []; + return filters.length > 0; + } catch { + return false; + } +} + +export function selectRangeAction( + filterManager: FilterManager, + timeFilter: TimefilterContract +): IAction { + return createAction({ + type: SELECT_RANGE_ACTION, + id: SELECT_RANGE_ACTION, + getDisplayName: () => { + return i18n.translate('data.filter.applyFilterActionTitle', { + defaultMessage: 'Apply filter to current view', + }); + }, + isCompatible, + execute: async ({ timeFieldName, data }: ActionContext) => { + if (!(await isCompatible({ timeFieldName, data }))) { + throw new IncompatibleActionError(); + } + + const filters: esFilters.Filter[] = (await onBrushEvent(data, getIndexPatterns)) || []; + + const selectedFilters: esFilters.Filter[] = mapAndFlattenFilters(filters); + + if (timeFieldName) { + const { timeRangeFilter, restOfFilters } = extractTimeFilter( + timeFieldName, + selectedFilters + ); + filterManager.addFilters(restOfFilters); + if (timeRangeFilter) { + changeTimeFilter(timeFilter, timeRangeFilter); + } + } else { + filterManager.addFilters(selectedFilters); + } + }, + }); +} diff --git a/src/legacy/core_plugins/data/public/actions/value_click_action.ts b/src/legacy/core_plugins/data/public/actions/value_click_action.ts new file mode 100644 index 00000000000000..2f622eb1eb6695 --- /dev/null +++ b/src/legacy/core_plugins/data/public/actions/value_click_action.ts @@ -0,0 +1,126 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { toMountPoint } from '../../../../../plugins/kibana_react/public'; +import { + IAction, + createAction, + IncompatibleActionError, +} from '../../../../../plugins/ui_actions/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getOverlays, getIndexPatterns } from '../../../../../plugins/data/public/services'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { applyFiltersPopover } from '../../../../../plugins/data/public/ui/apply_filters'; +// @ts-ignore +import { createFiltersFromEvent } from './filters/create_filters_from_event'; +import { + esFilters, + FilterManager, + TimefilterContract, + changeTimeFilter, + extractTimeFilter, + mapAndFlattenFilters, +} from '../../../../../plugins/data/public'; + +export const VALUE_CLICK_ACTION = 'VALUE_CLICK_ACTION'; + +interface ActionContext { + data: any; + timeFieldName: string; +} + +async function isCompatible(context: ActionContext) { + try { + const filters: esFilters.Filter[] = (await createFiltersFromEvent(context.data)) || []; + return filters.length > 0; + } catch { + return false; + } +} + +export function valueClickAction( + filterManager: FilterManager, + timeFilter: TimefilterContract +): IAction { + return createAction({ + type: VALUE_CLICK_ACTION, + id: VALUE_CLICK_ACTION, + getDisplayName: () => { + return i18n.translate('data.filter.applyFilterActionTitle', { + defaultMessage: 'Apply filter to current view', + }); + }, + isCompatible, + execute: async ({ timeFieldName, data }: ActionContext) => { + if (!(await isCompatible({ timeFieldName, data }))) { + throw new IncompatibleActionError(); + } + + const filters: esFilters.Filter[] = (await createFiltersFromEvent(data)) || []; + + let selectedFilters: esFilters.Filter[] = mapAndFlattenFilters(filters); + + if (selectedFilters.length > 1) { + const indexPatterns = await Promise.all( + filters.map(filter => { + return getIndexPatterns().get(filter.meta.index!); + }) + ); + + const filterSelectionPromise: Promise = new Promise(resolve => { + const overlay = getOverlays().openModal( + toMountPoint( + applyFiltersPopover( + filters, + indexPatterns, + () => { + overlay.close(); + resolve([]); + }, + (filterSelection: esFilters.Filter[]) => { + overlay.close(); + resolve(filterSelection); + } + ) + ), + { + 'data-test-subj': 'selectFilterOverlay', + } + ); + }); + + selectedFilters = await filterSelectionPromise; + } + + if (timeFieldName) { + const { timeRangeFilter, restOfFilters } = extractTimeFilter( + timeFieldName, + selectedFilters + ); + filterManager.addFilters(restOfFilters); + if (timeRangeFilter) { + changeTimeFilter(timeFilter, timeRangeFilter); + } + } else { + filterManager.addFilters(selectedFilters); + } + }, + }); +} diff --git a/src/legacy/core_plugins/data/public/legacy.ts b/src/legacy/core_plugins/data/public/legacy.ts index a6646ea338c93c..d37c17c2240727 100644 --- a/src/legacy/core_plugins/data/public/legacy.ts +++ b/src/legacy/core_plugins/data/public/legacy.ts @@ -39,8 +39,6 @@ import { plugin } from '.'; const dataPlugin = plugin(); -export const setup = dataPlugin.setup(npSetup.core); +export const setup = dataPlugin.setup(npSetup.core, npSetup.plugins); -export const start = dataPlugin.start(npStart.core, { - data: npStart.plugins.data, -}); +export const start = dataPlugin.start(npStart.core, npStart.plugins); diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index 6bd85ef020f167..da35366cdff318 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -22,6 +22,7 @@ import { DataPublicPluginStart, addSearchStrategy, defaultSearchStrategy, + DataPublicPluginSetup, } from '../../../../plugins/data/public'; import { ExpressionsSetup } from '../../../../plugins/expressions/public'; @@ -32,15 +33,27 @@ import { setInjectedMetadata, setFieldFormats, setSearchService, + setOverlays, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/data/public/services'; +import { SELECT_RANGE_ACTION, selectRangeAction } from './actions/select_range_action'; +import { VALUE_CLICK_ACTION, valueClickAction } from './actions/value_click_action'; +import { + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../plugins/embeddable/public/lib/triggers'; +import { IUiActionsSetup, IUiActionsStart } from '../../../../plugins/ui_actions/public'; export interface DataPluginSetupDependencies { + data: DataPublicPluginSetup; expressions: ExpressionsSetup; + uiActions: IUiActionsSetup; } export interface DataPluginStartDependencies { data: DataPublicPluginStart; + uiActions: IUiActionsStart; } /** @@ -64,19 +77,30 @@ export interface DataStart {} // eslint-disable-line @typescript-eslint/no-empty export class DataPlugin implements Plugin { - public setup(core: CoreSetup) { + public setup(core: CoreSetup, { data, uiActions }: DataPluginSetupDependencies) { setInjectedMetadata(core.injectedMetadata); // This is to be deprecated once we switch to the new search service fully addSearchStrategy(defaultSearchStrategy); + + uiActions.registerAction( + selectRangeAction(data.query.filterManager, data.query.timefilter.timefilter) + ); + uiActions.registerAction( + valueClickAction(data.query.filterManager, data.query.timefilter.timefilter) + ); } - public start(core: CoreStart, { data }: DataPluginStartDependencies): DataStart { + public start(core: CoreStart, { data, uiActions }: DataPluginStartDependencies): DataStart { setUiSettings(core.uiSettings); setQueryService(data.query); setIndexPatterns(data.indexPatterns); setFieldFormats(data.fieldFormats); setSearchService(data.search); + setOverlays(core.overlays); + + uiActions.attachAction(SELECT_RANGE_TRIGGER, SELECT_RANGE_ACTION); + uiActions.attachAction(VALUE_CLICK_TRIGGER, VALUE_CLICK_ACTION); return {}; } diff --git a/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts index 6e6d2a15fa2ac8..8f7953c408a971 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts @@ -19,9 +19,9 @@ import { set } from 'lodash'; // @ts-ignore -import { createFilter } from '../../../../visualizations/public'; import { FormattedData } from '../../../../../../plugins/inspector/public'; - +// @ts-ignore +import { createFilter } from './create_filter'; interface Column { id: string; name: string; diff --git a/src/legacy/core_plugins/data/public/search/expressions/create_filter.js b/src/legacy/core_plugins/data/public/search/expressions/create_filter.js new file mode 100644 index 00000000000000..3f4028a9b5525b --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/expressions/create_filter.js @@ -0,0 +1,66 @@ +/* + * 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. + */ + +const getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { + if (rowIndex === -1) { + return []; + } + + // get only rows where cell value matches current row for all the fields before columnIndex + const rows = table.rows.filter(row => { + return table.columns.every((column, i) => { + return row[column.id] === table.rows[rowIndex][column.id] || i >= columnIndex; + }); + }); + const terms = rows.map(row => row[table.columns[columnIndex].id]); + + return [ + ...new Set( + terms.filter(term => { + const notOther = term !== '__other__'; + const notMissing = term !== '__missing__'; + return notOther && notMissing; + }) + ), + ]; +}; + +const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { + const column = table.columns[columnIndex]; + const aggConfig = aggConfigs[columnIndex]; + let filter = []; + const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : cellValue; + if (value === null || value === undefined || !aggConfig.isFilterable()) { + return; + } + if (aggConfig.type.name === 'terms' && aggConfig.params.otherBucket) { + const terms = getOtherBucketFilterTerms(table, columnIndex, rowIndex); + filter = aggConfig.createFilter(value, { terms }); + } else { + filter = aggConfig.createFilter(value); + } + + if (!Array.isArray(filter)) { + filter = [filter]; + } + + return filter; +}; + +export { createFilter }; diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts index 143283152d1044..b4ea2cd378d61b 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -46,6 +46,7 @@ import { Adapters } from '../../../../../../plugins/inspector/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getQueryService, getIndexPatterns } from '../../../../../../plugins/data/public/services'; import { getRequestInspectorStats, getResponseInspectorStats } from '../..'; +import { serializeAggConfig } from './utils'; export interface RequestHandlerParams { searchSource: ISearchSource; @@ -289,6 +290,7 @@ export const esaggs = (): ExpressionFunction { + return { + type: aggConfig.type.name, + indexPatternId: aggConfig.getIndexPattern().id, + aggConfigParams: aggConfig.toJSON().params, + }; +}; + +interface DeserializeAggConfigParams { + type: string; + aggConfigParams: Record; + indexPattern: IndexPattern; +} + +export const deserializeAggConfig = ({ + type, + aggConfigParams, + indexPattern, +}: DeserializeAggConfigParams) => { + const aggConfigs = new AggConfigs(indexPattern); + const aggConfig = aggConfigs.createAggConfig({ + enabled: true, + type, + params: aggConfigParams, + }); + return aggConfig; +}; diff --git a/src/legacy/core_plugins/data/public/search/index.ts b/src/legacy/core_plugins/data/public/search/index.ts index e1c93ec0e3b1c4..c975d5772e0a8b 100644 --- a/src/legacy/core_plugins/data/public/search/index.ts +++ b/src/legacy/core_plugins/data/public/search/index.ts @@ -18,3 +18,4 @@ */ export { getRequestInspectorStats, getResponseInspectorStats } from './utils'; +export { serializeAggConfig } from './expressions/utils'; diff --git a/src/legacy/core_plugins/elasticsearch/index.d.ts b/src/legacy/core_plugins/elasticsearch/index.d.ts index 4cbb1c82cc1e40..df713160137a6a 100644 --- a/src/legacy/core_plugins/elasticsearch/index.d.ts +++ b/src/legacy/core_plugins/elasticsearch/index.d.ts @@ -523,6 +523,7 @@ export interface CallCluster { } export interface ElasticsearchPlugin { + status: { on: (status: string, cb: () => void) => void }; getCluster(name: string): Cluster; createCluster(name: string, config: ClusterConfig): Cluster; waitUntilReady(): Promise; diff --git a/src/legacy/core_plugins/elasticsearch/index.js b/src/legacy/core_plugins/elasticsearch/index.js index 5872a33d8aa082..35dd6562aed984 100644 --- a/src/legacy/core_plugins/elasticsearch/index.js +++ b/src/legacy/core_plugins/elasticsearch/index.js @@ -17,10 +17,10 @@ * under the License. */ import { first } from 'rxjs/operators'; -import healthCheck from './server/lib/health_check'; import { Cluster } from './server/lib/cluster'; import { createProxy } from './server/lib/create_proxy'; import { handleESError } from './server/lib/handle_es_error'; +import { versionHealthCheck } from './lib/version_health_check'; export default function(kibana) { let defaultVars; @@ -92,15 +92,13 @@ export default function(kibana) { createProxy(server); - // Set up the health check service and start it. - const { start, waitUntilReady } = healthCheck( + const waitUntilHealthy = versionHealthCheck( this, - server, - esConfig.healthCheckDelay.asMilliseconds(), - esConfig.ignoreVersionMismatch + server.logWithMetadata, + server.newPlatform.__internals.elasticsearch.esNodesCompatibility$ ); - server.expose('waitUntilReady', waitUntilReady); - start(); + + server.expose('waitUntilReady', () => waitUntilHealthy); }, }); } diff --git a/src/legacy/core_plugins/elasticsearch/integration_tests/elasticsearch.test.ts b/src/legacy/core_plugins/elasticsearch/integration_tests/elasticsearch.test.ts new file mode 100644 index 00000000000000..5806c31b784147 --- /dev/null +++ b/src/legacy/core_plugins/elasticsearch/integration_tests/elasticsearch.test.ts @@ -0,0 +1,89 @@ +/* + * 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 { + createTestServers, + TestElasticsearchUtils, + TestKibanaUtils, + TestUtils, + createRootWithCorePlugins, + getKbnServer, +} from '../../../../test_utils/kbn_server'; + +import { BehaviorSubject } from 'rxjs'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { NodesVersionCompatibility } from 'src/core/server/elasticsearch/version_check/ensure_es_version'; + +describe('Elasticsearch plugin', () => { + let servers: TestUtils; + let esServer: TestElasticsearchUtils; + let root: TestKibanaUtils['root']; + let elasticsearch: TestKibanaUtils['kbnServer']['server']['plugins']['elasticsearch']; + + const esNodesCompatibility$ = new BehaviorSubject({ + isCompatible: true, + incompatibleNodes: [], + warningNodes: [], + kibanaVersion: '8.0.0', + }); + + beforeAll(async function() { + const settings = { + elasticsearch: {}, + adjustTimeout: (t: any) => { + jest.setTimeout(t); + }, + }; + servers = createTestServers(settings); + esServer = await servers.startES(); + + const elasticsearchSettings = { + hosts: esServer.hosts, + username: esServer.username, + password: esServer.password, + }; + root = createRootWithCorePlugins({ elasticsearch: elasticsearchSettings }); + + const setup = await root.setup(); + setup.elasticsearch.esNodesCompatibility$ = esNodesCompatibility$; + await root.start(); + + elasticsearch = getKbnServer(root).server.plugins.elasticsearch; + }); + + afterAll(async () => { + await esServer.stop(); + await root.shutdown(); + }, 30000); + + it("should set it's status to green when all nodes are compatible", done => { + jest.setTimeout(30000); + elasticsearch.status.on('green', () => done()); + }); + + it("should set it's status to red when some nodes aren't compatible", done => { + esNodesCompatibility$.next({ + isCompatible: false, + incompatibleNodes: [], + warningNodes: [], + kibanaVersion: '8.0.0', + }); + elasticsearch.status.on('red', () => done()); + }); +}); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/kibana_version.js b/src/legacy/core_plugins/elasticsearch/lib/version_health_check.js similarity index 57% rename from src/legacy/core_plugins/elasticsearch/server/lib/kibana_version.js rename to src/legacy/core_plugins/elasticsearch/lib/version_health_check.js index 344dbbb5bdf692..4ee8307f490eb0 100644 --- a/src/legacy/core_plugins/elasticsearch/server/lib/kibana_version.js +++ b/src/legacy/core_plugins/elasticsearch/lib/version_health_check.js @@ -17,11 +17,23 @@ * under the License. */ -import { version as kibanaVersion } from '../../../../../../package.json'; +export const versionHealthCheck = (esPlugin, logWithMetadata, esNodesCompatibility$) => { + esPlugin.status.yellow('Waiting for Elasticsearch'); -export default { - // Make the version stubbable to improve testability. - get() { - return kibanaVersion; - }, + return new Promise(resolve => { + esNodesCompatibility$.subscribe(({ isCompatible, message, kibanaVersion, warningNodes }) => { + if (!isCompatible) { + esPlugin.status.red(message); + } else { + if (message) { + logWithMetadata(['warning'], message, { + kibanaVersion, + nodes: warningNodes, + }); + } + esPlugin.status.green('Ready'); + resolve(); + } + }); + }); }; diff --git a/src/legacy/core_plugins/elasticsearch/lib/version_health_check.test.js b/src/legacy/core_plugins/elasticsearch/lib/version_health_check.test.js new file mode 100644 index 00000000000000..ba7c95bcdfec54 --- /dev/null +++ b/src/legacy/core_plugins/elasticsearch/lib/version_health_check.test.js @@ -0,0 +1,71 @@ +/* + * 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 { versionHealthCheck } from './version_health_check'; +import { Subject } from 'rxjs'; + +describe('plugins/elasticsearch', () => { + describe('lib/health_version_check', function() { + let plugin; + let logWithMetadata; + + beforeEach(() => { + plugin = { + status: { + red: jest.fn(), + green: jest.fn(), + yellow: jest.fn(), + }, + }; + + logWithMetadata = jest.fn(); + jest.clearAllMocks(); + }); + + it('returned promise resolves when all nodes are compatible ', function() { + const esNodesCompatibility$ = new Subject(); + const versionHealthyPromise = versionHealthCheck( + plugin, + logWithMetadata, + esNodesCompatibility$ + ); + esNodesCompatibility$.next({ isCompatible: true, message: undefined }); + return expect(versionHealthyPromise).resolves.toBe(undefined); + }); + + it('should set elasticsearch plugin status to green when all nodes are compatible', function() { + const esNodesCompatibility$ = new Subject(); + versionHealthCheck(plugin, logWithMetadata, esNodesCompatibility$); + expect(plugin.status.yellow).toHaveBeenCalledWith('Waiting for Elasticsearch'); + expect(plugin.status.green).not.toHaveBeenCalled(); + esNodesCompatibility$.next({ isCompatible: true, message: undefined }); + expect(plugin.status.green).toHaveBeenCalledWith('Ready'); + expect(plugin.status.red).not.toHaveBeenCalled(); + }); + + it('should set elasticsearch plugin status to red when some nodes are incompatible', function() { + const esNodesCompatibility$ = new Subject(); + versionHealthCheck(plugin, logWithMetadata, esNodesCompatibility$); + expect(plugin.status.yellow).toHaveBeenCalledWith('Waiting for Elasticsearch'); + expect(plugin.status.red).not.toHaveBeenCalled(); + esNodesCompatibility$.next({ isCompatible: false, message: 'your nodes are incompatible' }); + expect(plugin.status.red).toHaveBeenCalledWith('your nodes are incompatible'); + expect(plugin.status.green).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/ensure_es_version.js b/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/ensure_es_version.js deleted file mode 100644 index 781d198c662364..00000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/ensure_es_version.js +++ /dev/null @@ -1,223 +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 _ from 'lodash'; -import Bluebird from 'bluebird'; -import sinon from 'sinon'; -import expect from '@kbn/expect'; - -import { esTestConfig } from '@kbn/test'; -import { ensureEsVersion } from '../ensure_es_version'; - -describe('plugins/elasticsearch', () => { - describe('lib/ensure_es_version', () => { - const KIBANA_VERSION = '5.1.0'; - - let server; - - beforeEach(function() { - server = { - log: sinon.stub(), - logWithMetadata: sinon.stub(), - plugins: { - elasticsearch: { - getCluster: sinon - .stub() - .withArgs('admin') - .returns({ callWithInternalUser: sinon.stub() }), - status: { - red: sinon.stub(), - }, - url: esTestConfig.getUrl(), - }, - }, - config() { - return { - get: sinon.stub(), - }; - }, - }; - }); - - function setNodes(/* ...versions */) { - const versions = _.shuffle(arguments); - const nodes = {}; - let i = 0; - - while (versions.length) { - const name = 'node-' + ++i; - const version = versions.shift(); - - const node = { - version: version, - http: { - publish_address: 'http_address', - }, - ip: 'ip', - }; - - if (!_.isString(version)) _.assign(node, version); - nodes[name] = node; - } - - const cluster = server.plugins.elasticsearch.getCluster('admin'); - cluster.callWithInternalUser - .withArgs('nodes.info', sinon.match.any) - .returns(Bluebird.resolve({ nodes: nodes })); - } - - function setNodeWithoutHTTP(version) { - const nodes = { 'node-without-http': { version, ip: 'ip' } }; - const cluster = server.plugins.elasticsearch.getCluster('admin'); - cluster.callWithInternalUser - .withArgs('nodes.info', sinon.match.any) - .returns(Bluebird.resolve({ nodes: nodes })); - } - - it('returns true with single a node that matches', async () => { - setNodes('5.1.0'); - const result = await ensureEsVersion(server, KIBANA_VERSION); - expect(result).to.be(true); - }); - - it('returns true with multiple nodes that satisfy', async () => { - setNodes('5.1.0', '5.2.0', '5.1.1-Beta1'); - const result = await ensureEsVersion(server, KIBANA_VERSION); - expect(result).to.be(true); - }); - - it('throws an error with a single node that is out of date', async () => { - // 5.0.0 ES is too old to work with a 5.1.0 version of Kibana. - setNodes('5.1.0', '5.2.0', '5.0.0'); - try { - await ensureEsVersion(server, KIBANA_VERSION); - } catch (e) { - expect(e).to.be.a(Error); - } - }); - - it('does not throw on outdated nodes, if `ignoreVersionMismatch` is enabled in development mode', async () => { - // set config values - server.config = () => ({ - get: name => { - switch (name) { - case 'env.dev': - return true; - default: - throw new Error(`Unknown option "${name}"`); - } - }, - }); - - // 5.0.0 ES is too old to work with a 5.1.0 version of Kibana. - setNodes('5.1.0', '5.2.0', '5.0.0'); - - const ignoreVersionMismatch = true; - const result = await ensureEsVersion(server, KIBANA_VERSION, ignoreVersionMismatch); - expect(result).to.be(true); - }); - - it('throws an error if `ignoreVersionMismatch` is enabled in production mode', async () => { - // set config values - server.config = () => ({ - get: name => { - switch (name) { - case 'env.dev': - return false; - default: - throw new Error(`Unknown option "${name}"`); - } - }, - }); - - // 5.0.0 ES is too old to work with a 5.1.0 version of Kibana. - setNodes('5.1.0', '5.2.0', '5.0.0'); - - try { - const ignoreVersionMismatch = true; - await ensureEsVersion(server, KIBANA_VERSION, ignoreVersionMismatch); - } catch (e) { - expect(e).to.be.a(Error); - } - }); - - it('fails if that single node is a client node', async () => { - setNodes('5.1.0', '5.2.0', { version: '5.0.0', attributes: { client: 'true' } }); - try { - await ensureEsVersion(server, KIBANA_VERSION); - } catch (e) { - expect(e).to.be.a(Error); - } - }); - - it('warns if a node is only off by a patch version', async () => { - setNodes('5.1.1'); - await ensureEsVersion(server, KIBANA_VERSION); - sinon.assert.callCount(server.logWithMetadata, 2); - expect(server.logWithMetadata.getCall(0).args[0]).to.contain('debug'); - expect(server.logWithMetadata.getCall(1).args[0]).to.contain('warning'); - }); - - it('warns if a node is off by a patch version and without http publish address', async () => { - setNodeWithoutHTTP('5.1.1'); - await ensureEsVersion(server, KIBANA_VERSION); - sinon.assert.callCount(server.logWithMetadata, 2); - expect(server.logWithMetadata.getCall(0).args[0]).to.contain('debug'); - expect(server.logWithMetadata.getCall(1).args[0]).to.contain('warning'); - }); - - it('errors if a node incompatible and without http publish address', async () => { - setNodeWithoutHTTP('6.1.1'); - try { - await ensureEsVersion(server, KIBANA_VERSION); - } catch (e) { - expect(e.message).to.contain('incompatible nodes'); - expect(e).to.be.a(Error); - } - }); - - it('only warns once per node list', async () => { - setNodes('5.1.1'); - - await ensureEsVersion(server, KIBANA_VERSION); - sinon.assert.callCount(server.logWithMetadata, 2); - expect(server.logWithMetadata.getCall(0).args[0]).to.contain('debug'); - expect(server.logWithMetadata.getCall(1).args[0]).to.contain('warning'); - - await ensureEsVersion(server, KIBANA_VERSION); - sinon.assert.callCount(server.logWithMetadata, 3); - expect(server.logWithMetadata.getCall(2).args[0]).to.contain('debug'); - }); - - it('warns again if the node list changes', async () => { - setNodes('5.1.1'); - - await ensureEsVersion(server, KIBANA_VERSION); - sinon.assert.callCount(server.logWithMetadata, 2); - expect(server.logWithMetadata.getCall(0).args[0]).to.contain('debug'); - expect(server.logWithMetadata.getCall(1).args[0]).to.contain('warning'); - - setNodes('5.1.2'); - await ensureEsVersion(server, KIBANA_VERSION); - sinon.assert.callCount(server.logWithMetadata, 4); - expect(server.logWithMetadata.getCall(2).args[0]).to.contain('debug'); - expect(server.logWithMetadata.getCall(3).args[0]).to.contain('warning'); - }); - }); -}); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/health_check.js b/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/health_check.js deleted file mode 100644 index 3b593c6352394e..00000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/health_check.js +++ /dev/null @@ -1,151 +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 Bluebird from 'bluebird'; -import sinon from 'sinon'; -import expect from '@kbn/expect'; - -const NoConnections = require('elasticsearch').errors.NoConnections; - -import healthCheck from '../health_check'; -import kibanaVersion from '../kibana_version'; - -const esPort = 9220; - -describe('plugins/elasticsearch', () => { - describe('lib/health_check', function() { - let health; - let plugin; - let cluster; - let server; - const sandbox = sinon.createSandbox(); - - function getTimerCount() { - return Object.keys(sandbox.clock.timers || {}).length; - } - - beforeEach(() => { - sandbox.useFakeTimers(); - const COMPATIBLE_VERSION_NUMBER = '5.0.0'; - - // Stub the Kibana version instead of drawing from package.json. - sandbox.stub(kibanaVersion, 'get').returns(COMPATIBLE_VERSION_NUMBER); - - // setup the plugin stub - plugin = { - name: 'elasticsearch', - status: { - red: sinon.stub(), - green: sinon.stub(), - yellow: sinon.stub(), - }, - }; - - cluster = { callWithInternalUser: sinon.stub(), errors: { NoConnections } }; - cluster.callWithInternalUser.withArgs('index', sinon.match.any).returns(Bluebird.resolve()); - cluster.callWithInternalUser - .withArgs('mget', sinon.match.any) - .returns(Bluebird.resolve({ ok: true })); - cluster.callWithInternalUser - .withArgs('get', sinon.match.any) - .returns(Bluebird.resolve({ found: false })); - cluster.callWithInternalUser - .withArgs('search', sinon.match.any) - .returns(Bluebird.resolve({ hits: { hits: [] } })); - cluster.callWithInternalUser.withArgs('nodes.info', sinon.match.any).returns( - Bluebird.resolve({ - nodes: { - 'node-01': { - version: COMPATIBLE_VERSION_NUMBER, - http_address: `inet[/127.0.0.1:${esPort}]`, - ip: '127.0.0.1', - }, - }, - }) - ); - - // Setup the server mock - server = { - logWithMetadata: sinon.stub(), - info: { port: 5601 }, - config: () => ({ get: sinon.stub() }), - plugins: { - elasticsearch: { - getCluster: sinon.stub().returns(cluster), - }, - }, - ext: sinon.stub(), - }; - - health = healthCheck(plugin, server, 0); - }); - - afterEach(() => sandbox.restore()); - - it('should stop when cluster is shutdown', () => { - // ensure that health.start() is responsible for the timer we are observing - expect(getTimerCount()).to.be(0); - health.start(); - expect(getTimerCount()).to.be(1); - - // ensure that a server extension was registered - sinon.assert.calledOnce(server.ext); - sinon.assert.calledWithExactly(server.ext, sinon.match.string, sinon.match.func); - - const [, handler] = server.ext.firstCall.args; - handler(); // this should be health.stop - - // ensure that the handler unregistered the timer - expect(getTimerCount()).to.be(0); - }); - - it('should set the cluster green if everything is ready', function() { - cluster.callWithInternalUser.withArgs('ping').returns(Bluebird.resolve()); - - return health.run().then(function() { - sinon.assert.calledOnce(plugin.status.yellow); - sinon.assert.calledWithExactly(plugin.status.yellow, 'Waiting for Elasticsearch'); - - sinon.assert.calledOnce( - cluster.callWithInternalUser.withArgs('nodes.info', sinon.match.any) - ); - sinon.assert.notCalled(plugin.status.red); - sinon.assert.calledOnce(plugin.status.green); - sinon.assert.calledWithExactly(plugin.status.green, 'Ready'); - }); - }); - - describe('#waitUntilReady', function() { - it('waits for green status', function() { - plugin.status.once = sinon.spy(function(event, handler) { - expect(event).to.be('green'); - setImmediate(handler); - }); - - const waitUntilReadyPromise = health.waitUntilReady(); - - sandbox.clock.runAll(); - - return waitUntilReadyPromise.then(function() { - sinon.assert.calledOnce(plugin.status.once); - }); - }); - }); - }); -}); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/ensure_es_version.js b/src/legacy/core_plugins/elasticsearch/server/lib/ensure_es_version.js deleted file mode 100644 index 8d304cd5584182..00000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/ensure_es_version.js +++ /dev/null @@ -1,126 +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. - */ - -/** - * ES and Kibana versions are locked, so Kibana should require that ES has the same version as - * that defined in Kibana's package.json. - */ - -import { forEach, get } from 'lodash'; -import { coerce } from 'semver'; -import isEsCompatibleWithKibana from './is_es_compatible_with_kibana'; - -/** - * tracks the node descriptions that get logged in warnings so - * that we don't spam the log with the same message over and over. - * - * There are situations, like in testing or multi-tenancy, where - * the server argument changes, so we must track the previous - * node warnings per server - */ -const lastWarnedNodesForServer = new WeakMap(); - -export function ensureEsVersion(server, kibanaVersion, ignoreVersionMismatch = false) { - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); - - server.logWithMetadata(['plugin', 'debug'], 'Checking Elasticsearch version'); - return callWithInternalUser('nodes.info', { - filterPath: ['nodes.*.version', 'nodes.*.http.publish_address', 'nodes.*.ip'], - }).then(function(info) { - // Aggregate incompatible ES nodes. - const incompatibleNodes = []; - - // Aggregate ES nodes which should prompt a Kibana upgrade. - const warningNodes = []; - - forEach(info.nodes, esNode => { - if (!isEsCompatibleWithKibana(esNode.version, kibanaVersion)) { - // Exit early to avoid collecting ES nodes with newer major versions in the `warningNodes`. - return incompatibleNodes.push(esNode); - } - - // It's acceptable if ES and Kibana versions are not the same so long as - // they are not incompatible, but we should warn about it - - // Ignore version qualifiers - // https://github.com/elastic/elasticsearch/issues/36859 - const looseMismatch = coerce(esNode.version).version !== coerce(kibanaVersion).version; - if (looseMismatch) { - warningNodes.push(esNode); - } - }); - - function getHumanizedNodeNames(nodes) { - return nodes.map(node => { - const publishAddress = get(node, 'http.publish_address') - ? get(node, 'http.publish_address') + ' ' - : ''; - return 'v' + node.version + ' @ ' + publishAddress + '(' + node.ip + ')'; - }); - } - - if (warningNodes.length) { - const simplifiedNodes = warningNodes.map(node => ({ - version: node.version, - http: { - publish_address: get(node, 'http.publish_address'), - }, - ip: node.ip, - })); - - // Don't show the same warning over and over again. - const warningNodeNames = getHumanizedNodeNames(simplifiedNodes).join(', '); - if (lastWarnedNodesForServer.get(server) !== warningNodeNames) { - lastWarnedNodesForServer.set(server, warningNodeNames); - server.logWithMetadata( - ['warning'], - `You're running Kibana ${kibanaVersion} with some different versions of ` + - 'Elasticsearch. Update Kibana or Elasticsearch to the same ' + - `version to prevent compatibility issues: ${warningNodeNames}`, - { - kibanaVersion, - nodes: simplifiedNodes, - } - ); - } - } - - if (incompatibleNodes.length && !shouldIgnoreVersionMismatch(server, ignoreVersionMismatch)) { - const incompatibleNodeNames = getHumanizedNodeNames(incompatibleNodes); - throw new Error( - `This version of Kibana requires Elasticsearch v` + - `${kibanaVersion} on all nodes. I found ` + - `the following incompatible nodes in your cluster: ${incompatibleNodeNames.join(', ')}` - ); - } - - return true; - }); -} - -function shouldIgnoreVersionMismatch(server, ignoreVersionMismatch) { - const isDevMode = server.config().get('env.dev'); - if (!isDevMode && ignoreVersionMismatch) { - throw new Error( - `Option "elasticsearch.ignoreVersionMismatch" can only be used in development mode` - ); - } - - return isDevMode && ignoreVersionMismatch; -} diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/health_check.js b/src/legacy/core_plugins/elasticsearch/server/lib/health_check.js deleted file mode 100644 index 40053ec7745421..00000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/health_check.js +++ /dev/null @@ -1,75 +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 Bluebird from 'bluebird'; -import kibanaVersion from './kibana_version'; -import { ensureEsVersion } from './ensure_es_version'; - -export default function(plugin, server, requestDelay, ignoreVersionMismatch) { - plugin.status.yellow('Waiting for Elasticsearch'); - - function waitUntilReady() { - return new Bluebird(resolve => { - plugin.status.once('green', resolve); - }); - } - - function check() { - return ensureEsVersion(server, kibanaVersion.get(), ignoreVersionMismatch) - .then(() => plugin.status.green('Ready')) - .catch(err => plugin.status.red(err)); - } - - let timeoutId = null; - - function scheduleCheck(ms) { - if (timeoutId) return; - - const myId = setTimeout(function() { - check().finally(function() { - if (timeoutId === myId) startorRestartChecking(); - }); - }, ms); - - timeoutId = myId; - } - - function startorRestartChecking() { - scheduleCheck(stopChecking() ? requestDelay : 1); - } - - function stopChecking() { - if (!timeoutId) return false; - clearTimeout(timeoutId); - timeoutId = null; - return true; - } - - server.ext('onPreStop', stopChecking); - - return { - waitUntilReady: waitUntilReady, - run: check, - start: startorRestartChecking, - stop: stopChecking, - isRunning: function() { - return !!timeoutId; - }, - }; -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index 97d165b6b5c23e..14bdef99ca8948 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -38,15 +38,8 @@ export { showSaveModal, SaveResult } from 'ui/saved_objects/show_saved_object_sa export { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; export { KbnUrl } from 'ui/url/kbn_url'; // @ts-ignore -export { GlobalStateProvider } from 'ui/state_management/global_state'; -// @ts-ignore -export { StateManagementConfigProvider } from 'ui/state_management/config_provider'; -// @ts-ignore export { PrivateProvider } from 'ui/private/private'; // @ts-ignore -export { EventsProvider } from 'ui/events'; -export { PersistedState } from 'ui/persisted_state'; -// @ts-ignore export { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_top_nav'; // @ts-ignore export { PromiseServiceCreator } from 'ui/promises/promises'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index 1a0a99311d06b0..0d461028d994a3 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -33,15 +33,12 @@ import { confirmModalFactory, createTopNavDirective, createTopNavHelper, - EventsProvider, IPrivate, KbnUrlProvider, - PersistedState, PrivateProvider, PromiseServiceCreator, RedirectWhenMissingProvider, SavedObjectLoader, - StateManagementConfigProvider, } from '../legacy_imports'; // @ts-ignore import { initDashboardApp } from './legacy_app'; @@ -112,8 +109,6 @@ function createLocalAngularModule(core: AppMountContext['core'], navigation: Nav createLocalPromiseModule(); createLocalConfigModule(core); createLocalKbnUrlModule(); - createLocalStateModule(); - createLocalPersistedStateModule(); createLocalTopNavModule(navigation); createLocalConfirmModalModule(); createLocalIconModule(); @@ -123,9 +118,9 @@ function createLocalAngularModule(core: AppMountContext['core'], navigation: Nav 'app/dashboard/Config', 'app/dashboard/I18n', 'app/dashboard/Private', - 'app/dashboard/PersistedState', 'app/dashboard/TopNav', - 'app/dashboard/State', + 'app/dashboard/KbnUrl', + 'app/dashboard/Promise', 'app/dashboard/ConfirmModal', 'app/dashboard/icon', ]); @@ -145,29 +140,6 @@ function createLocalConfirmModalModule() { .directive('confirmModal', reactDirective => reactDirective(EuiConfirmModal)); } -function createLocalStateModule() { - angular.module('app/dashboard/State', [ - 'app/dashboard/Private', - 'app/dashboard/Config', - 'app/dashboard/KbnUrl', - 'app/dashboard/Promise', - 'app/dashboard/PersistedState', - ]); -} - -function createLocalPersistedStateModule() { - angular - .module('app/dashboard/PersistedState', ['app/dashboard/Private', 'app/dashboard/Promise']) - .factory('PersistedState', (Private: IPrivate) => { - const Events = Private(EventsProvider); - return class AngularPersistedState extends PersistedState { - constructor(value: any, path: any) { - super(value, path, Events); - } - }; - }); -} - function createLocalKbnUrlModule() { angular .module('app/dashboard/KbnUrl', ['app/dashboard/Private', 'ngRoute']) @@ -176,16 +148,13 @@ function createLocalKbnUrlModule() { } function createLocalConfigModule(core: AppMountContext['core']) { - angular - .module('app/dashboard/Config', ['app/dashboard/Private']) - .provider('stateManagementConfig', StateManagementConfigProvider) - .provider('config', () => { - return { - $get: () => ({ - get: core.uiSettings.get.bind(core.uiSettings), - }), - }; - }); + angular.module('app/dashboard/Config', ['app/dashboard/Private']).provider('config', () => { + return { + $get: () => ({ + get: core.uiSettings.get.bind(core.uiSettings), + }), + }; + }); } function createLocalPromiseModule() { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js index 2121b51ea78d85..abc0c789326f8a 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js @@ -59,11 +59,6 @@ export function initDashboardApp(app, deps) { addHelpMenuToAppChrome(deps.chrome, deps.core.docLinks); } - app.config(stateManagementConfigProvider => { - // Dashboard state management is handled by state containers and state_sync utilities - stateManagementConfigProvider.disable(); - }); - app.factory('history', () => createHashHistory()); app.factory('kbnUrlStateStorage', history => createKbnUrlStateStorage({ @@ -141,7 +136,7 @@ export function initDashboardApp(app, deps) { }); }, resolve: { - dash: function($rootScope, $route, redirectWhenMissing, kbnUrl) { + dash: function($rootScope, $route, redirectWhenMissing, kbnUrl, history) { return ensureDefaultIndexPattern(deps.core, deps.npDataStart, $rootScope, kbnUrl).then( () => { const savedObjectsClient = deps.savedObjectsClient; @@ -160,13 +155,13 @@ export function initDashboardApp(app, deps) { dashboard.attributes.title.toLowerCase() === title.toLowerCase() ); if (matchingDashboards.length === 1) { - kbnUrl.redirect(createDashboardEditUrl(matchingDashboards[0].id)); + history.replace(createDashboardEditUrl(matchingDashboards[0].id)); } else { - kbnUrl.redirect( + history.replace( `${DashboardConstants.LANDING_PAGE_PATH}?filter="${title}"` ); + $route.reload(); } - $rootScope.$digest(); return new Promise(() => {}); }); } diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx index 6f0a5a3784b070..e66dff01b6bf21 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx @@ -34,8 +34,8 @@ jest.mock('@elastic/eui', () => ({ jest.mock('../../../legacy_imports', () => ({ getTableAggs: jest.fn(), })); -jest.mock('../../../../../visualizations/public', () => ({ - createFiltersFromEvent: jest.fn().mockReturnValue(['yes']), +jest.mock('../../../../../data/public/actions/filters/create_filters_from_event', () => ({ + createFiltersFromEvent: jest.fn().mockResolvedValue(['yes']), })); const vis = { @@ -95,8 +95,8 @@ const uiState = { setSilent: jest.fn(), }; -const getWrapper = (props?: Partial) => - mount( +const getWrapper = async (props?: Partial) => { + const wrapper = mount( ) => ); + await (wrapper.find(VisLegend).instance() as VisLegend).refresh(); + wrapper.update(); + return wrapper; +}; + const getLegendItems = (wrapper: ReactWrapper) => wrapper.find('.visLegend__button'); describe('VisLegend Component', () => { @@ -120,9 +125,9 @@ describe('VisLegend Component', () => { }); describe('Legend open', () => { - beforeEach(() => { + beforeEach(async () => { mockState.set('vis.legendOpen', true); - wrapper = getWrapper(); + wrapper = await getWrapper(); }); it('should match the snapshot', () => { @@ -131,9 +136,9 @@ describe('VisLegend Component', () => { }); describe('Legend closed', () => { - beforeEach(() => { + beforeEach(async () => { mockState.set('vis.legendOpen', false); - wrapper = getWrapper(); + wrapper = await getWrapper(); }); it('should match the snapshot', () => { @@ -142,25 +147,26 @@ describe('VisLegend Component', () => { }); describe('Highlighting', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); - it('should call highlight handler when legend item is focused', () => { + it('should call highlight handler when legend item is focused', async () => { const first = getLegendItems(wrapper).first(); + first.simulate('focus'); expect(vislibVis.handler.highlight).toHaveBeenCalledTimes(1); }); - it('should call highlight handler when legend item is hovered', () => { + it('should call highlight handler when legend item is hovered', async () => { const first = getLegendItems(wrapper).first(); first.simulate('mouseEnter'); expect(vislibVis.handler.highlight).toHaveBeenCalledTimes(1); }); - it('should call unHighlight handler when legend item is blurred', () => { + it('should call unHighlight handler when legend item is blurred', async () => { let first = getLegendItems(wrapper).first(); first.simulate('focus'); first = getLegendItems(wrapper).first(); @@ -169,7 +175,7 @@ describe('VisLegend Component', () => { expect(vislibVis.handler.unHighlight).toHaveBeenCalledTimes(1); }); - it('should call unHighlight handler when legend item is unhovered', () => { + it('should call unHighlight handler when legend item is unhovered', async () => { const first = getLegendItems(wrapper).first(); first.simulate('mouseEnter'); @@ -187,8 +193,8 @@ describe('VisLegend Component', () => { }, }; - expect(() => { - wrapper = getWrapper({ vis: newVis }); + expect(async () => { + wrapper = await getWrapper({ vis: newVis }); const first = getLegendItems(wrapper).first(); first.simulate('focus'); first.simulate('blur'); @@ -197,8 +203,8 @@ describe('VisLegend Component', () => { }); describe('Filtering', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); it('should filter out when clicked', () => { @@ -223,8 +229,8 @@ describe('VisLegend Component', () => { }); describe('Toggles details', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); it('should show details when clicked', () => { @@ -236,8 +242,8 @@ describe('VisLegend Component', () => { }); describe('setColor', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); it('sets the color in the UI state', () => { @@ -255,18 +261,18 @@ describe('VisLegend Component', () => { }); describe('toggleLegend function', () => { - it('click should show legend once toggled from hidden', () => { + it('click should show legend once toggled from hidden', async () => { mockState.set('vis.legendOpen', false); - wrapper = getWrapper(); + wrapper = await getWrapper(); const toggleButton = wrapper.find('.visLegend__toggle').first(); toggleButton.simulate('click'); expect(wrapper.exists('.visLegend__list')).toBe(true); }); - it('click should hide legend once toggled from shown', () => { + it('click should hide legend once toggled from shown', async () => { mockState.set('vis.legendOpen', true); - wrapper = getWrapper(); + wrapper = await getWrapper(); const toggleButton = wrapper.find('.visLegend__toggle').first(); toggleButton.simulate('click'); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx index 0eec557dd334ee..a170af33583df0 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx @@ -24,7 +24,8 @@ import { i18n } from '@kbn/i18n'; import { EuiPopoverProps, EuiIcon, keyCodes, htmlIdGenerator } from '@elastic/eui'; // @ts-ignore -import { createFiltersFromEvent } from '../../../../../visualizations/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createFiltersFromEvent } from '../../../../../data/public/actions/filters/create_filters_from_event'; import { CUSTOM_LEGEND_VIS_TYPES, LegendItem } from './models'; import { VisLegendItem } from './legend_item'; import { getPieNames } from './pie_utils'; @@ -94,11 +95,11 @@ export class VisLegend extends PureComponent { this.props.vis.API.events.filter({ data, negate }); }; - canFilter = (item: LegendItem): boolean => { + canFilter = async (item: LegendItem): Promise => { if (CUSTOM_LEGEND_VIS_TYPES.includes(this.props.vislibVis.visConfigArgs.type)) { return false; } - const filters = createFiltersFromEvent({ aggConfigs: this.state.tableAggs, data: item.values }); + const filters = await createFiltersFromEvent({ data: item.values }); return Boolean(filters.length); }; @@ -123,16 +124,39 @@ export class VisLegend extends PureComponent { }; // Most of these functions were moved directly from the old Legend class. Not a fan of this. - getLabels = (data: any, type: string) => { - if (!data) return []; - data = data.columns || data.rows || [data]; + setLabels = (data: any, type: string): Promise => + new Promise(async resolve => { + let labels = []; + if (CUSTOM_LEGEND_VIS_TYPES.includes(type)) { + const legendLabels = this.props.vislibVis.getLegendLabels(); + if (legendLabels) { + labels = map(legendLabels, label => { + return { label }; + }); + } + } else { + if (!data) return []; + data = data.columns || data.rows || [data]; - if (type === 'pie') return getPieNames(data); + labels = type === 'pie' ? getPieNames(data) : this.getSeriesLabels(data); + } - return this.getSeriesLabels(data); - }; + const labelsConfig = await Promise.all( + labels.map(async label => ({ + ...label, + canFilter: await this.canFilter(label), + })) + ); + + this.setState( + { + labels: labelsConfig, + }, + resolve + ); + }); - refresh = () => { + refresh = async () => { const vislibVis = this.props.vislibVis; if (!vislibVis || !vislibVis.visConfig) { this.setState({ @@ -154,24 +178,12 @@ export class VisLegend extends PureComponent { this.setState({ open: this.props.vis.params.addLegend }); } - if (CUSTOM_LEGEND_VIS_TYPES.includes(vislibVis.visConfigArgs.type)) { - const legendLabels = this.props.vislibVis.getLegendLabels(); - if (legendLabels) { - this.setState({ - labels: map(legendLabels, label => { - return { label }; - }), - }); - } - } else { - this.setState({ labels: this.getLabels(this.props.visData, vislibVis.visConfigArgs.type) }); - } - if (vislibVis.visConfig) { this.getColor = this.props.vislibVis.visConfig.data.getColorFunc(); } this.setState({ tableAggs: getTableAggs(this.props.vis) }); + await this.setLabels(this.props.visData, vislibVis.visConfigArgs.type); }; highlight = (event: BaseSyntheticEvent) => { @@ -219,7 +231,7 @@ export class VisLegend extends PureComponent { key={item.label} anchorPosition={anchorPosition} selected={this.state.selectedLabel === item.label} - canFilter={this.canFilter(item)} + canFilter={item.canFilter} onFilter={this.filter} onSelect={this.toggleDetails} legendId={this.legendId} diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts index 2af468ff77de6e..d3badcc6bdc3fc 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -17,13 +17,12 @@ * under the License. */ -import _, { forEach } from 'lodash'; +import _ from 'lodash'; import { PersistedState } from 'ui/persisted_state'; import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; import { SavedObject } from 'ui/saved_objects/types'; -import { getTableAggs } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { AppState } from 'ui/state_management/app_state'; import { npStart } from 'ui/new_platform'; import { IExpressionLoaderParams } from 'src/plugins/expressions/public'; @@ -34,7 +33,6 @@ import { Query, onlyDisabledFiltersChanged, esFilters, - mapAndFlattenFilters, ISearchSource, } from '../../../../../plugins/data/public'; import { @@ -42,7 +40,8 @@ import { EmbeddableOutput, Embeddable, Container, - APPLY_FILTER_TRIGGER, + VALUE_CLICK_TRIGGER, + SELECT_RANGE_TRIGGER, } from '../../../../../plugins/embeddable/public'; import { dispatchRenderComplete } from '../../../../../plugins/kibana_utils/public'; import { SavedSearch } from '../../../kibana/public/discover/np_ready/types'; @@ -105,7 +104,6 @@ export class VisualizeEmbeddable extends Embeddable { - if (event.disabled || !eventName) { - return; - } else { - this.actions[eventName] = event.defaultAction; - } - }); - // This is a hack to give maps visualizations access to data in the // globalState, since they can no longer access it via searchSource. // TODO: Remove this as a part of elastic/kibana#30593 @@ -301,18 +290,13 @@ export class VisualizeEmbeddable extends Embeddable { - if (this.actions[event.name]) { - event.data.aggConfigs = getTableAggs(this.vis); - const filters: esFilters.Filter[] = this.actions[event.name](event.data) || []; - const mappedFilters = mapAndFlattenFilters(filters); - const timeFieldName = this.vis.indexPattern.timeFieldName; - - npStart.plugins.uiActions.executeTriggerActions(APPLY_FILTER_TRIGGER, { - embeddable: this, - filters: mappedFilters, - timeFieldName, - }); - } + const eventName = event.name === 'brush' ? SELECT_RANGE_TRIGGER : VALUE_CLICK_TRIGGER; + + npStart.plugins.uiActions.executeTriggerActions(eventName, { + embeddable: this, + timeFieldName: this.vis.indexPattern.timeFieldName, + data: event.data, + }); }) ); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts deleted file mode 100644 index 4558621dc6615a..00000000000000 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts +++ /dev/null @@ -1,21 +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. - */ - -// @ts-ignore -export * from './vis_filters'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts index 4dffcb8ce995e3..3c4a1c1449d475 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts @@ -44,7 +44,6 @@ export function plugin(initializerContext: PluginInitializerContext) { /** @public static code */ export { Vis, VisParams, VisState } from './vis'; -export * from './filters'; export { TypesService } from './types/types_service'; export { Status } from './legacy/update_status'; @@ -53,6 +52,4 @@ export { buildPipeline, buildVislibDimensions, SchemaConfig } from './legacy/bui // @ts-ignore export { updateOldState } from './legacy/vis_update_state'; export { calculateObjectHash } from './legacy/calculate_object_hash'; -// @ts-ignore -export { createFiltersFromEvent } from './filters/vis_filters'; export { createSavedVisLoader } from '../../saved_visualizations/saved_visualizations'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js index f62b3a0b393aca..351acc48e26766 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js @@ -19,7 +19,6 @@ import _ from 'lodash'; -import { createFiltersFromEvent, onBrushEvent } from '../filters'; import { DefaultEditorController } from '../../../../../vis_default_editor/public'; export class BaseVisType { @@ -60,15 +59,6 @@ export class BaseVisType { showIndexSelection: true, hierarchicalData: false, // we should get rid of this i guess ? }, - events: { - filterBucket: { - defaultAction: createFiltersFromEvent, - }, - brush: { - defaultAction: onBrushEvent, - disabled: true, - }, - }, stage: 'production', feedbackMessage: '', hidden: false, diff --git a/src/legacy/server/http/integration_tests/default_route_provider.test.ts b/src/legacy/server/http/integration_tests/default_route_provider.test.ts index 4898cb2b67852e..d91438d904558b 100644 --- a/src/legacy/server/http/integration_tests/default_route_provider.test.ts +++ b/src/legacy/server/http/integration_tests/default_route_provider.test.ts @@ -29,7 +29,7 @@ let mockDefaultRouteSetting: any = ''; describe('default route provider', () => { let root: Root; beforeAll(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ migrations: { skip: true } }); await root.setup(); await root.start(); diff --git a/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts b/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts index da785a59893ab6..8365941cbeb10e 100644 --- a/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts +++ b/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts @@ -30,6 +30,7 @@ describe('default route provider', () => { server: { defaultRoute: '/app/some/default/route', }, + migrations: { skip: true }, }); await root.setup(); diff --git a/src/legacy/server/http/integration_tests/max_payload_size.test.js b/src/legacy/server/http/integration_tests/max_payload_size.test.js index 4408f0141bb21a..7f22f83c78f0ee 100644 --- a/src/legacy/server/http/integration_tests/max_payload_size.test.js +++ b/src/legacy/server/http/integration_tests/max_payload_size.test.js @@ -21,7 +21,7 @@ import * as kbnTestServer from '../../../../test_utils/kbn_server'; let root; beforeAll(async () => { - root = kbnTestServer.createRoot({ server: { maxPayloadBytes: 100 } }); + root = kbnTestServer.createRoot({ server: { maxPayloadBytes: 100 }, migrations: { skip: true } }); await root.setup(); await root.start(); diff --git a/src/legacy/ui/public/agg_types/agg_config.ts b/src/legacy/ui/public/agg_types/agg_config.ts index 3f88c540be1641..17a8b14b57d020 100644 --- a/src/legacy/ui/public/agg_types/agg_config.ts +++ b/src/legacy/ui/public/agg_types/agg_config.ts @@ -63,7 +63,7 @@ const unknownSchema: Schema = { const getTypeFromRegistry = (type: string): AggType => { // We need to inline require here, since we're having a cyclic dependency // from somewhere inside agg_types back to AggConfig. - const aggTypes = require('../agg_types').aggTypes; + const aggTypes = require('./agg_types').aggTypes; const registeredType = aggTypes.metrics.find((agg: AggType) => agg.name === type) || aggTypes.buckets.find((agg: AggType) => agg.name === type); diff --git a/src/legacy/ui/public/agg_types/agg_types.ts b/src/legacy/ui/public/agg_types/agg_types.ts new file mode 100644 index 00000000000000..1b05f5926ebfc9 --- /dev/null +++ b/src/legacy/ui/public/agg_types/agg_types.ts @@ -0,0 +1,92 @@ +/* + * 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 { countMetricAgg } from './metrics/count'; +import { avgMetricAgg } from './metrics/avg'; +import { sumMetricAgg } from './metrics/sum'; +import { medianMetricAgg } from './metrics/median'; +import { minMetricAgg } from './metrics/min'; +import { maxMetricAgg } from './metrics/max'; +import { topHitMetricAgg } from './metrics/top_hit'; +import { stdDeviationMetricAgg } from './metrics/std_deviation'; +import { cardinalityMetricAgg } from './metrics/cardinality'; +import { percentilesMetricAgg } from './metrics/percentiles'; +import { geoBoundsMetricAgg } from './metrics/geo_bounds'; +import { geoCentroidMetricAgg } from './metrics/geo_centroid'; +import { percentileRanksMetricAgg } from './metrics/percentile_ranks'; +import { derivativeMetricAgg } from './metrics/derivative'; +import { cumulativeSumMetricAgg } from './metrics/cumulative_sum'; +import { movingAvgMetricAgg } from './metrics/moving_avg'; +import { serialDiffMetricAgg } from './metrics/serial_diff'; +import { dateHistogramBucketAgg } from './buckets/date_histogram'; +import { histogramBucketAgg } from './buckets/histogram'; +import { rangeBucketAgg } from './buckets/range'; +import { dateRangeBucketAgg } from './buckets/date_range'; +import { ipRangeBucketAgg } from './buckets/ip_range'; +import { termsBucketAgg } from './buckets/terms'; +import { filterBucketAgg } from './buckets/filter'; +import { filtersBucketAgg } from './buckets/filters'; +import { significantTermsBucketAgg } from './buckets/significant_terms'; +import { geoHashBucketAgg } from './buckets/geo_hash'; +import { geoTileBucketAgg } from './buckets/geo_tile'; +import { bucketSumMetricAgg } from './metrics/bucket_sum'; +import { bucketAvgMetricAgg } from './metrics/bucket_avg'; +import { bucketMinMetricAgg } from './metrics/bucket_min'; +import { bucketMaxMetricAgg } from './metrics/bucket_max'; + +export { AggType } from './agg_type'; + +export const aggTypes = { + metrics: [ + countMetricAgg, + avgMetricAgg, + sumMetricAgg, + medianMetricAgg, + minMetricAgg, + maxMetricAgg, + stdDeviationMetricAgg, + cardinalityMetricAgg, + percentilesMetricAgg, + percentileRanksMetricAgg, + topHitMetricAgg, + derivativeMetricAgg, + cumulativeSumMetricAgg, + movingAvgMetricAgg, + serialDiffMetricAgg, + bucketAvgMetricAgg, + bucketSumMetricAgg, + bucketMinMetricAgg, + bucketMaxMetricAgg, + geoBoundsMetricAgg, + geoCentroidMetricAgg, + ], + buckets: [ + dateHistogramBucketAgg, + histogramBucketAgg, + rangeBucketAgg, + dateRangeBucketAgg, + ipRangeBucketAgg, + termsBucketAgg, + filterBucketAgg, + filtersBucketAgg, + significantTermsBucketAgg, + geoHashBucketAgg, + geoTileBucketAgg, + ], +}; diff --git a/src/legacy/ui/public/agg_types/index.ts b/src/legacy/ui/public/agg_types/index.ts index ca7c2f82023c94..cf2733b9a9f36a 100644 --- a/src/legacy/ui/public/agg_types/index.ts +++ b/src/legacy/ui/public/agg_types/index.ts @@ -17,80 +17,7 @@ * under the License. */ -import { countMetricAgg } from './metrics/count'; -import { avgMetricAgg } from './metrics/avg'; -import { sumMetricAgg } from './metrics/sum'; -import { medianMetricAgg } from './metrics/median'; -import { minMetricAgg } from './metrics/min'; -import { maxMetricAgg } from './metrics/max'; -import { topHitMetricAgg } from './metrics/top_hit'; -import { stdDeviationMetricAgg } from './metrics/std_deviation'; -import { cardinalityMetricAgg } from './metrics/cardinality'; -import { percentilesMetricAgg } from './metrics/percentiles'; -import { geoBoundsMetricAgg } from './metrics/geo_bounds'; -import { geoCentroidMetricAgg } from './metrics/geo_centroid'; -import { percentileRanksMetricAgg } from './metrics/percentile_ranks'; -import { derivativeMetricAgg } from './metrics/derivative'; -import { cumulativeSumMetricAgg } from './metrics/cumulative_sum'; -import { movingAvgMetricAgg } from './metrics/moving_avg'; -import { serialDiffMetricAgg } from './metrics/serial_diff'; -import { dateHistogramBucketAgg, setBounds } from './buckets/date_histogram'; -import { histogramBucketAgg } from './buckets/histogram'; -import { rangeBucketAgg } from './buckets/range'; -import { dateRangeBucketAgg } from './buckets/date_range'; -import { ipRangeBucketAgg } from './buckets/ip_range'; -import { termsBucketAgg, termsAggFilter } from './buckets/terms'; -import { filterBucketAgg } from './buckets/filter'; -import { filtersBucketAgg } from './buckets/filters'; -import { significantTermsBucketAgg } from './buckets/significant_terms'; -import { geoHashBucketAgg } from './buckets/geo_hash'; -import { geoTileBucketAgg } from './buckets/geo_tile'; -import { bucketSumMetricAgg } from './metrics/bucket_sum'; -import { bucketAvgMetricAgg } from './metrics/bucket_avg'; -import { bucketMinMetricAgg } from './metrics/bucket_min'; -import { bucketMaxMetricAgg } from './metrics/bucket_max'; - -export { AggType } from './agg_type'; - -export const aggTypes = { - metrics: [ - countMetricAgg, - avgMetricAgg, - sumMetricAgg, - medianMetricAgg, - minMetricAgg, - maxMetricAgg, - stdDeviationMetricAgg, - cardinalityMetricAgg, - percentilesMetricAgg, - percentileRanksMetricAgg, - topHitMetricAgg, - derivativeMetricAgg, - cumulativeSumMetricAgg, - movingAvgMetricAgg, - serialDiffMetricAgg, - bucketAvgMetricAgg, - bucketSumMetricAgg, - bucketMinMetricAgg, - bucketMaxMetricAgg, - geoBoundsMetricAgg, - geoCentroidMetricAgg, - ], - buckets: [ - dateHistogramBucketAgg, - histogramBucketAgg, - rangeBucketAgg, - dateRangeBucketAgg, - ipRangeBucketAgg, - termsBucketAgg, - filterBucketAgg, - filtersBucketAgg, - significantTermsBucketAgg, - geoHashBucketAgg, - geoTileBucketAgg, - ], -}; - +export { aggTypes } from './agg_types'; export { AggParam } from './agg_params'; export { AggConfig } from './agg_config'; export { AggConfigs } from './agg_configs'; @@ -99,5 +26,6 @@ export { FieldParamType } from './param_types'; export { BUCKET_TYPES } from './buckets/bucket_agg_types'; export { METRIC_TYPES } from './metrics/metric_agg_types'; export { ISchemas, Schema, Schemas } from './schemas'; - -export { setBounds, termsAggFilter }; +export { AggType } from './agg_type'; +export { setBounds } from './buckets/date_histogram'; +export { termsAggFilter } from './buckets/terms'; diff --git a/src/legacy/ui/public/agg_types/param_types/field.ts b/src/legacy/ui/public/agg_types/param_types/field.ts index 4ce5bb29f8ff60..d01e059c6c616b 100644 --- a/src/legacy/ui/public/agg_types/param_types/field.ts +++ b/src/legacy/ui/public/agg_types/param_types/field.ts @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { AggConfig } from '../agg_config'; import { SavedObjectNotFound } from '../../../../../plugins/kibana_utils/public'; import { BaseParamType } from './base'; -import { toastNotifications } from '../../notify'; +import { npStart } from '../../new_platform'; import { propFilter } from '../filter'; import { Field, IFieldList } from '../../../../../plugins/data/public'; import { isNestedField } from '../../../../../plugins/data/public'; @@ -89,7 +89,7 @@ export class FieldParamType extends BaseParamType { (f: any) => f.name === fieldName ); if (!validField) { - toastNotifications.addDanger( + npStart.core.notifications.toasts.addDanger( i18n.translate( 'common.ui.aggTypes.paramTypes.field.invalidSavedFieldParameterErrorMessage', { diff --git a/src/legacy/ui/public/agg_types/utils.ts b/src/legacy/ui/public/agg_types/utils.ts index fd405d49625eda..e382f821b31a90 100644 --- a/src/legacy/ui/public/agg_types/utils.ts +++ b/src/legacy/ui/public/agg_types/utils.ts @@ -17,7 +17,7 @@ * under the License. */ -import { isValidEsInterval } from '../../../core_plugins/data/public'; +import { isValidEsInterval } from '../../../core_plugins/data/common/parse_es_interval/is_valid_es_interval'; import { leastCommonInterval } from '../vis/lib/least_common_interval'; /** diff --git a/src/legacy/ui/public/vis/lib/least_common_interval.ts b/src/legacy/ui/public/vis/lib/least_common_interval.ts index 244bc1d0111e3b..72426855f70af5 100644 --- a/src/legacy/ui/public/vis/lib/least_common_interval.ts +++ b/src/legacy/ui/public/vis/lib/least_common_interval.ts @@ -19,7 +19,7 @@ import dateMath from '@elastic/datemath'; import { leastCommonMultiple } from './least_common_multiple'; -import { parseEsInterval } from '../../../../core_plugins/data/public'; +import { parseEsInterval } from '../../../../core_plugins/data/common/parse_es_interval/parse_es_interval'; /** * Finds the lowest common interval between two given ES date histogram intervals diff --git a/src/legacy/utils/index.d.ts b/src/legacy/utils/index.d.ts index a57caad1d34bf8..c294c79542bbe9 100644 --- a/src/legacy/utils/index.d.ts +++ b/src/legacy/utils/index.d.ts @@ -18,3 +18,16 @@ */ export function unset(object: object, rawPath: string): void; + +export { + concatStreamProviders, + createConcatStream, + createFilterStream, + createIntersperseStream, + createListStream, + createMapStream, + createPromiseFromStreams, + createReduceStream, + createReplaceStream, + createSplitStream, +} from './streams'; diff --git a/src/legacy/utils/streams/index.d.ts b/src/legacy/utils/streams/index.d.ts index b8d4c67050b2da..5ef39b292c6858 100644 --- a/src/legacy/utils/streams/index.d.ts +++ b/src/legacy/utils/streams/index.d.ts @@ -20,17 +20,17 @@ import { Readable, Transform, Writable, TransformOptions } from 'stream'; export function concatStreamProviders( - sourceProviders: Readable[], + sourceProviders: Array<() => Readable>, options: TransformOptions ): Transform; export function createIntersperseStream(intersperseChunk: string | Buffer): Transform; export function createSplitStream(splitChunk: T): Transform; -export function createListStream(items: any[]): Readable; +export function createListStream(items: any | any[]): Readable; export function createReduceStream(reducer: (value: any, chunk: T, enc: string) => T): Transform; export function createPromiseFromStreams([first, ...rest]: [Readable, ...Writable[]]): Promise< T >; -export function createConcatStream(initial: any): Transform; +export function createConcatStream(initial?: any): Transform; export function createMapStream(fn: (value: T, i: number) => void): Transform; export function createReplaceStream(toReplace: string, replacement: string | Buffer): Transform; export function createFilterStream(fn: (obj: T) => boolean): Transform; diff --git a/src/plugins/console/server/lib/elasticsearch_proxy_config.ts b/src/plugins/console/server/lib/elasticsearch_proxy_config.ts index 901d726ac51d82..28a971794d403d 100644 --- a/src/plugins/console/server/lib/elasticsearch_proxy_config.ts +++ b/src/plugins/console/server/lib/elasticsearch_proxy_config.ts @@ -21,9 +21,10 @@ import _ from 'lodash'; import http from 'http'; import https from 'https'; import url from 'url'; -import { Duration } from 'moment'; -const createAgent = (legacyConfig: any) => { +import { ESConfigForProxy } from '../types'; + +const createAgent = (legacyConfig: ESConfigForProxy) => { const target = url.parse(_.head(legacyConfig.hosts)); if (!/^https/.test(target.protocol || '')) return new http.Agent(); @@ -59,7 +60,7 @@ const createAgent = (legacyConfig: any) => { return new https.Agent(agentOptions); }; -export const getElasticsearchProxyConfig = (legacyConfig: { requestTimeout: Duration }) => { +export const getElasticsearchProxyConfig = (legacyConfig: ESConfigForProxy) => { return { timeout: legacyConfig.requestTimeout.asMilliseconds(), agent: createAgent(legacyConfig), diff --git a/src/plugins/console/server/plugin.ts b/src/plugins/console/server/plugin.ts index c8ef84aee3b615..65647bd5acb7c5 100644 --- a/src/plugins/console/server/plugin.ts +++ b/src/plugins/console/server/plugin.ts @@ -60,9 +60,7 @@ export class ConsoleServerPlugin implements Plugin { const legacyConfig = readLegacyEsConfig(); return { ...elasticsearch, - hosts: legacyConfig.hosts, - requestHeadersWhitelist: legacyConfig.requestHeadersWhitelist, - customHeaders: legacyConfig.customHeaders, + ...legacyConfig, }; }, pathFilters: proxyPathFilters, diff --git a/src/plugins/console/server/types.ts b/src/plugins/console/server/types.ts index 60ce56ad39fcd4..adafcd4d305269 100644 --- a/src/plugins/console/server/types.ts +++ b/src/plugins/console/server/types.ts @@ -31,4 +31,12 @@ export interface ESConfigForProxy { requestHeadersWhitelist: string[]; customHeaders: Record; requestTimeout: Duration; + ssl?: { + verificationMode: 'none' | 'certificate' | 'full'; + certificateAuthorities: string[] | string; + alwaysPresentCertificate: boolean; + certificate?: string; + key?: string; + keyPassphrase?: string; + }; } diff --git a/src/plugins/embeddable/public/bootstrap.ts b/src/plugins/embeddable/public/bootstrap.ts index 801329a4a79af9..1e0e7dfdb09331 100644 --- a/src/plugins/embeddable/public/bootstrap.ts +++ b/src/plugins/embeddable/public/bootstrap.ts @@ -23,6 +23,8 @@ import { APPLY_FILTER_TRIGGER, createFilterAction, PANEL_BADGE_TRIGGER, + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, } from './lib'; /** @@ -50,11 +52,25 @@ export const bootstrap = (uiActions: IUiActionsSetup) => { description: 'Actions appear in title bar when an embeddable loads in a panel', actionIds: [], }; + const selectRangeTrigger = { + id: SELECT_RANGE_TRIGGER, + title: 'Select range', + description: 'Applies a range filter', + actionIds: [], + }; + const valueClickTrigger = { + id: VALUE_CLICK_TRIGGER, + title: 'Value clicked', + description: 'Value was clicked', + actionIds: [], + }; const actionApplyFilter = createFilterAction(); uiActions.registerTrigger(triggerContext); uiActions.registerTrigger(triggerFilter); uiActions.registerAction(actionApplyFilter); uiActions.registerTrigger(triggerBadge); + uiActions.registerTrigger(selectRangeTrigger); + uiActions.registerTrigger(valueClickTrigger); // uiActions.attachAction(triggerFilter.id, actionApplyFilter.id); }; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 583b21ddfa4338..ec71a1e724c7d8 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -25,6 +25,8 @@ export { APPLY_FILTER_ACTION, APPLY_FILTER_TRIGGER, PANEL_BADGE_TRIGGER, + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, Adapters, AddPanelAction, CONTEXT_MENU_TRIGGER, diff --git a/src/plugins/embeddable/public/lib/triggers/index.ts b/src/plugins/embeddable/public/lib/triggers/index.ts index ffa7f6d0c0f448..72565b3f527adc 100644 --- a/src/plugins/embeddable/public/lib/triggers/index.ts +++ b/src/plugins/embeddable/public/lib/triggers/index.ts @@ -19,4 +19,6 @@ export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; export const APPLY_FILTER_TRIGGER = 'FILTER_TRIGGER'; +export const SELECT_RANGE_TRIGGER = 'SELECT_RANGE_TRIGGER'; +export const VALUE_CLICK_TRIGGER = 'VALUE_CLICK_TRIGGER'; export const PANEL_BADGE_TRIGGER = 'PANEL_BADGE_TRIGGER'; diff --git a/src/plugins/expressions/common/expression_types/kibana_datatable.ts b/src/plugins/expressions/common/expression_types/kibana_datatable.ts index c360a2be8c7f7b..38227d2ed6207b 100644 --- a/src/plugins/expressions/common/expression_types/kibana_datatable.ts +++ b/src/plugins/expressions/common/expression_types/kibana_datatable.ts @@ -23,9 +23,16 @@ import { Datatable, PointSeries } from '.'; const name = 'kibana_datatable'; +export interface KibanaDatatableColumnMeta { + type: string; + indexPatternId?: string; + aggConfigParams?: Record; +} + export interface KibanaDatatableColumn { id: string; name: string; + meta?: KibanaDatatableColumnMeta; formatHint?: SerializedFieldFormat; } diff --git a/test/interpreter_functional/screenshots/baseline/combined_test.png b/test/interpreter_functional/screenshots/baseline/combined_test.png index 87f3173d560247..56c055b8b1cff8 100644 Binary files a/test/interpreter_functional/screenshots/baseline/combined_test.png and b/test/interpreter_functional/screenshots/baseline/combined_test.png differ diff --git a/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png b/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png index d860bb73521cea..753ab2c2c6e949 100644 Binary files a/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png and b/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_all_data.png b/test/interpreter_functional/screenshots/baseline/metric_all_data.png index 43943b1e6c46ce..44226877bdc5af 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_all_data.png and b/test/interpreter_functional/screenshots/baseline/metric_all_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png b/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png index 795f2f7c832f33..e0cffd065fc4ad 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png and b/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png b/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png index 580889bb7deaf3..14457f0a4d0ab7 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png and b/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png b/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png index 916a284433874a..c4fc4d3979152a 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png and b/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_1.png b/test/interpreter_functional/screenshots/baseline/partial_test_1.png index 9815f25d00b165..51998d019c66c0 100644 Binary files a/test/interpreter_functional/screenshots/baseline/partial_test_1.png and b/test/interpreter_functional/screenshots/baseline/partial_test_1.png differ diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_2.png b/test/interpreter_functional/screenshots/baseline/partial_test_2.png index 87f3173d560247..56c055b8b1cff8 100644 Binary files a/test/interpreter_functional/screenshots/baseline/partial_test_2.png and b/test/interpreter_functional/screenshots/baseline/partial_test_2.png differ diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_3.png b/test/interpreter_functional/screenshots/baseline/partial_test_3.png index ee9182a654d1e8..7b96f3ec43c7ed 100644 Binary files a/test/interpreter_functional/screenshots/baseline/partial_test_3.png and b/test/interpreter_functional/screenshots/baseline/partial_test_3.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png index 03ffc7ac7b1a58..a7088de3849a54 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png b/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png index 3a7315df405dfe..8f93ba81ad2adc 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png index 795f2f7c832f33..e0cffd065fc4ad 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png index 5cdce692966736..98890b9687ac95 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_options.png b/test/interpreter_functional/screenshots/baseline/tagcloud_options.png index 394b5585097a24..479280f598aef7 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_options.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_options.png differ diff --git a/test/interpreter_functional/snapshots/baseline/combined_test2.json b/test/interpreter_functional/snapshots/baseline/combined_test2.json index 98c7844e41f19b..84203617ff8535 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test2.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/combined_test3.json b/test/interpreter_functional/snapshots/baseline/combined_test3.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test3.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/final_output_test.json b/test/interpreter_functional/snapshots/baseline/final_output_test.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/baseline/final_output_test.json +++ b/test/interpreter_functional/snapshots/baseline/final_output_test.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_all_data.json b/test/interpreter_functional/snapshots/baseline/metric_all_data.json index 26f111e9edcf92..9b0122c1574812 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json index 7f64f978451915..2d6e756a7f0a3a 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json index e171a65be8bab5..37c6885d76cb08 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json index ed8b0b258fd90f..60a0e450906a27 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json index 8a349aa5df0600..6b2f93b47c0b28 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_1.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_2.json b/test/interpreter_functional/snapshots/baseline/partial_test_2.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_2.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_2.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_3.json b/test/interpreter_functional/snapshots/baseline/partial_test_3.json index c1e429508c37ff..4241d6f208bfd4 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_3.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test2.json b/test/interpreter_functional/snapshots/baseline/step_output_test2.json index 98c7844e41f19b..84203617ff8535 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test2.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test3.json b/test/interpreter_functional/snapshots/baseline/step_output_test3.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test3.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json index 1325c7fbed03ea..ae1e817424cb10 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json index 2b063b518665af..c0da4794728807 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json index 6152fd406961fc..c5fbcd63b06854 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json index e4c6b09a264ddb..b67b0744494034 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test2.json b/test/interpreter_functional/snapshots/session/combined_test2.json index 98c7844e41f19b..84203617ff8535 100644 --- a/test/interpreter_functional/snapshots/session/combined_test2.json +++ b/test/interpreter_functional/snapshots/session/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test3.json b/test/interpreter_functional/snapshots/session/combined_test3.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/session/combined_test3.json +++ b/test/interpreter_functional/snapshots/session/combined_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/final_output_test.json b/test/interpreter_functional/snapshots/session/final_output_test.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/session/final_output_test.json +++ b/test/interpreter_functional/snapshots/session/final_output_test.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_all_data.json b/test/interpreter_functional/snapshots/session/metric_all_data.json index 26f111e9edcf92..9b0122c1574812 100644 --- a/test/interpreter_functional/snapshots/session/metric_all_data.json +++ b/test/interpreter_functional/snapshots/session/metric_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json index 7f64f978451915..2d6e756a7f0a3a 100644 --- a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json index e171a65be8bab5..37c6885d76cb08 100644 --- a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json index ed8b0b258fd90f..60a0e450906a27 100644 --- a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_1.json b/test/interpreter_functional/snapshots/session/partial_test_1.json index 8a349aa5df0600..6b2f93b47c0b28 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_1.json +++ b/test/interpreter_functional/snapshots/session/partial_test_1.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_2.json b/test/interpreter_functional/snapshots/session/partial_test_2.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_2.json +++ b/test/interpreter_functional/snapshots/session/partial_test_2.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_3.json b/test/interpreter_functional/snapshots/session/partial_test_3.json index c1e429508c37ff..4241d6f208bfd4 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_3.json +++ b/test/interpreter_functional/snapshots/session/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test2.json b/test/interpreter_functional/snapshots/session/step_output_test2.json index 98c7844e41f19b..84203617ff8535 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test2.json +++ b/test/interpreter_functional/snapshots/session/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test3.json b/test/interpreter_functional/snapshots/session/step_output_test3.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test3.json +++ b/test/interpreter_functional/snapshots/session/step_output_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json index 1325c7fbed03ea..ae1e817424cb10 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json index 2b063b518665af..c0da4794728807 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json index 6152fd406961fc..c5fbcd63b06854 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_options.json b/test/interpreter_functional/snapshots/session/tagcloud_options.json index e4c6b09a264ddb..b67b0744494034 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_options.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index a6ba936b76570e..1346d403edda1a 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -29,7 +29,7 @@ import { CreateAPIKeyResult as SecurityPluginCreateAPIKeyResult, InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, } from '../../../../plugins/security/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../plugins/encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../../../plugins/task_manager/server'; type NormalizedAlertAction = Omit; @@ -45,7 +45,7 @@ interface ConstructorOptions { taskManager: TaskManagerStartContract; savedObjectsClient: SavedObjectsClientContract; alertTypeRegistry: AlertTypeRegistry; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceId?: string; namespace?: string; getUserName: () => Promise; @@ -120,7 +120,7 @@ export class AlertsClient { private readonly invalidateAPIKey: ( params: InvalidateAPIKeyParams ) => Promise; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; constructor({ alertTypeRegistry, diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts b/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts index eab1cc3ce627b4..de789fba0ac388 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts @@ -11,7 +11,7 @@ import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; import { SecurityPluginStartContract } from './shim'; import { KibanaRequest, Logger } from '../../../../../src/core/server'; import { InvalidateAPIKeyParams } from '../../../../plugins/security/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../plugins/encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../../../plugins/task_manager/server'; export interface ConstructorOpts { @@ -21,7 +21,7 @@ export interface ConstructorOpts { securityPluginSetup?: SecurityPluginStartContract; getSpaceId: (request: Hapi.Request) => string | undefined; spaceIdToNamespace: SpaceIdToNamespaceFunction; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; } export class AlertsClientFactory { @@ -31,7 +31,7 @@ export class AlertsClientFactory { private readonly securityPluginSetup?: SecurityPluginStartContract; private readonly getSpaceId: (request: Hapi.Request) => string | undefined; private readonly spaceIdToNamespace: SpaceIdToNamespaceFunction; - private readonly encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + private readonly encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; constructor(options: ConstructorOpts) { this.logger = options.logger; diff --git a/x-pack/legacy/plugins/alerting/server/shim.ts b/x-pack/legacy/plugins/alerting/server/shim.ts index 80d01ea722926f..bc8b0eb863634c 100644 --- a/x-pack/legacy/plugins/alerting/server/shim.ts +++ b/x-pack/legacy/plugins/alerting/server/shim.ts @@ -15,10 +15,10 @@ import { getTaskManagerSetup, getTaskManagerStart } from '../../task_manager/ser import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; import KbnServer from '../../../../../src/legacy/server/kbn_server'; import { - PluginSetupContract as EncryptedSavedObjectsSetupContract, - PluginStartContract as EncryptedSavedObjectsStartContract, + EncryptedSavedObjectsPluginSetup, + EncryptedSavedObjectsPluginStart, } from '../../../../plugins/encrypted_saved_objects/server'; -import { PluginSetupContract as SecurityPlugin } from '../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { CoreSetup, LoggerFactory, @@ -44,8 +44,8 @@ export interface Server extends Legacy.Server { /** * Shim what we're thinking setup and start contracts will look like */ -export type SecurityPluginSetupContract = Pick; -export type SecurityPluginStartContract = Pick; +export type SecurityPluginSetupContract = Pick; +export type SecurityPluginStartContract = Pick; export type XPackMainPluginSetupContract = Pick; /** @@ -71,14 +71,14 @@ export interface AlertingPluginsSetup { taskManager: TaskManagerSetupContract; actions: ActionsPluginSetupContract; xpack_main: XPackMainPluginSetupContract; - encryptedSavedObjects: EncryptedSavedObjectsSetupContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; licensing: LicensingPluginSetup; } export interface AlertingPluginsStart { actions: ActionsPluginStartContract; security?: SecurityPluginStartContract; spaces: () => SpacesPluginStartContract | undefined; - encryptedSavedObjects: EncryptedSavedObjectsStartContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; taskManager: TaskManagerStartContract; } @@ -120,7 +120,7 @@ export function shim( actions: newPlatform.setup.plugins.actions as ActionsPluginSetupContract, xpack_main: server.plugins.xpack_main, encryptedSavedObjects: newPlatform.setup.plugins - .encryptedSavedObjects as EncryptedSavedObjectsSetupContract, + .encryptedSavedObjects as EncryptedSavedObjectsPluginSetup, licensing: newPlatform.setup.plugins.licensing as LicensingPluginSetup, }; @@ -131,7 +131,7 @@ export function shim( // initializes after this function is called spaces: () => server.plugins.spaces, encryptedSavedObjects: newPlatform.start.plugins - .encryptedSavedObjects as EncryptedSavedObjectsStartContract, + .encryptedSavedObjects as EncryptedSavedObjectsPluginStart, taskManager: getTaskManagerStart(server)!, }; diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts index 67fef33b69c6dd..d2ecfb64c8a813 100644 --- a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts +++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts @@ -5,7 +5,7 @@ */ import { Logger } from '../../../../../../src/core/server'; import { RunContext } from '../../../../../plugins/task_manager/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../../plugins/encrypted_saved_objects/server'; import { PluginStartContract as ActionsPluginStartContract } from '../../../../../plugins/actions/server'; import { AlertType, @@ -19,7 +19,7 @@ export interface TaskRunnerContext { logger: Logger; getServices: GetServicesFunction; executeAction: ActionsPluginStartContract['execute']; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceIdToNamespace: SpaceIdToNamespaceFunction; getBasePath: GetBasePathFunction; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx new file mode 100644 index 00000000000000..378ad9509c2170 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -0,0 +1,66 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiTitle +} from '@elastic/eui'; +import cytoscape from 'cytoscape'; +import React from 'react'; +import { Buttons } from './Buttons'; +import { Info } from './Info'; +import { ServiceMetricList } from './ServiceMetricList'; + +const popoverMinWidth = 280; + +interface ContentsProps { + focusedServiceName?: string; + isService: boolean; + label: string; + onFocusClick: () => void; + selectedNodeData: cytoscape.NodeDataDefinition; + selectedNodeServiceName: string; +} + +export function Contents({ + selectedNodeData, + focusedServiceName, + isService, + label, + onFocusClick, + selectedNodeServiceName +}: ContentsProps) { + return ( + + + +

{label}

+
+ +
+ + {isService ? ( + + ) : ( + + )} + + {isService && ( + + )} +
+ ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx index 1c5443e404f9b3..d432119505382a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; +import cytoscape from 'cytoscape'; +import React from 'react'; import styled from 'styled-components'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; const ItemRow = styled.div` line-height: 2; @@ -19,8 +20,8 @@ const ItemTitle = styled.dt` const ItemDescription = styled.dd``; -interface InfoProps { - type: string; +interface InfoProps extends cytoscape.NodeDataDefinition { + type?: string; subtype?: string; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx new file mode 100644 index 00000000000000..b26488c5ef7de9 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -0,0 +1,49 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import React from 'react'; +import { + ApmPluginContext, + ApmPluginContextValue +} from '../../../../context/ApmPluginContext'; +import { Contents } from './Contents'; + +const selectedNodeData = { + id: 'opbeans-node', + label: 'opbeans-node', + href: + '#/services/opbeans-node/service-map?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0', + agentName: 'nodejs', + type: 'service' +}; + +storiesOf('app/ServiceMap/Popover/Contents', module).add( + 'example', + () => { + return ( + + {}} + selectedNodeServiceName="opbeans-node" + /> + + ); + }, + { + info: { + propTablesExclude: [ApmPluginContext.Provider], + source: false + } + } +); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx index dfb78aaa0214c4..e8e37cfdfb1f0f 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx @@ -4,27 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiPopover, - EuiTitle -} from '@elastic/eui'; +import { EuiPopover } from '@elastic/eui'; import cytoscape from 'cytoscape'; import React, { CSSProperties, + useCallback, useContext, useEffect, - useState, - useCallback + useRef, + useState } from 'react'; import { CytoscapeContext } from '../Cytoscape'; -import { Buttons } from './Buttons'; -import { Info } from './Info'; -import { ServiceMetricList } from './ServiceMetricList'; - -const popoverMinWidth = 280; +import { Contents } from './Contents'; interface PopoverProps { focusedServiceName?: string; @@ -35,56 +26,62 @@ export function Popover({ focusedServiceName }: PopoverProps) { const [selectedNode, setSelectedNode] = useState< cytoscape.NodeSingular | undefined >(undefined); - const onFocusClick = useCallback(() => setSelectedNode(undefined), [ + const deselect = useCallback(() => setSelectedNode(undefined), [ setSelectedNode ]); - - useEffect(() => { - const selectHandler: cytoscape.EventHandler = event => { - setSelectedNode(event.target); - }; - const unselectHandler: cytoscape.EventHandler = () => { - setSelectedNode(undefined); - }; - - if (cy) { - cy.on('select', 'node', selectHandler); - cy.on('unselect', 'node', unselectHandler); - cy.on('data viewport', unselectHandler); - } - - return () => { - if (cy) { - cy.removeListener('select', 'node', selectHandler); - cy.removeListener('unselect', 'node', unselectHandler); - cy.removeListener('data viewport', undefined, unselectHandler); - } - }; - }, [cy]); - const renderedHeight = selectedNode?.renderedHeight() ?? 0; const renderedWidth = selectedNode?.renderedWidth() ?? 0; const { x, y } = selectedNode?.renderedPosition() ?? { x: 0, y: 0 }; const isOpen = !!selectedNode; - const selectedNodeServiceName: string = selectedNode?.data('id'); const isService = selectedNode?.data('type') === 'service'; const triggerStyle: CSSProperties = { background: 'transparent', height: renderedHeight, position: 'absolute', - width: renderedWidth + width: renderedWidth, + border: '3px dotted red' }; - const trigger =
; - + const trigger =
; const zoom = cy?.zoom() ?? 1; const height = selectedNode?.height() ?? 0; - const translateY = y - (zoom + 1) * (height / 2); + const translateY = y - ((zoom + 1) * height) / 4; const popoverStyle: CSSProperties = { position: 'absolute', transform: `translate(${x}px, ${translateY}px)` }; - const data = selectedNode?.data() ?? {}; - const label = data.label || selectedNodeServiceName; + const selectedNodeData = selectedNode?.data() ?? {}; + const selectedNodeServiceName = selectedNodeData.id; + const label = selectedNodeData.label || selectedNodeServiceName; + const popoverRef = useRef(null); + + // Set up Cytoscape event handlers + useEffect(() => { + const selectHandler: cytoscape.EventHandler = event => { + setSelectedNode(event.target); + }; + + if (cy) { + cy.on('select', 'node', selectHandler); + cy.on('unselect', 'node', deselect); + cy.on('data viewport', deselect); + } + + return () => { + if (cy) { + cy.removeListener('select', 'node', selectHandler); + cy.removeListener('unselect', 'node', deselect); + cy.removeListener('data viewport', undefined, deselect); + } + }; + }, [cy, deselect]); + + // Handle positioning of popover. This makes it so the popover positions + // itself correctly and the arrows are always pointing to where they should. + useEffect(() => { + if (popoverRef.current) { + popoverRef.current.positionPopoverFluid(); + } + }, [popoverRef, x, y]); return ( {}} isOpen={isOpen} + ref={popoverRef} style={popoverStyle} > - - - -

{label}

-
- -
- - - {isService ? ( - - ) : ( - - )} - - {isService && ( - - )} -
+
); } diff --git a/x-pack/legacy/plugins/canvas/server/plugin.ts b/x-pack/legacy/plugins/canvas/server/plugin.ts index ac3edbabce930e..1f17e85bfd294f 100644 --- a/x-pack/legacy/plugins/canvas/server/plugin.ts +++ b/x-pack/legacy/plugins/canvas/server/plugin.ts @@ -5,14 +5,11 @@ */ import { CoreSetup, PluginsSetup } from './shim'; -import { routes } from './routes'; import { functions } from '../canvas_plugin_src/functions/server'; import { loadSampleData } from './sample_data'; export class Plugin { public setup(core: CoreSetup, plugins: PluginsSetup) { - routes(core); - plugins.interpreter.register({ serverFunctions: functions }); core.injectUiAppVars('canvas', async () => { diff --git a/x-pack/legacy/plugins/canvas/server/routes/index.ts b/x-pack/legacy/plugins/canvas/server/routes/index.ts deleted file mode 100644 index 6898a3c459e3d4..00000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { shareableWorkpads } from './shareables'; -import { CoreSetup } from '../shim'; - -export function routes(setup: CoreSetup): void { - shareableWorkpads(setup.http.route); -} diff --git a/x-pack/legacy/plugins/canvas/server/routes/shareables.ts b/x-pack/legacy/plugins/canvas/server/routes/shareables.ts deleted file mode 100644 index e8186ceceb47f2..00000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/shareables.ts +++ /dev/null @@ -1,72 +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 archiver from 'archiver'; - -import { - API_ROUTE_SHAREABLE_RUNTIME, - API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD, - API_ROUTE_SHAREABLE_ZIP, -} from '../../common/lib/constants'; - -import { - SHAREABLE_RUNTIME_FILE, - SHAREABLE_RUNTIME_NAME, - SHAREABLE_RUNTIME_SRC, -} from '../../shareable_runtime/constants'; - -import { CoreSetup } from '../shim'; - -export function shareableWorkpads(route: CoreSetup['http']['route']) { - // get runtime - route({ - method: 'GET', - path: API_ROUTE_SHAREABLE_RUNTIME, - - handler: { - file: { - path: SHAREABLE_RUNTIME_FILE, - // The option setting is not for typical use. We're using it here to avoid - // problems in Cloud environments. See elastic/kibana#47405. - confine: false, - }, - }, - }); - - // download runtime - route({ - method: 'GET', - path: API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD, - - handler(_request, handler) { - // The option setting is not for typical use. We're using it here to avoid - // problems in Cloud environments. See elastic/kibana#47405. - // @ts-ignore No type for inert Hapi handler - const file = handler.file(SHAREABLE_RUNTIME_FILE, { confine: false }); - file.type('application/octet-stream'); - return file; - }, - }); - - route({ - method: 'POST', - path: API_ROUTE_SHAREABLE_ZIP, - handler(request, handler) { - const workpad = request.payload; - - const archive = archiver('zip'); - archive.append(JSON.stringify(workpad), { name: 'workpad.json' }); - archive.file(`${SHAREABLE_RUNTIME_SRC}/template.html`, { name: 'index.html' }); - archive.file(SHAREABLE_RUNTIME_FILE, { name: `${SHAREABLE_RUNTIME_NAME}.js` }); - - const response = handler.response(archive); - response.header('content-type', 'application/zip'); - archive.finalize(); - - return response; - }, - }); -} diff --git a/x-pack/legacy/plugins/encrypted_saved_objects/index.ts b/x-pack/legacy/plugins/encrypted_saved_objects/index.ts index 85aa5c22135b6c..ce343dba006cfc 100644 --- a/x-pack/legacy/plugins/encrypted_saved_objects/index.ts +++ b/x-pack/legacy/plugins/encrypted_saved_objects/index.ts @@ -6,7 +6,7 @@ import { Root } from 'joi'; import { Legacy } from 'kibana'; -import { PluginSetupContract } from '../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginSetup } from '../../../plugins/encrypted_saved_objects/server'; // @ts-ignore import { AuditLogger } from '../../server/lib/audit_logger'; @@ -21,13 +21,15 @@ export const encryptedSavedObjects = (kibana: { // Some legacy plugins still use `enabled` config key, so we keep it here, but the rest of the // keys is handled by the New Platform plugin. config: (Joi: Root) => - Joi.object({ enabled: Joi.boolean().default(true) }) + Joi.object({ + enabled: Joi.boolean().default(true), + }) .unknown(true) .default(), init(server: Legacy.Server) { const encryptedSavedObjectsPlugin = (server.newPlatform.setup.plugins - .encryptedSavedObjects as unknown) as PluginSetupContract; + .encryptedSavedObjects as unknown) as EncryptedSavedObjectsPluginSetup; if (!encryptedSavedObjectsPlugin) { throw new Error('New Platform XPack EncryptedSavedObjects plugin is not available.'); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/_index.scss b/x-pack/legacy/plugins/maps/public/layers/styles/_index.scss index 6d332ee878d95a..b5d9113619c769 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/_index.scss +++ b/x-pack/legacy/plugins/maps/public/layers/styles/_index.scss @@ -1,4 +1,4 @@ @import './components/color_gradient'; -@import './vector/components/static_dynamic_style_row'; +@import './vector/components/style_prop_editor'; @import './vector/components/color/color_stops'; @import './vector/components/symbol/icon_select'; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/_static_dynamic_style_row.scss b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/_static_dynamic_style_row.scss deleted file mode 100644 index 8ec006d32a8b9b..00000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/_static_dynamic_style_row.scss +++ /dev/null @@ -1,3 +0,0 @@ -.mapStaticDynamicSylingOption__dynamicSizeHack { - width: calc(100% - #{$euiSizeXXL + $euiSizeS}); -} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/_style_prop_editor.scss b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/_style_prop_editor.scss new file mode 100644 index 00000000000000..138605b1a7dcc3 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/_style_prop_editor.scss @@ -0,0 +1,3 @@ +.mapStyleFormDisabledTooltip { + width: 100%; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js index c5fdda70c4eb48..35e6fa60b28e75 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js @@ -8,6 +8,13 @@ import { i18n } from '@kbn/i18n'; import { VECTOR_STYLES } from '../vector_style_defaults'; +export function getDisabledByMessage(styleName) { + return i18n.translate('xpack.maps.styles.vector.disabledByMessage', { + defaultMessage: `Set '{styleLabel}' to enable`, + values: { styleLabel: getVectorStyleLabel(styleName) }, + }); +} + export function getVectorStyleLabel(styleName) { switch (styleName) { case VECTOR_STYLES.FILL_COLOR: diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_border_size_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_border_size_editor.js index 7d06e8b530011b..04bb800eb1ecf9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_border_size_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_border_size_editor.js @@ -6,9 +6,9 @@ import React from 'react'; -import { EuiFormRow, EuiSelect } from '@elastic/eui'; +import { EuiFormRow, EuiSelect, EuiToolTip } from '@elastic/eui'; import { LABEL_BORDER_SIZES, VECTOR_STYLES } from '../../vector_style_defaults'; -import { getVectorStyleLabel } from '../get_vector_style_label'; +import { getVectorStyleLabel, getDisabledByMessage } from '../get_vector_style_label'; import { i18n } from '@kbn/i18n'; const options = [ @@ -38,7 +38,12 @@ const options = [ }, ]; -export function VectorStyleLabelBorderSizeEditor({ handlePropertyChange, styleProperty }) { +export function VectorStyleLabelBorderSizeEditor({ + disabled, + disabledBy, + handlePropertyChange, + styleProperty, +}) { function onChange(e) { const styleDescriptor = { options: { size: e.target.value }, @@ -46,12 +51,13 @@ export function VectorStyleLabelBorderSizeEditor({ handlePropertyChange, stylePr handlePropertyChange(styleProperty.getStyleName(), styleDescriptor); } - return ( + const labelBorderSizeForm = ( ); + + if (!disabled) { + return labelBorderSizeForm; + } + + return ( + + {labelBorderSizeForm} + + ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js index e8b544d8ede161..f1180a3a564940 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js @@ -5,8 +5,15 @@ */ import React, { Component, Fragment } from 'react'; -import { getVectorStyleLabel } from './get_vector_style_label'; -import { EuiFormRow, EuiSelect } from '@elastic/eui'; +import { getVectorStyleLabel, getDisabledByMessage } from './get_vector_style_label'; +import { + EuiFormRow, + EuiSelect, + EuiFlexGroup, + EuiFlexItem, + EuiFieldText, + EuiToolTip, +} from '@elastic/eui'; import { VectorStyle } from '../vector_style'; import { i18n } from '@kbn/i18n'; @@ -69,7 +76,7 @@ export class StylePropEditor extends Component { : VectorStyle.STYLE_TYPE.STATIC } onChange={this._onTypeToggle} - disabled={this.props.fields.length === 0} + disabled={this.props.disabled || this.props.fields.length === 0} aria-label={i18n.translate('xpack.maps.styles.staticDynamicSelect.ariaLabel', { defaultMessage: 'Select to style by fixed value or by data value', })} @@ -83,17 +90,35 @@ export class StylePropEditor extends Component { this._onFieldMetaOptionsChange ); + const staticDynamicSelect = this.renderStaticDynamicSelect(); + + const stylePropertyForm = this.props.disabled ? ( + + + {staticDynamicSelect} + + + + + + ) : ( + + {React.cloneElement(this.props.children, { + staticDynamicSelect, + })} + {fieldMetaOptionsPopover} + + ); + return ( - - {React.cloneElement(this.props.children, { - staticDynamicSelect: this.renderStaticDynamicSelect(), - })} - {fieldMetaOptionsPopover} - + {stylePropertyForm} ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/vector_style_symbolize_as_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/vector_style_symbolize_as_editor.js index 9394e5c0685690..219fee311ba1b4 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/vector_style_symbolize_as_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/vector_style_symbolize_as_editor.js @@ -6,11 +6,12 @@ import React from 'react'; -import { EuiFormRow, EuiButtonGroup } from '@elastic/eui'; +import { EuiFormRow, EuiButtonGroup, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SYMBOLIZE_AS_TYPES } from '../../../../../../common/constants'; import { VECTOR_STYLES } from '../../vector_style_defaults'; +import { getDisabledByMessage } from '../get_vector_style_label'; const SYMBOLIZE_AS_OPTIONS = [ { @@ -27,7 +28,12 @@ const SYMBOLIZE_AS_OPTIONS = [ }, ]; -export function VectorStyleSymbolizeAsEditor({ styleProperty, handlePropertyChange }) { +export function VectorStyleSymbolizeAsEditor({ + disabled, + disabledBy, + styleProperty, + handlePropertyChange, +}) { const styleOptions = styleProperty.getOptions(); const selectedOption = SYMBOLIZE_AS_OPTIONS.find(({ id }) => { return id === styleOptions.value; @@ -42,7 +48,7 @@ export function VectorStyleSymbolizeAsEditor({ styleProperty, handlePropertyChan handlePropertyChange(VECTOR_STYLES.SYMBOLIZE_AS, styleDescriptor); }; - return ( + const symbolizeAsForm = ( ); + + if (!disabled) { + return symbolizeAsForm; + } + + return ( + + {symbolizeAsForm} + + ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index 9299022b7895b6..441ebfb2d53bfe 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -18,6 +18,7 @@ import { OrientationEditor } from './orientation/orientation_editor'; import { getDefaultDynamicProperties, getDefaultStaticProperties, + LABEL_BORDER_SIZES, VECTOR_STYLES, } from '../vector_style_defaults'; import { DEFAULT_FILL_COLORS, DEFAULT_LINE_COLORS } from '../../color_utils'; @@ -145,9 +146,33 @@ export class VectorStyleEditor extends Component { this.props.handlePropertyChange(propertyName, styleDescriptor); }; - _renderFillColor() { + _hasBorder() { + const width = this.props.styleProperties[VECTOR_STYLES.LINE_WIDTH]; + return width.isDynamic() ? width.isComplete() : width.getOptions().size !== 0; + } + + _hasMarkerOrIcon() { + const iconSize = this.props.styleProperties[VECTOR_STYLES.ICON_SIZE]; + return !iconSize.isDynamic() && iconSize.getOptions().size > 0; + } + + _hasLabel() { + const label = this.props.styleProperties[VECTOR_STYLES.LABEL_TEXT]; + return label.isDynamic() + ? label.isComplete() + : label.getOptions().value != null && label.getOptions().value.length; + } + + _hasLabelBorder() { + const labelBorderSize = this.props.styleProperties[VECTOR_STYLES.LABEL_BORDER_SIZE]; + return labelBorderSize.getOptions().size !== LABEL_BORDER_SIZES.NONE; + } + + _renderFillColor(isPointFillColor = false) { return ( - ); - } - _renderLabelProperties() { + const hasLabel = this._hasLabel(); + const hasLabelBorder = this._hasLabelBorder(); return ( @@ -286,12 +309,15 @@ export class VectorStyleEditor extends Component { } _renderPointProperties() { + const hasMarkerOrIcon = this._hasMarkerOrIcon(); let iconOrientationEditor; let iconEditor; if (this.props.styleProperties[VECTOR_STYLES.SYMBOLIZE_AS].isSymbolizedAsIcon()) { iconOrientationEditor = ( @@ -335,18 +365,29 @@ export class VectorStyleEditor extends Component { {iconEditor} - {this._renderFillColor()} + {this._renderFillColor(true)} - {this._renderLineColor()} + {this._renderLineColor(true)} - {this._renderLineWidth()} + {this._renderLineWidth(true)} {iconOrientationEditor} - {this._renderSymbolSize()} + {this._renderLabelProperties()} diff --git a/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.html b/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.html index 8c67451b86f36e..63cd4440ecf8a2 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.html +++ b/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.html @@ -15,9 +15,9 @@ class="kuiInfoPanelBody__message" i18n-id="xpack.monitoring.accessDenied.notAuthorizedDescription" i18n-default-message="You are not authorized to access Monitoring. To use Monitoring, you - need the privileges granted by both the `{kibanaUser}` and + need the privileges granted by both the `{kibanaAdmin}` and `{monitoringUser}` roles." - i18n-values="{ kibanaUser: 'kibana_user', monitoringUser: 'monitoring_user' }" + i18n-values="{ kibanaAdmin: 'kibana_admin', monitoringUser: 'monitoring_user' }" >
clusterStub, + }, + }; + beforeAll(async function() { const crypto = nodeCrypto({ encryptionKey }); encryptedHeaders = await crypto.encrypt(headers); @@ -55,11 +61,11 @@ describe('CSV Execute Job', function() { _scroll_id: 'defaultScrollId', }; clusterStub = { - callWithRequest: function() {}, + callAsCurrentUser: function() {}, }; - callWithRequestStub = sinon - .stub(clusterStub, 'callWithRequest') + callAsCurrentUserStub = sinon + .stub(clusterStub, 'callAsCurrentUser') .resolves(defaultElasticsearchResponse); const configGetStub = sinon.stub(); @@ -68,7 +74,6 @@ describe('CSV Execute Job', function() { uiSettingsGetStub.withArgs('csv:quoteValues').returns(true); mockServer = { - expose: function() {}, fieldFormatServiceFactory: function() { const uiConfigMock = {}; uiConfigMock['format:defaultTypeMap'] = { @@ -81,13 +86,6 @@ describe('CSV Execute Job', function() { return fieldFormatsRegistry; }, - plugins: { - elasticsearch: { - getCluster: function() { - return clusterStub; - }, - }, - }, config: function() { return { get: configGetStub, @@ -117,7 +115,7 @@ describe('CSV Execute Job', function() { describe('calls getScopedSavedObjectsClient with request', function() { it('containing decrypted headers', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -135,7 +133,7 @@ describe('CSV Execute Job', function() { .config() .get.withArgs('server.basePath') .returns(serverBasePath); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -153,7 +151,7 @@ describe('CSV Execute Job', function() { .config() .get.withArgs('server.basePath') .returns(serverBasePath); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobBasePath = 'foo-job/basePath/'; await executeJob( 'job789', @@ -176,7 +174,7 @@ describe('CSV Execute Job', function() { it('passed scoped SavedObjectsClient to uiSettingsServiceFactory', async function() { const returnValue = Symbol(); mockServer.savedObjects.getScopedSavedObjectsClient.returns(returnValue); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -190,15 +188,15 @@ describe('CSV Execute Job', function() { }); describe('basic Elasticsearch call behavior', function() { - it('should decrypt encrypted headers and pass to callWithRequest', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + it('should decrypt encrypted headers and pass to callAsCurrentUser', async function() { + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, cancellationToken ); - expect(callWithRequestStub.called).toBe(true); - expect(callWithRequestStub.firstCall.args[0].headers).toEqual(headers); + expect(callAsCurrentUserStub.called).toBe(true); + expect(callAsCurrentUserStub.firstCall.args[0]).toEqual('search'); }); it('should pass the index and body to execute the initial search', async function() { @@ -207,7 +205,7 @@ describe('CSV Execute Job', function() { testBody: true, }; - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const job = { headers: encryptedHeaders, fields: [], @@ -219,115 +217,115 @@ describe('CSV Execute Job', function() { await executeJob('job777', job, cancellationToken); - const searchCall = callWithRequestStub.firstCall; - expect(searchCall.args[1]).toBe('search'); - expect(searchCall.args[2].index).toBe(index); - expect(searchCall.args[2].body).toBe(body); + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); + expect(searchCall.args[1].index).toBe(index); + expect(searchCall.args[1].body).toBe(body); }); it('should pass the scrollId from the initial search to the subsequent scroll', async function() { const scrollId = getRandomScrollId(); - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: scrollId, }); - callWithRequestStub.onSecondCall().resolves(defaultElasticsearchResponse); - const executeJob = executeJobFactory(mockServer, mockLogger); + callAsCurrentUserStub.onSecondCall().resolves(defaultElasticsearchResponse); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, cancellationToken ); - const scrollCall = callWithRequestStub.secondCall; + const scrollCall = callAsCurrentUserStub.secondCall; - expect(scrollCall.args[1]).toBe('scroll'); - expect(scrollCall.args[2].scrollId).toBe(scrollId); + expect(scrollCall.args[0]).toBe('scroll'); + expect(scrollCall.args[1].scrollId).toBe(scrollId); }); it('should not execute scroll if there are no hits from the search', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, cancellationToken ); - expect(callWithRequestStub.callCount).toBe(2); + expect(callAsCurrentUserStub.callCount).toBe(2); - const searchCall = callWithRequestStub.firstCall; - expect(searchCall.args[1]).toBe('search'); + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); - const clearScrollCall = callWithRequestStub.secondCall; - expect(clearScrollCall.args[1]).toBe('clearScroll'); + const clearScrollCall = callAsCurrentUserStub.secondCall; + expect(clearScrollCall.args[0]).toBe('clearScroll'); }); it('should stop executing scroll if there are no hits', async function() { - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - callWithRequestStub.onSecondCall().resolves({ + callAsCurrentUserStub.onSecondCall().resolves({ hits: { hits: [], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, cancellationToken ); - expect(callWithRequestStub.callCount).toBe(3); + expect(callAsCurrentUserStub.callCount).toBe(3); - const searchCall = callWithRequestStub.firstCall; - expect(searchCall.args[1]).toBe('search'); + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); - const scrollCall = callWithRequestStub.secondCall; - expect(scrollCall.args[1]).toBe('scroll'); + const scrollCall = callAsCurrentUserStub.secondCall; + expect(scrollCall.args[0]).toBe('scroll'); - const clearScroll = callWithRequestStub.thirdCall; - expect(clearScroll.args[1]).toBe('clearScroll'); + const clearScroll = callAsCurrentUserStub.thirdCall; + expect(clearScroll.args[0]).toBe('clearScroll'); }); it('should call clearScroll with scrollId when there are no more hits', async function() { const lastScrollId = getRandomScrollId(); - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - callWithRequestStub.onSecondCall().resolves({ + callAsCurrentUserStub.onSecondCall().resolves({ hits: { hits: [], }, _scroll_id: lastScrollId, }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, cancellationToken ); - const lastCall = callWithRequestStub.getCall(callWithRequestStub.callCount - 1); - expect(lastCall.args[1]).toBe('clearScroll'); - expect(lastCall.args[2].scrollId).toEqual([lastScrollId]); + const lastCall = callAsCurrentUserStub.getCall(callAsCurrentUserStub.callCount - 1); + expect(lastCall.args[0]).toBe('clearScroll'); + expect(lastCall.args[1].scrollId).toEqual([lastScrollId]); }); it('calls clearScroll when there is an error iterating the hits', async function() { const lastScrollId = getRandomScrollId(); - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [ { @@ -341,7 +339,7 @@ describe('CSV Execute Job', function() { _scroll_id: lastScrollId, }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -352,9 +350,9 @@ describe('CSV Execute Job', function() { executeJob('job123', jobParams, cancellationToken) ).rejects.toMatchInlineSnapshot(`[TypeError: Cannot read property 'indexOf' of undefined]`); - const lastCall = callWithRequestStub.getCall(callWithRequestStub.callCount - 1); - expect(lastCall.args[1]).toBe('clearScroll'); - expect(lastCall.args[2].scrollId).toEqual([lastScrollId]); + const lastCall = callAsCurrentUserStub.getCall(callAsCurrentUserStub.callCount - 1); + expect(lastCall.args[0]).toBe('clearScroll'); + expect(lastCall.args[1].scrollId).toEqual([lastScrollId]); }); }); @@ -364,14 +362,14 @@ describe('CSV Execute Job', function() { .config() .get.withArgs('xpack.reporting.csv.checkForFormulas') .returns(true); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: '=SUM(A1:A2)', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -392,14 +390,14 @@ describe('CSV Execute Job', function() { .config() .get.withArgs('xpack.reporting.csv.checkForFormulas') .returns(true); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { '=SUM(A1:A2)': 'foo', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['=SUM(A1:A2)', 'two'], @@ -420,14 +418,14 @@ describe('CSV Execute Job', function() { .config() .get.withArgs('xpack.reporting.csv.checkForFormulas') .returns(true); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -448,14 +446,14 @@ describe('CSV Execute Job', function() { .config() .get.withArgs('xpack.reporting.csv.checkForFormulas') .returns(false); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: '=SUM(A1:A2)', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -474,8 +472,8 @@ describe('CSV Execute Job', function() { describe('Elasticsearch call errors', function() { it('should reject Promise if search call errors out', async function() { - callWithRequestStub.rejects(new Error()); - const executeJob = executeJobFactory(mockServer, mockLogger); + callAsCurrentUserStub.rejects(new Error()); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -487,14 +485,14 @@ describe('CSV Execute Job', function() { }); it('should reject Promise if scroll call errors out', async function() { - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - callWithRequestStub.onSecondCall().rejects(new Error()); - const executeJob = executeJobFactory(mockServer, mockLogger); + callAsCurrentUserStub.onSecondCall().rejects(new Error()); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -508,14 +506,14 @@ describe('CSV Execute Job', function() { describe('invalid responses', function() { it('should reject Promise if search returns hits but no _scroll_id', async function() { - callWithRequestStub.resolves({ + callAsCurrentUserStub.resolves({ hits: { hits: [{}], }, _scroll_id: undefined, }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -529,14 +527,14 @@ describe('CSV Execute Job', function() { }); it('should reject Promise if search returns no hits and no _scroll_id', async function() { - callWithRequestStub.resolves({ + callAsCurrentUserStub.resolves({ hits: { hits: [], }, _scroll_id: undefined, }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -550,21 +548,21 @@ describe('CSV Execute Job', function() { }); it('should reject Promise if scroll returns hits but no _scroll_id', async function() { - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - callWithRequestStub.onSecondCall().resolves({ + callAsCurrentUserStub.onSecondCall().resolves({ hits: { hits: [{}], }, _scroll_id: undefined, }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -578,21 +576,21 @@ describe('CSV Execute Job', function() { }); it('should reject Promise if scroll returns no hits and no _scroll_id', async function() { - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - callWithRequestStub.onSecondCall().resolves({ + callAsCurrentUserStub.onSecondCall().resolves({ hits: { hits: [], }, _scroll_id: undefined, }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -610,23 +608,25 @@ describe('CSV Execute Job', function() { const scrollId = getRandomScrollId(); beforeEach(function() { - // We have to "re-stub" the callWithRequest stub here so that we can use the fakeFunction + // We have to "re-stub" the callAsCurrentUser stub here so that we can use the fakeFunction // that delays the Promise resolution so we have a chance to call cancellationToken.cancel(). // Otherwise, we get into an endless loop, and don't have a chance to call cancel - callWithRequestStub.restore(); - callWithRequestStub = sinon.stub(clusterStub, 'callWithRequest').callsFake(async function() { - await delay(1); - return { - hits: { - hits: [{}], - }, - _scroll_id: scrollId, - }; - }); + callAsCurrentUserStub.restore(); + callAsCurrentUserStub = sinon + .stub(clusterStub, 'callAsCurrentUser') + .callsFake(async function() { + await delay(1); + return { + hits: { + hits: [{}], + }, + _scroll_id: scrollId, + }; + }); }); it('should stop calling Elasticsearch when cancellationToken.cancel is called', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); executeJob( 'job345', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -634,14 +634,14 @@ describe('CSV Execute Job', function() { ); await delay(250); - const callCount = callWithRequestStub.callCount; + const callCount = callAsCurrentUserStub.callCount; cancellationToken.cancel(); await delay(250); - expect(callWithRequestStub.callCount).toBe(callCount + 1); // last call is to clear the scroll + expect(callAsCurrentUserStub.callCount).toBe(callCount + 1); // last call is to clear the scroll }); it(`shouldn't call clearScroll if it never got a scrollId`, async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); executeJob( 'job345', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -649,13 +649,13 @@ describe('CSV Execute Job', function() { ); cancellationToken.cancel(); - for (let i = 0; i < callWithRequestStub.callCount; ++i) { - expect(callWithRequestStub.getCall(i).args[1]).to.not.be('clearScroll'); + for (let i = 0; i < callAsCurrentUserStub.callCount; ++i) { + expect(callAsCurrentUserStub.getCall(i).args[1]).to.not.be('clearScroll'); } }); it('should call clearScroll if it got a scrollId', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); executeJob( 'job345', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -665,15 +665,15 @@ describe('CSV Execute Job', function() { cancellationToken.cancel(); await delay(100); - const lastCall = callWithRequestStub.getCall(callWithRequestStub.callCount - 1); - expect(lastCall.args[1]).toBe('clearScroll'); - expect(lastCall.args[2].scrollId).toEqual([scrollId]); + const lastCall = callAsCurrentUserStub.getCall(callAsCurrentUserStub.callCount - 1); + expect(lastCall.args[0]).toBe('clearScroll'); + expect(lastCall.args[1].scrollId).toEqual([scrollId]); }); }); describe('csv content', function() { it('should write column headers to output, even if there are no results', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -685,7 +685,7 @@ describe('CSV Execute Job', function() { it('should use custom uiSettings csv:separator for header', async function() { uiSettingsGetStub.withArgs('csv:separator').returns(';'); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -697,7 +697,7 @@ describe('CSV Execute Job', function() { it('should escape column headers if uiSettings csv:quoteValues is true', async function() { uiSettingsGetStub.withArgs('csv:quoteValues').returns(true); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one and a half', 'two', 'three-and-four', 'five & six'], @@ -709,7 +709,7 @@ describe('CSV Execute Job', function() { it(`shouldn't escape column headers if uiSettings csv:quoteValues is false`, async function() { uiSettingsGetStub.withArgs('csv:quoteValues').returns(false); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one and a half', 'two', 'three-and-four', 'five & six'], @@ -720,8 +720,8 @@ describe('CSV Execute Job', function() { }); it('should write column headers to output, when there are results', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); - callWithRequestStub.onFirstCall().resolves({ + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ one: '1', two: '2' }], }, @@ -740,8 +740,8 @@ describe('CSV Execute Job', function() { }); it('should use comma separated values of non-nested fields from _source', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); - callWithRequestStub.onFirstCall().resolves({ + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], }, @@ -761,14 +761,14 @@ describe('CSV Execute Job', function() { }); it('should concatenate the hits from multiple responses', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); - callWithRequestStub.onFirstCall().resolves({ + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - callWithRequestStub.onSecondCall().resolves({ + callAsCurrentUserStub.onSecondCall().resolves({ hits: { hits: [{ _source: { one: 'baz', two: 'qux' } }], }, @@ -789,8 +789,8 @@ describe('CSV Execute Job', function() { }); it('should use field formatters to format fields', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); - callWithRequestStub.onFirstCall().resolves({ + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], }, @@ -834,7 +834,7 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.maxSizeBytes') .returns(1); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -867,7 +867,7 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.maxSizeBytes') .returns(9); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -900,14 +900,14 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.maxSizeBytes') .returns(9); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -941,14 +941,14 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.maxSizeBytes') .returns(18); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -981,14 +981,14 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.scroll') .returns({ duration: scrollDuration }); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -998,9 +998,9 @@ describe('CSV Execute Job', function() { await executeJob('job123', jobParams, cancellationToken); - const searchCall = callWithRequestStub.firstCall; - expect(searchCall.args[1]).toBe('search'); - expect(searchCall.args[2].scroll).toBe(scrollDuration); + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); + expect(searchCall.args[1].scroll).toBe(scrollDuration); }); it('passes scroll size to initial search call', async function() { @@ -1010,14 +1010,14 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.scroll') .returns({ size: scrollSize }); - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -1027,9 +1027,9 @@ describe('CSV Execute Job', function() { await executeJob('job123', jobParams, cancellationToken); - const searchCall = callWithRequestStub.firstCall; - expect(searchCall.args[1]).toBe('search'); - expect(searchCall.args[2].size).toBe(scrollSize); + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); + expect(searchCall.args[1].size).toBe(scrollSize); }); it('passes scroll duration to subsequent scroll call', async function() { @@ -1039,14 +1039,14 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.scroll') .returns({ duration: scrollDuration }); - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -1056,9 +1056,9 @@ describe('CSV Execute Job', function() { await executeJob('job123', jobParams, cancellationToken); - const scrollCall = callWithRequestStub.secondCall; - expect(scrollCall.args[1]).toBe('scroll'); - expect(scrollCall.args[2].scroll).toBe(scrollDuration); + const scrollCall = callAsCurrentUserStub.secondCall; + expect(scrollCall.args[0]).toBe('scroll'); + expect(scrollCall.args[1].scroll).toBe(scrollDuration); }); }); }); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts index fe64fdc96d9043..280bbf13fa9928 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import Hapi from 'hapi'; import { i18n } from '@kbn/i18n'; -import { KibanaRequest } from '../../../../../../../src/core/server'; +import { ElasticsearchServiceSetup, KibanaRequest } from '../../../../../../../src/core/server'; import { CSV_JOB_TYPE } from '../../../common/constants'; import { cryptoFactory } from '../../../server/lib'; import { ESQueueWorkerExecuteFn, ExecuteJobFactory, Logger, ServerFacade } from '../../../types'; @@ -15,8 +16,11 @@ import { createGenerateCsv } from './lib/generate_csv'; export const executeJobFactory: ExecuteJobFactory> = function executeJobFactoryFn(server: ServerFacade, parentLogger: Logger) { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); +>> = function executeJobFactoryFn( + server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, + parentLogger: Logger +) { const crypto = cryptoFactory(server); const config = server.config(); const logger = parentLogger.clone([CSV_JOB_TYPE, 'execute-job']); @@ -74,8 +78,11 @@ export const executeJobFactory: ExecuteJobFactory { - return callWithRequest(fakeRequest, endpoint, clientParams, options); + return callAsCurrentUser(endpoint, clientParams, options); }; const savedObjects = server.savedObjects; const savedObjectsClient = savedObjects.getScopedSavedObjectsClient( diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts index a270e3e0329fe8..ddef2aa0a62688 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts @@ -6,24 +6,25 @@ import { notFound, notImplemented } from 'boom'; import { get } from 'lodash'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../../common/constants'; import { cryptoFactory } from '../../../../server/lib'; import { CreateJobFactory, ImmediateCreateJobFn, - ServerFacade, - RequestFacade, Logger, + RequestFacade, + ServerFacade, } from '../../../../types'; import { + JobDocPayloadPanelCsv, + JobParamsPanelCsv, SavedObject, SavedObjectServiceError, SavedSearchObjectAttributesJSON, SearchPanel, TimeRangeParams, VisObjectAttributesJSON, - JobDocPayloadPanelCsv, - JobParamsPanelCsv, } from '../../types'; import { createJobSearch } from './create_job_search'; @@ -35,7 +36,11 @@ interface VisData { export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(server: ServerFacade, parentLogger: Logger) { +>> = function createJobFactoryFn( + server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, + parentLogger: Logger +) { const crypto = cryptoFactory(server); const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'create-job']); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts index 03f491deaa43d6..b1b7b7d818200e 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; import { cryptoFactory } from '../../../server/lib'; import { @@ -21,7 +22,11 @@ import { createGenerateCsv } from './lib'; export const executeJobFactory: ExecuteJobFactory> = function executeJobFactoryFn(server: ServerFacade, parentLogger: Logger) { +>> = function executeJobFactoryFn( + server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, + parentLogger: Logger +) { const crypto = cryptoFactory(server); const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'execute-job']); @@ -85,6 +90,7 @@ export const executeJobFactory: ExecuteJobFactory { export async function generateCsvSearch( req: RequestFacade, server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, logger: Logger, searchPanel: SearchPanel, jobParams: JobParamsDiscoverCsv @@ -152,8 +152,11 @@ export async function generateCsvSearch( sort: sortConfig, }, }; - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); - const callCluster = (...params: [string, object]) => callWithRequest(req, ...params); + + const { callAsCurrentUser } = elasticsearch.dataClient.asScoped( + KibanaRequest.from(req.getRawRequest()) + ); + const callCluster = (...params: [string, object]) => callAsCurrentUser(...params); const config = server.config(); const uiSettings = await getUiSettings(uiConfig); diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js index 4f02ab5d4c077e..bb33ef9c19a1dd 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js @@ -32,15 +32,6 @@ beforeEach(() => { info: { protocol: 'http', }, - plugins: { - elasticsearch: { - getCluster: memoize(() => { - return { - callWithRequest: jest.fn(), - }; - }), - }, - }, savedObjects: { getScopedSavedObjectsClient: jest.fn(), }, @@ -57,6 +48,12 @@ beforeEach(() => { afterEach(() => generatePngObservableFactory.mockReset()); +const mockElasticsearch = { + dataClient: { + asScoped: () => ({ callAsCurrentUser: jest.fn() }), + }, +}; + const getMockLogger = () => new LevelLogger(); const encryptHeaders = async headers => { @@ -70,7 +67,9 @@ test(`passes browserTimezone to generatePng`, async () => { const generatePngObservable = generatePngObservableFactory(); generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - const executeJob = executeJobFactory(mockServer, getMockLogger(), { browserDriverFactory: {} }); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, getMockLogger(), { + browserDriverFactory: {}, + }); const browserTimezone = 'UTC'; await executeJob( 'pngJobId', @@ -88,7 +87,9 @@ test(`passes browserTimezone to generatePng`, async () => { }); test(`returns content_type of application/png`, async () => { - const executeJob = executeJobFactory(mockServer, getMockLogger(), { browserDriverFactory: {} }); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, getMockLogger(), { + browserDriverFactory: {}, + }); const encryptedHeaders = await encryptHeaders({}); const generatePngObservable = generatePngObservableFactory(); @@ -108,7 +109,9 @@ test(`returns content of generatePng getBuffer base64 encoded`, async () => { const generatePngObservable = generatePngObservableFactory(); generatePngObservable.mockReturnValue(Rx.of(Buffer.from(testContent))); - const executeJob = executeJobFactory(mockServer, getMockLogger(), { browserDriverFactory: {} }); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, getMockLogger(), { + browserDriverFactory: {}, + }); const encryptedHeaders = await encryptHeaders({}); const { content } = await executeJob( 'pngJobId', diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts index 7d5c69655c362f..c9f370197da662 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts @@ -5,6 +5,7 @@ */ import * as Rx from 'rxjs'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { PNG_JOB_TYPE } from '../../../../common/constants'; import { @@ -27,6 +28,7 @@ type QueuedPngExecutorFactory = ExecuteJobFactory { info: { protocol: 'http', }, - plugins: { - elasticsearch: { - getCluster: memoize(() => { - return { - callWithRequest: jest.fn(), - }; - }), - }, - }, savedObjects: { getScopedSavedObjectsClient: jest.fn(), }, @@ -58,6 +49,11 @@ beforeEach(() => { afterEach(() => generatePdfObservableFactory.mockReset()); const getMockLogger = () => new LevelLogger(); +const mockElasticsearch = { + dataClient: { + asScoped: () => ({ callAsCurrentUser: jest.fn() }), + }, +}; const encryptHeaders = async headers => { const crypto = cryptoFactory(mockServer); @@ -70,7 +66,9 @@ test(`passes browserTimezone to generatePdf`, async () => { const generatePdfObservable = generatePdfObservableFactory(); generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - const executeJob = executeJobFactory(mockServer, getMockLogger(), { browserDriverFactory: {} }); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, getMockLogger(), { + browserDriverFactory: {}, + }); const browserTimezone = 'UTC'; await executeJob( 'pdfJobId', @@ -91,7 +89,9 @@ test(`passes browserTimezone to generatePdf`, async () => { }); test(`returns content_type of application/pdf`, async () => { - const executeJob = executeJobFactory(mockServer, getMockLogger(), { browserDriverFactory: {} }); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, getMockLogger(), { + browserDriverFactory: {}, + }); const encryptedHeaders = await encryptHeaders({}); const generatePdfObservable = generatePdfObservableFactory(); @@ -111,7 +111,9 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const generatePdfObservable = generatePdfObservableFactory(); generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(testContent))); - const executeJob = executeJobFactory(mockServer, getMockLogger(), { browserDriverFactory: {} }); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, getMockLogger(), { + browserDriverFactory: {}, + }); const encryptedHeaders = await encryptHeaders({}); const { content } = await executeJob( 'pdfJobId', diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts index dee53697c6681d..162376e31216e0 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts @@ -5,6 +5,7 @@ */ import * as Rx from 'rxjs'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { ServerFacade, @@ -28,6 +29,7 @@ type QueuedPdfExecutorFactory = ExecuteJobFactory { @@ -74,10 +69,6 @@ export const reporting = (kibana: any) => { async init(server: Legacy.Server) { const coreSetup = server.newPlatform.setup.core; - const pluginsSetup: ReportingSetupDeps = { - security: server.newPlatform.setup.plugins.security as SecurityPluginSetup, - usageCollection: server.newPlatform.setup.plugins.usageCollection, - }; const fieldFormatServiceFactory = async (uiSettings: IUiSettingsClient) => { const [, plugins] = await coreSetup.getStartServices(); @@ -90,18 +81,22 @@ export const reporting = (kibana: any) => { config: server.config, info: server.info, route: server.route.bind(server), - plugins: { - elasticsearch: server.plugins.elasticsearch, - xpack_main: server.plugins.xpack_main, - }, + plugins: { xpack_main: server.plugins.xpack_main }, savedObjects: server.savedObjects, fieldFormatServiceFactory, uiSettingsServiceFactory: server.uiSettingsServiceFactory, }; - const initializerContext = server.newPlatform.coreContext; - const plugin: ReportingPlugin = reportingPluginFactory(initializerContext, __LEGACY, this); - await plugin.setup(coreSetup, pluginsSetup); + const plugin: ReportingPlugin = reportingPluginFactory( + server.newPlatform.coreContext, + __LEGACY, + this + ); + await plugin.setup(coreSetup, { + elasticsearch: coreSetup.elasticsearch, + security: server.newPlatform.setup.plugins.security as SecurityPluginSetup, + usageCollection: server.newPlatform.setup.plugins.usageCollection, + }); }, deprecations({ unused }: any) { diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts index 05b760c0c3bd6c..c4e32b3ebcd99e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ElasticsearchServiceSetup } from 'kibana/server'; import { ServerFacade, ExportTypesRegistry, @@ -23,6 +24,7 @@ interface CreateQueueFactoryOpts { export function createQueueFactory( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, logger: Logger, { exportTypesRegistry, browserDriverFactory }: CreateQueueFactoryOpts ): Esqueue { @@ -33,7 +35,7 @@ export function createQueueFactory( interval: queueConfig.indexInterval, timeout: queueConfig.timeout, dateSeparator: '.', - client: server.plugins.elasticsearch.getCluster('admin'), + client: elasticsearch.dataClient, logger: createTaggedLogger(logger, ['esqueue', 'queue-worker']), }; @@ -41,7 +43,7 @@ export function createQueueFactory( if (queueConfig.pollEnabled) { // create workers to poll the index for idle jobs waiting to be claimed and executed - const createWorker = createWorkerFactory(server, logger, { + const createWorker = createWorkerFactory(server, elasticsearch, logger, { exportTypesRegistry, browserDriverFactory, }); diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts b/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts index 6a5c93db32376a..f5c42e5505cd1d 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts @@ -5,13 +5,14 @@ */ import * as sinon from 'sinon'; -import { ServerFacade, HeadlessChromiumDriverFactory } from '../../types'; -import { ExportTypesRegistry } from './export_types_registry'; +import { ElasticsearchServiceSetup } from 'kibana/server'; +import { HeadlessChromiumDriverFactory, ServerFacade } from '../../types'; import { createWorkerFactory } from './create_worker'; // @ts-ignore import { Esqueue } from './esqueue'; // @ts-ignore import { ClientMock } from './esqueue/__tests__/fixtures/legacy_elasticsearch'; +import { ExportTypesRegistry } from './export_types_registry'; const configGetStub = sinon.stub(); configGetStub.withArgs('xpack.reporting.queue').returns({ @@ -48,10 +49,15 @@ describe('Create Worker', () => { test('Creates a single Esqueue worker for Reporting', async () => { const exportTypesRegistry = getMockExportTypesRegistry(); - const createWorker = createWorkerFactory(getMockServer(), getMockLogger(), { - exportTypesRegistry: exportTypesRegistry as ExportTypesRegistry, - browserDriverFactory: {} as HeadlessChromiumDriverFactory, - }); + const createWorker = createWorkerFactory( + getMockServer(), + {} as ElasticsearchServiceSetup, + getMockLogger(), + { + exportTypesRegistry: exportTypesRegistry as ExportTypesRegistry, + browserDriverFactory: {} as HeadlessChromiumDriverFactory, + } + ); const registerWorkerSpy = sinon.spy(queue, 'registerWorker'); createWorker(queue); @@ -82,10 +88,15 @@ Object { { executeJobFactory: executeJobFactoryStub }, { executeJobFactory: executeJobFactoryStub }, ]); - const createWorker = createWorkerFactory(getMockServer(), getMockLogger(), { - exportTypesRegistry: exportTypesRegistry as ExportTypesRegistry, - browserDriverFactory: {} as HeadlessChromiumDriverFactory, - }); + const createWorker = createWorkerFactory( + getMockServer(), + {} as ElasticsearchServiceSetup, + getMockLogger(), + { + exportTypesRegistry: exportTypesRegistry as ExportTypesRegistry, + browserDriverFactory: {} as HeadlessChromiumDriverFactory, + } + ); const registerWorkerSpy = sinon.spy(queue, 'registerWorker'); createWorker(queue); diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts index 67869016a250be..2ca638f641291d 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ElasticsearchServiceSetup } from 'kibana/server'; import { PLUGIN_ID } from '../../common/constants'; import { ExportTypesRegistry, HeadlessChromiumDriverFactory } from '../../types'; import { CancellationToken } from '../../common/cancellation_token'; @@ -29,6 +30,7 @@ interface CreateWorkerFactoryOpts { export function createWorkerFactory( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, logger: Logger, { exportTypesRegistry, browserDriverFactory }: CreateWorkerFactoryOpts ) { @@ -50,7 +52,9 @@ export function createWorkerFactory( ExportTypeDefinition >) { // TODO: the executeJobFn should be unwrapped in the register method of the export types registry - const jobExecutor = exportType.executeJobFactory(server, logger, { browserDriverFactory }); + const jobExecutor = exportType.executeJobFactory(server, elasticsearch, logger, { + browserDriverFactory, + }); jobExecutors.set(exportType.jobType, jobExecutor); } diff --git a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts index 14c57fa35dcf4e..1da8a3795aacc5 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts @@ -5,6 +5,7 @@ */ import { get } from 'lodash'; +import { ElasticsearchServiceSetup } from 'kibana/server'; // @ts-ignore import { events as esqueueEvents } from './esqueue'; import { @@ -35,6 +36,7 @@ interface EnqueueJobFactoryOpts { export function enqueueJobFactory( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, parentLogger: Logger, { exportTypesRegistry, esqueue }: EnqueueJobFactoryOpts ): EnqueueJobFn { @@ -61,7 +63,7 @@ export function enqueueJobFactory( } // TODO: the createJobFn should be unwrapped in the register method of the export types registry - const createJob = exportType.createJobFactory(server, logger) as CreateJobFn; + const createJob = exportType.createJobFactory(server, elasticsearch, logger) as CreateJobFn; const payload = await createJob(jobParams, headers, request); const options = { diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/legacy_elasticsearch.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/legacy_elasticsearch.js index 31bdf7767983dd..ebda7ff955b11c 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/legacy_elasticsearch.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/legacy_elasticsearch.js @@ -1,10 +1,14 @@ -import { uniqueId, times, random } from 'lodash'; -import * as legacyElasticsearch from 'elasticsearch'; +/* + * 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 { constants } from '../../constants'; +import { uniqueId, times, random } from 'lodash'; +import { errors as esErrors } from 'elasticsearch'; export function ClientMock() { - this.callWithInternalUser = (endpoint, params = {}, ...rest) => { + this.callAsInternalUser = (endpoint, params = {}, ...rest) => { if (endpoint === 'indices.create') { return Promise.resolve({ acknowledged: true }); } @@ -21,12 +25,12 @@ export function ClientMock() { _seq_no: 1, _primary_term: 1, _shards: { total: shardCount, successful: shardCount, failed: 0 }, - created: true + created: true, }); } if (endpoint === 'get') { - if (params === legacyElasticsearch.errors.NotFound) return legacyElasticsearch.errors.NotFound; + if (params === esErrors.NotFound) return esErrors.NotFound; const _source = { jobtype: 'jobtype', @@ -34,7 +38,7 @@ export function ClientMock() { payload: { id: 'sample-job-1', - now: 'Mon Apr 25 2016 14:13:04 GMT-0700 (MST)' + now: 'Mon Apr 25 2016 14:13:04 GMT-0700 (MST)', }, priority: 10, @@ -43,7 +47,7 @@ export function ClientMock() { attempts: 0, max_attempts: 3, status: 'pending', - ...(rest[0] || {}) + ...(rest[0] || {}), }; return Promise.resolve({ @@ -52,7 +56,7 @@ export function ClientMock() { _seq_no: params._seq_no || 1, _primary_term: params._primary_term || 1, found: true, - _source: _source + _source: _source, }); } @@ -68,8 +72,8 @@ export function ClientMock() { _source: { created_at: new Date().toString(), number: random(0, count, true), - ...source - } + ...source, + }, }; }); return Promise.resolve({ @@ -78,13 +82,13 @@ export function ClientMock() { _shards: { total: 5, successful: 5, - failed: 0 + failed: 0, }, hits: { total: count, max_score: null, - hits: hits - } + hits: hits, + }, }); } @@ -96,7 +100,7 @@ export function ClientMock() { _seq_no: params.if_seq_no + 1 || 2, _primary_term: params.if_primary_term + 1 || 2, _shards: { total: shardCount, successful: shardCount, failed: 0 }, - created: true + created: true, }); } diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js index 23e9aab5bad115..2944574534a827 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js @@ -17,7 +17,7 @@ describe('Create Index', function() { beforeEach(function() { client = new ClientMock(); - createSpy = sinon.spy(client, 'callWithInternalUser').withArgs('indices.create'); + createSpy = sinon.spy(client, 'callAsInternalUser').withArgs('indices.create'); }); it('should return true', function() { @@ -75,10 +75,10 @@ describe('Create Index', function() { beforeEach(function() { client = new ClientMock(); sinon - .stub(client, 'callWithInternalUser') + .stub(client, 'callAsInternalUser') .withArgs('indices.exists') .callsFake(() => Promise.resolve(true)); - createSpy = client.callWithInternalUser.withArgs('indices.create'); + createSpy = client.callAsInternalUser.withArgs('indices.create'); }); it('should return true', function() { diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/index.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/index.js index 8f1ed69de5e7f6..428c0f0bc0736c 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/index.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/index.js @@ -40,7 +40,7 @@ describe('Esqueue class', function() { describe('Queue construction', function() { it('should ping the ES server', function() { - const pingSpy = sinon.spy(client, 'callWithInternalUser').withArgs('ping'); + const pingSpy = sinon.spy(client, 'callAsInternalUser').withArgs('ping'); new Esqueue('esqueue', { client }); sinon.assert.calledOnce(pingSpy); }); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/job.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/job.js index 2d8410c18ddeab..c7812ec151b005 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/job.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/job.js @@ -79,7 +79,7 @@ describe('Job Class', function() { beforeEach(function() { type = 'type1'; payload = { id: '123' }; - indexSpy = sinon.spy(client, 'callWithInternalUser').withArgs('index'); + indexSpy = sinon.spy(client, 'callAsInternalUser').withArgs('index'); }); it('should create the target index', function() { @@ -121,7 +121,7 @@ describe('Job Class', function() { }); it('should refresh the index', function() { - const refreshSpy = client.callWithInternalUser.withArgs('indices.refresh'); + const refreshSpy = client.callAsInternalUser.withArgs('indices.refresh'); const job = new Job(mockQueue, index, type, payload); return job.ready.then(() => { @@ -165,9 +165,9 @@ describe('Job Class', function() { it('should emit error on client index failure', function(done) { const errMsg = 'test document index failure'; - client.callWithInternalUser.restore(); + client.callAsInternalUser.restore(); sinon - .stub(client, 'callWithInternalUser') + .stub(client, 'callAsInternalUser') .withArgs('index') .callsFake(() => Promise.reject(new Error(errMsg))); const job = new Job(mockQueue, index, type, payload); @@ -215,7 +215,7 @@ describe('Job Class', function() { beforeEach(function() { type = 'type1'; payload = { id: '123' }; - indexSpy = sinon.spy(client, 'callWithInternalUser').withArgs('index'); + indexSpy = sinon.spy(client, 'callAsInternalUser').withArgs('index'); }); it('should set attempt count to 0', function() { @@ -281,7 +281,7 @@ describe('Job Class', function() { authorization: 'Basic cXdlcnR5', }, }; - indexSpy = sinon.spy(client, 'callWithInternalUser').withArgs('index'); + indexSpy = sinon.spy(client, 'callAsInternalUser').withArgs('index'); }); it('should index the created_by value', function() { @@ -367,10 +367,10 @@ describe('Job Class', function() { }; const job = new Job(mockQueue, index, type, payload, optionals); - return Promise.resolve(client.callWithInternalUser('get', {}, optionals)) + return Promise.resolve(client.callAsInternalUser('get', {}, optionals)) .then(doc => { sinon - .stub(client, 'callWithInternalUser') + .stub(client, 'callAsInternalUser') .withArgs('get') .returns(Promise.resolve(doc)); }) diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js index bd82a9e9f99db9..ad93a1882746dc 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js @@ -288,7 +288,7 @@ describe('Worker class', function() { describe('error handling', function() { it('should pass search errors', function(done) { searchStub = sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('search') .callsFake(() => Promise.reject()); worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); @@ -303,7 +303,7 @@ describe('Worker class', function() { describe('missing index', function() { it('should swallow error', function(done) { searchStub = sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('search') .callsFake(() => Promise.reject({ status: 404 })); worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); @@ -317,7 +317,7 @@ describe('Worker class', function() { it('should return an empty array', function(done) { searchStub = sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('search') .callsFake(() => Promise.reject({ status: 404 })); worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); @@ -343,7 +343,7 @@ describe('Worker class', function() { beforeEach(() => { searchStub = sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('search') .callsFake(() => Promise.resolve({ hits: { hits: [] } })); anchorMoment = moment(anchor); @@ -417,10 +417,10 @@ describe('Worker class', function() { type: 'test', id: 12345, }; - return mockQueue.client.callWithInternalUser('get', params).then(jobDoc => { + return mockQueue.client.callAsInternalUser('get', params).then(jobDoc => { job = jobDoc; worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); - updateSpy = sinon.spy(mockQueue.client, 'callWithInternalUser').withArgs('update'); + updateSpy = sinon.spy(mockQueue.client, 'callAsInternalUser').withArgs('update'); }); }); @@ -483,9 +483,9 @@ describe('Worker class', function() { }); it('should reject the promise on conflict errors', function() { - mockQueue.client.callWithInternalUser.restore(); + mockQueue.client.callAsInternalUser.restore(); sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .returns(Promise.reject({ statusCode: 409 })); return worker._claimJob(job).catch(err => { @@ -494,9 +494,9 @@ describe('Worker class', function() { }); it('should reject the promise on other errors', function() { - mockQueue.client.callWithInternalUser.restore(); + mockQueue.client.callAsInternalUser.restore(); sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .returns(Promise.reject({ statusCode: 401 })); return worker._claimJob(job).catch(err => { @@ -532,12 +532,12 @@ describe('Worker class', function() { }); afterEach(() => { - mockQueue.client.callWithInternalUser.restore(); + mockQueue.client.callAsInternalUser.restore(); }); it('should emit for errors from claiming job', function(done) { sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .rejects({ statusCode: 401 }); @@ -558,7 +558,7 @@ describe('Worker class', function() { it('should reject the promise if an error claiming the job', function() { sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .rejects({ statusCode: 409 }); return worker._claimPendingJobs(getMockJobs()).catch(err => { @@ -568,7 +568,7 @@ describe('Worker class', function() { it('should get the pending job', function() { sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .resolves({ test: 'cool' }); sinon.stub(worker, '_performJob').callsFake(identity); @@ -590,10 +590,10 @@ describe('Worker class', function() { anchorMoment = moment(anchor); clock = sinon.useFakeTimers(anchorMoment.valueOf()); - return mockQueue.client.callWithInternalUser('get').then(jobDoc => { + return mockQueue.client.callAsInternalUser('get').then(jobDoc => { job = jobDoc; worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); - updateSpy = sinon.spy(mockQueue.client, 'callWithInternalUser').withArgs('update'); + updateSpy = sinon.spy(mockQueue.client, 'callAsInternalUser').withArgs('update'); }); }); @@ -625,18 +625,18 @@ describe('Worker class', function() { }); it('should return true on conflict errors', function() { - mockQueue.client.callWithInternalUser.restore(); + mockQueue.client.callAsInternalUser.restore(); sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .rejects({ statusCode: 409 }); return worker._failJob(job).then(res => expect(res).to.equal(true)); }); it('should return false on other document update errors', function() { - mockQueue.client.callWithInternalUser.restore(); + mockQueue.client.callAsInternalUser.restore(); sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .rejects({ statusCode: 401 }); return worker._failJob(job).then(res => expect(res).to.equal(false)); @@ -672,9 +672,9 @@ describe('Worker class', function() { }); it('should emit on other document update errors', function(done) { - mockQueue.client.callWithInternalUser.restore(); + mockQueue.client.callAsInternalUser.restore(); sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .rejects({ statusCode: 401 }); @@ -703,9 +703,9 @@ describe('Worker class', function() { value: random(0, 100, true), }; - return mockQueue.client.callWithInternalUser('get', {}, { payload }).then(jobDoc => { + return mockQueue.client.callAsInternalUser('get', {}, { payload }).then(jobDoc => { job = jobDoc; - updateSpy = sinon.spy(mockQueue.client, 'callWithInternalUser').withArgs('update'); + updateSpy = sinon.spy(mockQueue.client, 'callAsInternalUser').withArgs('update'); }); }); @@ -871,7 +871,7 @@ describe('Worker class', function() { }; sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .rejects({ statusCode: 413 }); @@ -893,7 +893,7 @@ describe('Worker class', function() { describe('search failure', function() { it('causes _processPendingJobs to reject the Promise', function() { sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('search') .rejects(new Error('test error')); worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); @@ -996,7 +996,7 @@ describe('Worker class', function() { beforeEach(function() { sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('search') .callsFake(() => Promise.resolve({ hits: { hits: [] } })); }); @@ -1086,20 +1086,12 @@ describe('Format Job Object', () => { }); }); -// FAILING: https://github.com/elastic/kibana/issues/51372 -describe.skip('Get Doc Path from ES Response', () => { +describe('Get Doc Path from ES Response', () => { it('returns a formatted string after response of an update', function() { const responseMock = { _index: 'foo', _id: 'booId', }; - expect(getUpdatedDocPath(responseMock)).equal('/foo/_doc/booId'); - }); - it('returns the same formatted string even if there is no _doc provided', function() { - const responseMock = { - _index: 'foo', - _id: 'booId', - }; - expect(getUpdatedDocPath(responseMock)).equal('/foo/_doc/booId'); + expect(getUpdatedDocPath(responseMock)).equal('/foo/booId'); }); }); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/create_index.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/create_index.js index 670c2907fb8322..465f27a817ba75 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/create_index.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/create_index.js @@ -78,13 +78,13 @@ export function createIndex(client, indexName, indexSettings = {}) { }; return client - .callWithInternalUser('indices.exists', { + .callAsInternalUser('indices.exists', { index: indexName, }) .then(exists => { if (!exists) { return client - .callWithInternalUser('indices.create', { + .callAsInternalUser('indices.create', { index: indexName, body: body, }) diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/index.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/index.js index b42ef84168940d..bd30ca9ae0f29e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/index.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/index.js @@ -32,7 +32,7 @@ export class Esqueue extends EventEmitter { } _initTasks() { - const initTasks = [this.client.callWithInternalUser('ping')]; + const initTasks = [this.client.callAsInternalUser('ping')]; return Promise.all(initTasks).catch(err => { this._logger(['initTasks', 'error'], err); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js index a7d8f4df3fd541..826fcf360a4ca2 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js @@ -78,7 +78,7 @@ export class Job extends events.EventEmitter { } this.ready = createIndex(this._client, this.index, this.indexSettings) - .then(() => this._client.callWithInternalUser('index', indexParams)) + .then(() => this._client.callAsInternalUser('index', indexParams)) .then(doc => { this.document = { id: doc._id, @@ -89,7 +89,7 @@ export class Job extends events.EventEmitter { this.debug(`Job created in index ${this.index}`); return this._client - .callWithInternalUser('indices.refresh', { + .callAsInternalUser('indices.refresh', { index: this.index, }) .then(() => { @@ -111,7 +111,7 @@ export class Job extends events.EventEmitter { get() { return this.ready .then(() => { - return this._client.callWithInternalUser('get', { + return this._client.callAsInternalUser('get', { index: this.index, id: this.id, }); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js index 7ad84460a0c459..43735979422783 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js @@ -160,7 +160,7 @@ export class Worker extends events.EventEmitter { }; return this._client - .callWithInternalUser('update', { + .callAsInternalUser('update', { index: job._index, id: job._id, if_seq_no: job._seq_no, @@ -199,7 +199,7 @@ export class Worker extends events.EventEmitter { }); return this._client - .callWithInternalUser('update', { + .callAsInternalUser('update', { index: job._index, id: job._id, if_seq_no: job._seq_no, @@ -286,7 +286,7 @@ export class Worker extends events.EventEmitter { }; return this._client - .callWithInternalUser('update', { + .callAsInternalUser('update', { index: job._index, id: job._id, if_seq_no: job._seq_no, @@ -431,7 +431,7 @@ export class Worker extends events.EventEmitter { }; return this._client - .callWithInternalUser('search', { + .callAsInternalUser('search', { index: `${this.queue.index}-*`, body: query, }) diff --git a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts index 0c16f780c34acd..3562834230ea1d 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { errors as elasticsearchErrors } from 'elasticsearch'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { get } from 'lodash'; -import { ServerFacade, JobSource } from '../../types'; +import { JobSource, ServerFacade } from '../../types'; +const esErrors = elasticsearchErrors as Record; const defaultSize = 10; interface QueryBody { @@ -34,12 +37,9 @@ interface CountAggResult { count: number; } -export function jobsQueryFactory(server: ServerFacade) { +export function jobsQueryFactory(server: ServerFacade, elasticsearch: ElasticsearchServiceSetup) { const index = server.config().get('xpack.reporting.index'); - // @ts-ignore `errors` does not exist on type Cluster - const { callWithInternalUser, errors: esErrors } = server.plugins.elasticsearch.getCluster( - 'admin' - ); + const { callAsInternalUser } = elasticsearch.adminClient; function getUsername(user: any) { return get(user, 'username', false); @@ -61,7 +61,7 @@ export function jobsQueryFactory(server: ServerFacade) { body: Object.assign(defaultBody[queryType] || {}, body), }; - return callWithInternalUser(queryType, query).catch(err => { + return callAsInternalUser(queryType, query).catch(err => { if (err instanceof esErrors['401']) return; if (err instanceof esErrors['403']) return; if (err instanceof esErrors['404']) return; diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts index 79a64bd82d0228..028d8fa143487c 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts @@ -5,7 +5,8 @@ */ import { i18n } from '@kbn/i18n'; -import { ServerFacade, Logger } from '../../../types'; +import { ElasticsearchServiceSetup } from 'kibana/server'; +import { Logger, ServerFacade } from '../../../types'; import { HeadlessChromiumDriverFactory } from '../../browsers/chromium/driver_factory'; import { validateBrowser } from './validate_browser'; import { validateEncryptionKey } from './validate_encryption_key'; @@ -14,6 +15,7 @@ import { validateServerHost } from './validate_server_host'; export async function runValidations( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, logger: Logger, browserFactory: HeadlessChromiumDriverFactory ) { @@ -21,7 +23,7 @@ export async function runValidations( await Promise.all([ validateBrowser(server, browserFactory, logger), validateEncryptionKey(server, logger), - validateMaxContentLength(server, logger), + validateMaxContentLength(server, elasticsearch, logger), validateServerHost(server), ]); logger.debug( diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_max_content_length.js b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js similarity index 65% rename from x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_max_content_length.js rename to x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js index 48a58618f34cc7..942dcaf842696c 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_max_content_length.js +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js @@ -3,14 +3,26 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; + import sinon from 'sinon'; -import { validateMaxContentLength } from '../validate_max_content_length'; +import { validateMaxContentLength } from './validate_max_content_length'; const FIVE_HUNDRED_MEGABYTES = 524288000; const ONE_HUNDRED_MEGABYTES = 104857600; describe('Reporting: Validate Max Content Length', () => { + const elasticsearch = { + dataClient: { + callAsInternalUser: () => ({ + defaults: { + http: { + max_content_length: '100mb', + }, + }, + }), + }, + }; + const logger = { warning: sinon.spy(), }; @@ -24,22 +36,20 @@ describe('Reporting: Validate Max Content Length', () => { config: () => ({ get: sinon.stub().returns(FIVE_HUNDRED_MEGABYTES), }), - plugins: { - elasticsearch: { - getCluster: () => ({ - callWithInternalUser: () => ({ - defaults: { - http: { - max_content_length: '100mb', - }, - }, - }), - }), - }, + }; + const elasticsearch = { + dataClient: { + callAsInternalUser: () => ({ + defaults: { + http: { + max_content_length: '100mb', + }, + }, + }), }, }; - await validateMaxContentLength(server, logger); + await validateMaxContentLength(server, elasticsearch, logger); sinon.assert.calledWithMatch( logger.warning, @@ -64,22 +74,11 @@ describe('Reporting: Validate Max Content Length', () => { config: () => ({ get: sinon.stub().returns(ONE_HUNDRED_MEGABYTES), }), - plugins: { - elasticsearch: { - getCluster: () => ({ - callWithInternalUser: () => ({ - defaults: { - http: { - max_content_length: '100mb', - }, - }, - }), - }), - }, - }, }; - expect(async () => validateMaxContentLength(server, logger.warning)).not.to.throwError(); + expect( + async () => await validateMaxContentLength(server, elasticsearch, logger.warning) + ).not.toThrow(); sinon.assert.notCalled(logger.warning); }); }); diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts index f91cd40bfd3c73..ce4a5b93e74310 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts @@ -3,18 +3,24 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import numeral from '@elastic/numeral'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { defaults, get } from 'lodash'; import { Logger, ServerFacade } from '../../../types'; const KIBANA_MAX_SIZE_BYTES_PATH = 'xpack.reporting.csv.maxSizeBytes'; const ES_MAX_SIZE_BYTES_PATH = 'http.max_content_length'; -export async function validateMaxContentLength(server: ServerFacade, logger: Logger) { +export async function validateMaxContentLength( + server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, + logger: Logger +) { const config = server.config(); - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('data'); + const { callAsInternalUser } = elasticsearch.dataClient; - const elasticClusterSettingsResponse = await callWithInternalUser('cluster.getSettings', { + const elasticClusterSettingsResponse = await callAsInternalUser('cluster.getSettings', { includeDefaults: true, }); const { persistent, transient, defaults: defaultSettings } = elasticClusterSettingsResponse; diff --git a/x-pack/legacy/plugins/reporting/server/plugin.ts b/x-pack/legacy/plugins/reporting/server/plugin.ts index cf66ec74969cae..e618d23e8ed1f9 100644 --- a/x-pack/legacy/plugins/reporting/server/plugin.ts +++ b/x-pack/legacy/plugins/reporting/server/plugin.ts @@ -5,20 +5,26 @@ */ import { Legacy } from 'kibana'; -import { CoreSetup, CoreStart, Plugin, LoggerFactory } from 'src/core/server'; +import { + CoreSetup, + CoreStart, + ElasticsearchServiceSetup, + LoggerFactory, + Plugin, +} from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../../../plugins/security/server'; -import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; +import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; // @ts-ignore import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; +import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; import { PLUGIN_ID } from '../common/constants'; +import { logConfiguration } from '../log_configuration'; import { ReportingPluginSpecOptions } from '../types.d'; -import { registerRoutes } from './routes'; -import { checkLicenseFactory, getExportTypesRegistry, runValidations, LevelLogger } from './lib'; import { createBrowserDriverFactory } from './browsers'; +import { checkLicenseFactory, getExportTypesRegistry, LevelLogger, runValidations } from './lib'; +import { registerRoutes } from './routes'; import { registerReportingUsageCollector } from './usage'; -import { logConfiguration } from '../log_configuration'; -import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; export interface ReportingInitializerContext { logger: LoggerFactory; @@ -29,22 +35,16 @@ export type ReportingSetup = object; export type ReportingStart = object; export interface ReportingSetupDeps { + elasticsearch: ElasticsearchServiceSetup; usageCollection: UsageCollectionSetup; security: SecurityPluginSetup; } export type ReportingStartDeps = object; -type LegacyPlugins = Legacy.Server['plugins']; - export interface LegacySetup { config: Legacy.Server['config']; info: Legacy.Server['info']; - plugins: { - elasticsearch: LegacyPlugins['elasticsearch']; - xpack_main: XPackMainPlugin & { - status?: any; - }; - }; + plugins: { xpack_main: XPackMainPlugin & { status?: any } }; route: Legacy.Server['route']; savedObjects: Legacy.Server['savedObjects']; uiSettingsServiceFactory: Legacy.Server['uiSettingsServiceFactory']; @@ -76,10 +76,10 @@ export function reportingPluginFactory( public async setup(core: CoreSetup, plugins: ReportingSetupDeps): Promise { const exportTypesRegistry = getExportTypesRegistry(); + const { usageCollection, elasticsearch } = plugins; let isCollectorReady = false; // Register a function with server to manage the collection of usage stats - const { usageCollection } = plugins; registerReportingUsageCollector( usageCollection, __LEGACY, @@ -91,7 +91,7 @@ export function reportingPluginFactory( const browserDriverFactory = await createBrowserDriverFactory(__LEGACY, logger); logConfiguration(__LEGACY, logger); - runValidations(__LEGACY, logger, browserDriverFactory); + runValidations(__LEGACY, elasticsearch, logger, browserDriverFactory); const { xpack_main: xpackMainPlugin } = __LEGACY.plugins; mirrorPluginStatus(xpackMainPlugin, legacyPlugin); diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index f3ed760bba4302..fd1d85fef0f21b 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -36,6 +36,7 @@ export function registerGenerateCsvFromSavedObjectImmediate( parentLogger: Logger ) { const routeOptions = getRouteOptionsCsv(server, plugins, parentLogger); + const { elasticsearch } = plugins; /* * CSV export with the `immediate` option does not queue a job with Reporting's ESQueue to run the job async. Instead, this does: @@ -57,8 +58,8 @@ export function registerGenerateCsvFromSavedObjectImmediate( * * Calling an execute job factory requires passing a browserDriverFactory option, so we should not call the factory from here */ - const createJobFn = createJobFactory(server, logger); - const executeJobFn = executeJobFactory(server, logger, { + const createJobFn = createJobFactory(server, elasticsearch, logger); + const executeJobFn = executeJobFactory(server, elasticsearch, logger, { browserDriverFactory: {} as HeadlessChromiumDriverFactory, }); const jobDocPayload: JobDocPayloadPanelCsv = await createJobFn( diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.ts index 3c9ef6987b2d95..02a9541484bc63 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generation.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.ts @@ -5,6 +5,7 @@ */ import boom from 'boom'; +import { errors as elasticsearchErrors } from 'elasticsearch'; import { Legacy } from 'kibana'; import { API_BASE_URL } from '../../common/constants'; import { @@ -21,6 +22,8 @@ import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject' import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate'; import { makeRequestFacade } from './lib/make_request_facade'; +const esErrors = elasticsearchErrors as Record; + export function registerJobGenerationRoutes( server: ServerFacade, plugins: ReportingSetupDeps, @@ -30,11 +33,15 @@ export function registerJobGenerationRoutes( ) { const config = server.config(); const DOWNLOAD_BASE_URL = config.get('server.basePath') + `${API_BASE_URL}/jobs/download`; - // @ts-ignore TODO - const { errors: esErrors } = server.plugins.elasticsearch.getCluster('admin'); - - const esqueue = createQueueFactory(server, logger, { exportTypesRegistry, browserDriverFactory }); - const enqueueJob = enqueueJobFactory(server, logger, { exportTypesRegistry, esqueue }); + const { elasticsearch } = plugins; + const esqueue = createQueueFactory(server, elasticsearch, logger, { + exportTypesRegistry, + browserDriverFactory, + }); + const enqueueJob = enqueueJobFactory(server, elasticsearch, logger, { + exportTypesRegistry, + esqueue, + }); /* * Generates enqueued job details to use in responses diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js index c9d4f9fc027be3..811c81c502b812 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js @@ -43,18 +43,12 @@ beforeEach(() => { jobContentEncoding: 'base64', jobContentExtension: 'pdf', }); - mockServer.plugins = { - elasticsearch: { - getCluster: memoize(() => ({ callWithInternalUser: jest.fn() })), - createCluster: () => ({ - callWithRequest: jest.fn(), - callWithInternalUser: jest.fn(), - }), - }, - }; }); const mockPlugins = { + elasticsearch: { + adminClient: { callAsInternalUser: jest.fn() }, + }, security: null, }; @@ -67,9 +61,9 @@ const getHits = (...sources) => { }; test(`returns 404 if job not found`, async () => { - mockServer.plugins.elasticsearch - .getCluster('admin') - .callWithInternalUser.mockReturnValue(Promise.resolve(getHits())); + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(getHits())), + }; registerJobInfoRoutes(mockServer, mockPlugins, exportTypesRegistry, mockLogger); @@ -84,9 +78,11 @@ test(`returns 404 if job not found`, async () => { }); test(`returns 401 if not valid job type`, async () => { - mockServer.plugins.elasticsearch - .getCluster('admin') - .callWithInternalUser.mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' }))); + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest + .fn() + .mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' }))), + }; registerJobInfoRoutes(mockServer, mockPlugins, exportTypesRegistry, mockLogger); @@ -101,11 +97,13 @@ test(`returns 401 if not valid job type`, async () => { describe(`when job is incomplete`, () => { const getIncompleteResponse = async () => { - mockServer.plugins.elasticsearch - .getCluster('admin') - .callWithInternalUser.mockReturnValue( - Promise.resolve(getHits({ jobtype: 'unencodedJobType', status: 'pending' })) - ); + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest + .fn() + .mockReturnValue( + Promise.resolve(getHits({ jobtype: 'unencodedJobType', status: 'pending' })) + ), + }; registerJobInfoRoutes(mockServer, mockPlugins, exportTypesRegistry, mockLogger); @@ -145,9 +143,9 @@ describe(`when job is failed`, () => { status: 'failed', output: { content: 'job failure message' }, }); - mockServer.plugins.elasticsearch - .getCluster('admin') - .callWithInternalUser.mockReturnValue(Promise.resolve(hits)); + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; registerJobInfoRoutes(mockServer, mockPlugins, exportTypesRegistry, mockLogger); @@ -190,9 +188,9 @@ describe(`when job is completed`, () => { title, }, }); - mockServer.plugins.elasticsearch - .getCluster('admin') - .callWithInternalUser.mockReturnValue(Promise.resolve(hits)); + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; registerJobInfoRoutes(mockServer, mockPlugins, exportTypesRegistry, mockLogger); diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts index f9b731db5a702b..daabc2cf22f4e2 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts @@ -38,7 +38,8 @@ export function registerJobInfoRoutes( exportTypesRegistry: ExportTypesRegistry, logger: Logger ) { - const jobsQuery = jobsQueryFactory(server); + const { elasticsearch } = plugins; + const jobsQuery = jobsQueryFactory(server, elasticsearch); const getRouteConfig = getRouteConfigFactoryManagementPre(server, plugins, logger); const getRouteConfigDownload = getRouteConfigFactoryDownloadPre(server, plugins, logger); @@ -137,7 +138,7 @@ export function registerJobInfoRoutes( }); // trigger a download of the output from a job - const jobResponseHandler = jobResponseHandlerFactory(server, exportTypesRegistry); + const jobResponseHandler = jobResponseHandlerFactory(server, elasticsearch, exportTypesRegistry); server.route({ path: `${MAIN_ENTRY}/download/{docId}`, method: 'GET', diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts index 3ba7aa30eedcb7..62f0d0a72b389a 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -5,10 +5,11 @@ */ import Boom from 'boom'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { ResponseToolkit } from 'hapi'; -import { ServerFacade, ExportTypesRegistry } from '../../../types'; -import { jobsQueryFactory } from '../../lib/jobs_query'; import { WHITELISTED_JOB_CONTENT_TYPES } from '../../../common/constants'; +import { ExportTypesRegistry, ServerFacade } from '../../../types'; +import { jobsQueryFactory } from '../../lib/jobs_query'; import { getDocumentPayloadFactory } from './get_document_payload'; interface JobResponseHandlerParams { @@ -21,9 +22,10 @@ interface JobResponseHandlerOpts { export function jobResponseHandlerFactory( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, exportTypesRegistry: ExportTypesRegistry ) { - const jobsQuery = jobsQueryFactory(server); + const jobsQuery = jobsQueryFactory(server, elasticsearch); const getDocumentPayload = getDocumentPayloadFactory(server, exportTypesRegistry); return function jobResponseHandler( diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index 9ba016d8b828d8..a4ff39b23747dd 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -4,15 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ResponseObject } from 'hapi'; import { EventEmitter } from 'events'; +import { ResponseObject } from 'hapi'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { Legacy } from 'kibana'; import { CallCluster } from '../../../../src/legacy/core_plugins/elasticsearch'; import { CancellationToken } from './common/cancellation_token'; -import { LevelLogger } from './server/lib/level_logger'; import { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory'; import { BrowserType } from './server/browsers/types'; -import { LegacySetup } from './server/plugin'; +import { LevelLogger } from './server/lib/level_logger'; +import { LegacySetup, ReportingSetupDeps } from './server/plugin'; export type ReportingPlugin = object; // For Plugin contract @@ -276,10 +277,12 @@ export interface ESQueueInstance { export type CreateJobFactory = ( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, logger: LevelLogger ) => CreateJobFnType; export type ExecuteJobFactory = ( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, logger: LevelLogger, opts: { browserDriverFactory: HeadlessChromiumDriverFactory; @@ -302,10 +305,10 @@ export interface ExportTypeDefinition< validLicenses: string[]; } -export { ExportTypesRegistry } from './server/lib/export_types_registry'; +export { CancellationToken } from './common/cancellation_token'; export { HeadlessChromiumDriver } from './server/browsers/chromium/driver'; export { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory'; -export { CancellationToken } from './common/cancellation_token'; +export { ExportTypesRegistry } from './server/lib/export_types_registry'; // Prefer to import this type using: `import { LevelLogger } from 'relative/path/server/lib';` export { LevelLogger as Logger }; diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index cd9b7f59226b0b..0a3e447ac64a1c 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { get } from 'lodash/fp'; import { resolve } from 'path'; import { Server } from 'hapi'; import { Root } from 'joi'; @@ -155,6 +156,9 @@ export const siem = (kibana: any) => { const initializerContext = { ...coreContext, env } as PluginInitializerContext; const serverFacade = { config, + usingEphemeralEncryptionKey: + get('usingEphemeralEncryptionKey', newPlatform.setup.plugins.encryptedSavedObjects) ?? + false, plugins: { alerting: plugins.alerting, actions: newPlatform.start.plugins.actions, diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx index 14d40f9ffbc37a..d77d6283692a24 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -29,6 +29,7 @@ interface UsePrePackagedRuleProps { hasIndexWrite: boolean | null; hasManageApiKey: boolean | null; isAuthenticated: boolean | null; + hasEncryptionKey: boolean | null; isSignalIndexExists: boolean | null; } @@ -38,6 +39,7 @@ interface UsePrePackagedRuleProps { * @param hasIndexWrite boolean * @param hasManageApiKey boolean * @param isAuthenticated boolean + * @param hasEncryptionKey boolean * @param isSignalIndexExists boolean * */ @@ -46,6 +48,7 @@ export const usePrePackagedRules = ({ hasIndexWrite, hasManageApiKey, isAuthenticated, + hasEncryptionKey, isSignalIndexExists, }: UsePrePackagedRuleProps): Return => { const [rulesStatus, setRuleStatus] = useState< @@ -117,6 +120,7 @@ export const usePrePackagedRules = ({ hasIndexWrite && hasManageApiKey && isAuthenticated && + hasEncryptionKey && isSignalIndexExists ) { setLoadingCreatePrePackagedRules(true); @@ -180,7 +184,14 @@ export const usePrePackagedRules = ({ isSubscribed = false; abortCtrl.abort(); }; - }, [canUserCRUD, hasIndexWrite, hasManageApiKey, isAuthenticated, isSignalIndexExists]); + }, [ + canUserCRUD, + hasIndexWrite, + hasManageApiKey, + isAuthenticated, + hasEncryptionKey, + isSignalIndexExists, + ]); return { loading, diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts index ea4860dafd40f2..752de13567e5c8 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts @@ -97,4 +97,5 @@ export interface Privilege { }; }; is_authenticated: boolean; + has_encryption_key: boolean; } diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx index b93009c8ce2c28..55f3386b503d88 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx @@ -14,6 +14,7 @@ import * as i18n from './translations'; interface Return { loading: boolean; isAuthenticated: boolean | null; + hasEncryptionKey: boolean | null; hasIndexManage: boolean | null; hasManageApiKey: boolean | null; hasIndexWrite: boolean | null; @@ -25,9 +26,17 @@ interface Return { export const usePrivilegeUser = (): Return => { const [loading, setLoading] = useState(true); const [privilegeUser, setPrivilegeUser] = useState< - Pick + Pick< + Return, + | 'isAuthenticated' + | 'hasEncryptionKey' + | 'hasIndexManage' + | 'hasManageApiKey' + | 'hasIndexWrite' + > >({ isAuthenticated: null, + hasEncryptionKey: null, hasIndexManage: null, hasManageApiKey: null, hasIndexWrite: null, @@ -50,6 +59,7 @@ export const usePrivilegeUser = (): Return => { const indexName = Object.keys(privilege.index)[0]; setPrivilegeUser({ isAuthenticated: privilege.is_authenticated, + hasEncryptionKey: privilege.has_encryption_key, hasIndexManage: privilege.index[indexName].manage, hasIndexWrite: privilege.index[indexName].create || @@ -67,6 +77,7 @@ export const usePrivilegeUser = (): Return => { if (isSubscribed) { setPrivilegeUser({ isAuthenticated: false, + hasEncryptionKey: false, hasIndexManage: false, hasManageApiKey: false, hasIndexWrite: false, diff --git a/x-pack/legacy/plugins/siem/public/pages/common/translations.ts b/x-pack/legacy/plugins/siem/public/pages/common/translations.ts index 3e203383756163..072aee50d5136b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/common/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/common/translations.ts @@ -12,7 +12,7 @@ export const EMPTY_TITLE = i18n.translate('xpack.siem.pages.common.emptyTitle', export const EMPTY_MESSAGE = i18n.translate('xpack.siem.pages.common.emptyMessage', { defaultMessage: - 'To begin using security information and event management, you’ll need to begin adding SIEM-related data to Kibana by installing and configuring our data shippers, called Beats. Let’s do that now!', + 'To begin using security information and event management (SIEM), you’ll need to add SIEM-related data, in Elastic Common Schema (ECS) format, to the Elastic Stack. An easy way to get started is by installing and configuring our data shippers, called Beats. Let’s do that now!', }); export const EMPTY_ACTION_PRIMARY = i18n.translate('xpack.siem.pages.common.emptyActionPrimary', { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx new file mode 100644 index 00000000000000..2d517717ac59d5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx @@ -0,0 +1,26 @@ +/* + * 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 { EuiCallOut, EuiButton } from '@elastic/eui'; +import React, { memo, useCallback, useState } from 'react'; + +import * as i18n from './translations'; + +const NoApiIntegrationKeyCallOutComponent = () => { + const [showCallOut, setShowCallOut] = useState(true); + const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]); + + return showCallOut ? ( + +

{i18n.NO_API_INTEGRATION_KEY_CALLOUT_MSG}

+ + {i18n.DISMISS_CALLOUT} + +
+ ) : null; +}; + +export const NoApiIntegrationKeyCallOut = memo(NoApiIntegrationKeyCallOutComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts new file mode 100644 index 00000000000000..84804af8840f91 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const NO_API_INTEGRATION_KEY_CALLOUT_TITLE = i18n.translate( + 'xpack.siem.detectionEngine.noApiIntegrationKeyCallOutTitle', + { + defaultMessage: 'API integration key required', + } +); + +export const NO_API_INTEGRATION_KEY_CALLOUT_MSG = i18n.translate( + 'xpack.siem.detectionEngine.noApiIntegrationKeyCallOutMsg', + { + defaultMessage: `A new encryption key is generated for saved objects each time you start Kibana. Without a persistent key, you cannot delete or modify rules after Kibana restarts. To set a persistent key, add the xpack.encryptedSavedObjects.encryptionKey setting with any text value of 32 or more characters to the kibana.yml file.`, + } +); + +export const DISMISS_CALLOUT = i18n.translate( + 'xpack.siem.detectionEngine.dismissNoApiIntegrationKeyButton', + { + defaultMessage: 'Dismiss', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx index a33efeda2196b5..003d2baa53dbc1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx @@ -5,7 +5,7 @@ */ import dateMath from '@elastic/datemath'; -import { getOr } from 'lodash/fp'; +import { getOr, isEmpty } from 'lodash/fp'; import moment from 'moment'; import { updateSignalStatus } from '../../../../containers/detection_engine/signals/api'; @@ -78,6 +78,7 @@ export const sendSignalToTimelineAction = async ({ ecsData, updateTimelineIsLoading, }: SendSignalToTimelineActionProps) => { + let openSignalInBasicTimeline = true; const timelineId = ecsData.signal?.rule?.timeline_id != null ? ecsData.signal?.rule?.timeline_id[0] : ''; @@ -105,52 +106,57 @@ export const sendSignalToTimelineAction = async ({ id: timelineId, }, }); - const timelineTemplate: TimelineResult = omitTypenameInTimeline( getOr({}, 'data.getOneTimeline', responseTimeline) ); - const { timeline } = formatTimelineResultToModel(timelineTemplate, true); - const query = replaceTemplateFieldFromQuery( - timeline.kqlQuery?.filterQuery?.kuery?.expression ?? '', - ecsData - ); - const filters = replaceTemplateFieldFromMatchFilters(timeline.filters ?? [], ecsData); - const dataProviders = replaceTemplateFieldFromDataProviders( - timeline.dataProviders ?? [], - ecsData - ); - createTimeline({ - from, - timeline: { - ...timeline, - dataProviders, - eventType: 'all', - filters, - dateRange: { - start: from, - end: to, - }, - kqlQuery: { - filterQuery: { - kuery: { + if (!isEmpty(timelineTemplate)) { + openSignalInBasicTimeline = false; + const { timeline } = formatTimelineResultToModel(timelineTemplate, true); + const query = replaceTemplateFieldFromQuery( + timeline.kqlQuery?.filterQuery?.kuery?.expression ?? '', + ecsData + ); + const filters = replaceTemplateFieldFromMatchFilters(timeline.filters ?? [], ecsData); + const dataProviders = replaceTemplateFieldFromDataProviders( + timeline.dataProviders ?? [], + ecsData + ); + createTimeline({ + from, + timeline: { + ...timeline, + dataProviders, + eventType: 'all', + filters, + dateRange: { + start: from, + end: to, + }, + kqlQuery: { + filterQuery: { + kuery: { + kind: timeline.kqlQuery?.filterQuery?.kuery?.kind ?? 'kuery', + expression: query, + }, + serializedQuery: convertKueryToElasticSearchQuery(query), + }, + filterQueryDraft: { kind: timeline.kqlQuery?.filterQuery?.kuery?.kind ?? 'kuery', expression: query, }, - serializedQuery: convertKueryToElasticSearchQuery(query), - }, - filterQueryDraft: { - kind: timeline.kqlQuery?.filterQuery?.kuery?.kind ?? 'kuery', - expression: query, }, + show: true, }, - show: true, - }, - to, - }); + to, + }); + } } catch { + openSignalInBasicTimeline = true; updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); } - } else { + } + + if (openSignalInBasicTimeline) { createTimeline({ from, timeline: { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx index 0f6a51e52cd2e8..a96913f2ad541f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx @@ -18,6 +18,7 @@ export interface State { hasManageApiKey: boolean | null; isSignalIndexExists: boolean | null; isAuthenticated: boolean | null; + hasEncryptionKey: boolean | null; loading: boolean; signalIndexName: string | null; } @@ -29,6 +30,7 @@ const initialState: State = { hasManageApiKey: null, isSignalIndexExists: null, isAuthenticated: null, + hasEncryptionKey: null, loading: true, signalIndexName: null, }; @@ -55,6 +57,10 @@ export type Action = type: 'updateIsAuthenticated'; isAuthenticated: boolean | null; } + | { + type: 'updateHasEncryptionKey'; + hasEncryptionKey: boolean | null; + } | { type: 'updateCanUserCRUD'; canUserCRUD: boolean | null; @@ -102,6 +108,12 @@ export const userInfoReducer = (state: State, action: Action): State => { isAuthenticated: action.isAuthenticated, }; } + case 'updateHasEncryptionKey': { + return { + ...state, + hasEncryptionKey: action.hasEncryptionKey, + }; + } case 'updateCanUserCRUD': { return { ...state, @@ -142,6 +154,7 @@ export const useUserInfo = (): State => { hasManageApiKey, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, loading, signalIndexName, }, @@ -150,6 +163,7 @@ export const useUserInfo = (): State => { const { loading: privilegeLoading, isAuthenticated: isApiAuthenticated, + hasEncryptionKey: isApiEncryptionKey, hasIndexManage: hasApiIndexManage, hasIndexWrite: hasApiIndexWrite, hasManageApiKey: hasApiManageApiKey, @@ -205,6 +219,12 @@ export const useUserInfo = (): State => { } }, [loading, isAuthenticated, isApiAuthenticated]); + useEffect(() => { + if (!loading && hasEncryptionKey !== isApiEncryptionKey && isApiEncryptionKey != null) { + dispatch({ type: 'updateHasEncryptionKey', hasEncryptionKey: isApiEncryptionKey }); + } + }, [loading, hasEncryptionKey, isApiEncryptionKey]); + useEffect(() => { if (!loading && canUserCRUD !== capabilitiesCanUserCRUD && capabilitiesCanUserCRUD != null) { dispatch({ type: 'updateCanUserCRUD', canUserCRUD: capabilitiesCanUserCRUD }); @@ -220,6 +240,7 @@ export const useUserInfo = (): State => { useEffect(() => { if ( isAuthenticated && + hasEncryptionKey && hasIndexManage && isSignalIndexExists != null && !isSignalIndexExists && @@ -227,12 +248,13 @@ export const useUserInfo = (): State => { ) { createSignalIndex(); } - }, [createSignalIndex, isAuthenticated, isSignalIndexExists, hasIndexManage]); + }, [createSignalIndex, isAuthenticated, hasEncryptionKey, isSignalIndexExists, hasIndexManage]); return { loading, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, canUserCRUD, hasIndexManage, hasIndexWrite, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index b6ddb4de9fd390..d854c377e6ec86 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -18,7 +18,10 @@ import { GlobalTime } from '../../containers/global_time'; import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; import { AlertsTable } from '../../components/alerts_viewer/alerts_table'; import { FiltersGlobal } from '../../components/filters_global'; -import { DETECTION_ENGINE_PAGE_NAME } from '../../components/link_to/redirect_to_detection_engine'; +import { + getDetectionEngineTabUrl, + getRulesUrl, +} from '../../components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../components/search_bar'; import { WrapperPage } from '../../components/wrapper_page'; import { State } from '../../store'; @@ -30,6 +33,7 @@ import { InputsRange } from '../../store/inputs/model'; import { AlertsByCategory } from '../overview/alerts_by_category'; import { useSignalInfo } from './components/signals_info'; import { SignalsTable } from './components/signals'; +import { NoApiIntegrationKeyCallOut } from './components/no_api_integration_callout'; import { NoWriteSignalsCallOut } from './components/no_write_signals_callout'; import { SignalsHistogramPanel } from './components/signals_histogram_panel'; import { signalsHistogramOptions } from './components/signals_histogram_panel/config'; @@ -79,6 +83,7 @@ const DetectionEnginePageComponent: React.FC loading, isSignalIndexExists, isAuthenticated: isUserAuthenticated, + hasEncryptionKey, canUserCRUD, signalIndexName, hasIndexWrite, @@ -101,7 +106,7 @@ const DetectionEnginePageComponent: React.FC isSelected={tab.id === tabName} disabled={tab.disabled} key={tab.id} - href={`#/${DETECTION_ENGINE_PAGE_NAME}/${tab.id}`} + href={getDetectionEngineTabUrl(tab.id)} > {tab.name} @@ -134,6 +139,7 @@ const DetectionEnginePageComponent: React.FC return ( <> + {hasEncryptionKey != null && !hasEncryptionKey && } {hasIndexWrite != null && !hasIndexWrite && } {({ indicesExist, indexPattern }) => { @@ -155,7 +161,7 @@ const DetectionEnginePageComponent: React.FC } title={i18n.PAGE_TITLE} > - + {i18n.BUTTON_MANAGE_RULES} @@ -184,7 +190,7 @@ const DetectionEnginePageComponent: React.FC Cancel diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx index 91b2ee283609fa..9a68797aea79b2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx @@ -90,6 +90,11 @@ export const ImportRuleModalComponent = ({ } }, [selectedFiles, overwrite]); + const handleCloseModal = useCallback(() => { + setSelectedFiles(null); + closeModal(); + }, [closeModal]); + return ( <> {showModal && ( @@ -125,7 +130,7 @@ export const ImportRuleModalComponent = ({ - {i18n.CANCEL_BUTTON} + {i18n.CANCEL_BUTTON} = ({ min: 0, fullWidth: false, disabled: isLoading, - options: severityOptions, showTicks: true, tickInterval: 25, }, @@ -239,8 +238,13 @@ const StepAboutRuleComponent: FC = ({ {({ severity }) => { const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue]; + const severityField = form.getFields().severity; const riskScoreField = form.getFields().riskScore; - if (newRiskScore != null && riskScoreField.value !== newRiskScore) { + if ( + severityField.value !== severity && + newRiskScore != null && + riskScoreField.value !== newRiskScore + ) { riskScoreField.setValue(newRiskScore); } return null; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index 55b838077988cc..3adc22329ac4f6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -23,6 +23,7 @@ import { StepDefineRule } from '../components/step_define_rule'; import { StepScheduleRule } from '../components/step_schedule_rule'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; import * as RuleI18n from '../translations'; +import { redirectToDetections } from '../helpers'; import { AboutStepRule, DefineStepRule, RuleStep, RuleStepData, ScheduleStepRule } from '../types'; import { formatRule } from './helpers'; import * as i18n from './translations'; @@ -69,6 +70,7 @@ const CreateRulePageComponent: React.FC = () => { loading, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, canUserCRUD, hasManageApiKey, } = useUserInfo(); @@ -239,11 +241,7 @@ const CreateRulePageComponent: React.FC = () => { return ; } - if ( - isSignalIndexExists != null && - isAuthenticated != null && - (!isSignalIndexExists || !isAuthenticated) - ) { + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { return ; } else if (userHasNoPermissions) { return ; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index 7b615d5f159c2d..bac1494c4fdd81 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -25,9 +25,9 @@ import { connect } from 'react-redux'; import { FiltersGlobal } from '../../../../components/filters_global'; import { FormattedDate } from '../../../../components/formatted_date'; import { - getDetectionEngineUrl, getEditRuleUrl, getRulesUrl, + DETECTION_ENGINE_PAGE_NAME, } from '../../../../components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../../../components/search_bar'; import { WrapperPage } from '../../../../components/wrapper_page'; @@ -54,7 +54,7 @@ import * as detectionI18n from '../../translations'; import { ReadOnlyCallOut } from '../components/read_only_callout'; import { RuleSwitch } from '../components/rule_switch'; import { StepPanel } from '../components/step_panel'; -import { getStepsData } from '../helpers'; +import { getStepsData, redirectToDetections } from '../helpers'; import * as ruleI18n from '../translations'; import * as i18n from './translations'; import { GlobalTime } from '../../../../containers/global_time'; @@ -113,6 +113,7 @@ const RuleDetailsPageComponent: FC = ({ loading, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, canUserCRUD, hasManageApiKey, hasIndexWrite, @@ -236,12 +237,8 @@ const RuleDetailsPageComponent: FC = ({ [ruleEnabled, setRuleEnabled] ); - if ( - isSignalIndexExists != null && - isAuthenticated != null && - (!isSignalIndexExists || !isAuthenticated) - ) { - return ; + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { + return ; } return ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index 65f4bd2edf7cd0..99fcff6b8d2fda 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -32,7 +32,7 @@ import { StepAboutRule } from '../components/step_about_rule'; import { StepDefineRule } from '../components/step_define_rule'; import { StepScheduleRule } from '../components/step_schedule_rule'; import { formatRule } from '../create/helpers'; -import { getStepsData } from '../helpers'; +import { getStepsData, redirectToDetections } from '../helpers'; import * as ruleI18n from '../translations'; import { RuleStep, DefineStepRule, AboutStepRule, ScheduleStepRule } from '../types'; import * as i18n from './translations'; @@ -56,6 +56,7 @@ const EditRulePageComponent: FC = () => { loading: initLoading, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, canUserCRUD, hasManageApiKey, } = useUserInfo(); @@ -270,11 +271,7 @@ const EditRulePageComponent: FC = () => { return ; } - if ( - isSignalIndexExists != null && - isAuthenticated != null && - (!isSignalIndexExists || !isAuthenticated) - ) { + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { return ; } else if (userHasNoPermissions) { return ; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index ce0d50d9b61065..4e98fc17404c92 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -138,3 +138,13 @@ export const setFieldValue = ( form.setFieldValue(key, val); } }); + +export const redirectToDetections = ( + isSignalIndexExists: boolean | null, + isAuthenticated: boolean | null, + hasEncryptionKey: boolean | null +) => + isSignalIndexExists != null && + isAuthenticated != null && + hasEncryptionKey != null && + (!isSignalIndexExists || !isAuthenticated || !hasEncryptionKey); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx index 1c0ed34e927938..0c53ad19a35747 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx @@ -10,6 +10,7 @@ import { Redirect } from 'react-router-dom'; import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; import { + DETECTION_ENGINE_PAGE_NAME, getDetectionEngineUrl, getCreateRuleUrl, } from '../../../components/link_to/redirect_to_detection_engine'; @@ -22,7 +23,7 @@ import { AllRules } from './all'; import { ImportRuleModal } from './components/import_rule_modal'; import { ReadOnlyCallOut } from './components/read_only_callout'; import { UpdatePrePackagedRulesCallOut } from './components/pre_packaged_rules/update_callout'; -import { getPrePackagedRuleStatus } from './helpers'; +import { getPrePackagedRuleStatus, redirectToDetections } from './helpers'; import * as i18n from './translations'; type Func = () => void; @@ -35,6 +36,7 @@ const RulesPageComponent: React.FC = () => { loading, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, canUserCRUD, hasIndexWrite, hasManageApiKey, @@ -54,6 +56,7 @@ const RulesPageComponent: React.FC = () => { hasManageApiKey, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, }); const prePackagedRuleStatus = getPrePackagedRuleStatus( rulesInstalled, @@ -83,12 +86,8 @@ const RulesPageComponent: React.FC = () => { refreshRulesData.current = refreshRule; }, []); - if ( - isSignalIndexExists != null && - isAuthenticated != null && - (!isSignalIndexExists || !isAuthenticated) - ) { - return ; + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { + return ; } return ( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index eea25a1e89cc84..6a42aed123fa3c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -390,6 +390,7 @@ export const getMockPrivileges = () => ({ }, application: {}, is_authenticated: false, + has_encryption_key: true, }); export const getFindResultStatus = (): SavedObjectsFindResponse => ({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 803d9d645aadbe..5ea4dc7595b2b1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -29,8 +29,10 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve const callWithRequest = callWithRequestFactory(request, server); const index = getIndex(request, server); const permissions = await readPrivileges(callWithRequest, index); + const usingEphemeralEncryptionKey = server.usingEphemeralEncryptionKey; return merge(permissions, { is_authenticated: request?.auth?.isAuthenticated ?? false, + has_encryption_key: !usingEphemeralEncryptionKey, }); } catch (err) { return transformError(err); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts index b9ff2e60186242..ce624693428837 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -36,28 +36,32 @@ export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = return headers.response().code(404); } - const exportSizeLimit = server.config().get('savedObjects.maxImportExportSize'); - if (request.payload?.objects != null && request.payload.objects.length > exportSizeLimit) { - return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`); - } else { - const nonPackagedRulesCount = await getNonPackagedRulesCount({ alertsClient }); - if (nonPackagedRulesCount > exportSizeLimit) { + try { + const exportSizeLimit = server.config().get('savedObjects.maxImportExportSize'); + if (request.payload?.objects != null && request.payload.objects.length > exportSizeLimit) { return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`); + } else { + const nonPackagedRulesCount = await getNonPackagedRulesCount({ alertsClient }); + if (nonPackagedRulesCount > exportSizeLimit) { + return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`); + } } - } - const exported = - request.payload?.objects != null - ? await getExportByObjectIds(alertsClient, request.payload.objects) - : await getExportAll(alertsClient); + const exported = + request.payload?.objects != null + ? await getExportByObjectIds(alertsClient, request.payload.objects) + : await getExportAll(alertsClient); - const response = request.query.exclude_export_details - ? headers.response(exported.rulesNdjson) - : headers.response(`${exported.rulesNdjson}${exported.exportDetails}`); + const response = request.query.exclude_export_details + ? headers.response(exported.rulesNdjson) + : headers.response(`${exported.rulesNdjson}${exported.exportDetails}`); - return response - .header('Content-Disposition', `attachment; filename="${request.query.file_name}"`) - .header('Content-Type', 'application/ndjson'); + return response + .header('Content-Disposition', `attachment; filename="${request.query.file_name}"`) + .header('Content-Type', 'application/ndjson'); + } catch { + return Boom.badRequest(`Sorry, something went wrong to export rules`); + } }, }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 71fdef3623bc71..0d57f5739fc15a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -6,8 +6,9 @@ import Boom from 'boom'; import Hapi from 'hapi'; +import { chunk, isEmpty, isFunction } from 'lodash/fp'; import { extname } from 'path'; -import { isFunction } from 'lodash/fp'; +import uuid from 'uuid'; import { createPromiseFromStreams } from '../../../../../../../../../src/legacy/utils/streams'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { createRules } from '../../rules/create_rules'; @@ -18,17 +19,25 @@ import { getIndexExists } from '../../index/get_index_exists'; import { callWithRequestFactory, getIndex, - createImportErrorObject, - transformImportError, - ImportSuccessError, + createBulkErrorObject, + ImportRuleResponse, } from '../utils'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { ImportRuleAlertRest } from '../../types'; -import { transformOrImportError } from './utils'; import { updateRules } from '../../rules/update_rules'; import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema'; import { KibanaRequest } from '../../../../../../../../../src/core/server'; +type PromiseFromStreams = ImportRuleAlertRest | Error; + +/* + * We were getting some error like that possible EventEmitter memory leak detected + * So we decide to batch the update by 10 to avoid any complication in the node side + * https://nodejs.org/docs/latest/api/events.html#events_emitter_setmaxlisteners_n + * + */ +const CHUNK_PARSED_OBJECT_SIZE = 10; + export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { return { method: 'POST', @@ -67,145 +76,189 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = const objectLimit = server.config().get('savedObjects.maxImportExportSize'); const readStream = createRulesStreamFromNdJson(request.payload.file, objectLimit); - const parsedObjects = await createPromiseFromStreams<[ImportRuleAlertRest | Error]>([ - readStream, - ]); - - const reduced = await parsedObjects.reduce>( - async (accum, parsedRule) => { - const existingImportSuccessError = await accum; - if (parsedRule instanceof Error) { - // If the JSON object had a validation or parse error then we return - // early with the error and an (unknown) for the ruleId - return createImportErrorObject({ - ruleId: '(unknown)', // TODO: Better handling where we know which ruleId is having issues with imports - statusCode: 400, - message: parsedRule.message, - existingImportSuccessError, - }); - } + const parsedObjects = await createPromiseFromStreams([readStream]); - const { - description, - enabled, - false_positives: falsePositives, - from, - immutable, - query, - language, - output_index: outputIndex, - saved_id: savedId, - meta, - filters, - rule_id: ruleId, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - name, - severity, - tags, - threat, - to, - type, - references, - timeline_id: timelineId, - timeline_title: timelineTitle, - version, - } = parsedRule; - try { - const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, finalIndex); - if (!indexExists) { - return createImportErrorObject({ - ruleId, - statusCode: 409, - message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, - existingImportSuccessError, - }); - } - const rule = await readRules({ alertsClient, ruleId }); - if (rule == null) { - const createdRule = await createRules({ - alertsClient, - actionsClient, - description, - enabled, - falsePositives, - from, - immutable, - query, - language, - outputIndex: finalIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - ruleId, - index, - interval, - maxSignals, - riskScore, - name, - severity, - tags, - to, - type, - threat, - references, - version, - }); - return transformOrImportError(ruleId, createdRule, existingImportSuccessError); - } else if (rule != null && request.query.overwrite) { - const updatedRule = await updateRules({ - alertsClient, - actionsClient, - savedObjectsClient, - description, - enabled, - falsePositives, - from, - immutable, - query, - language, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - id: undefined, - ruleId, - index, - interval, - maxSignals, - riskScore, - name, - severity, - tags, - to, - type, - threat, - references, - version, - }); - return transformOrImportError(ruleId, updatedRule, existingImportSuccessError); - } else { - return existingImportSuccessError; - } - } catch (err) { - return transformImportError(ruleId, err, existingImportSuccessError); - } - }, - Promise.resolve({ - success: true, - success_count: 0, - errors: [], - }) + const uniqueParsedObjects = Array.from( + parsedObjects + .reduce( + (acc, parsedRule) => { + if (parsedRule instanceof Error) { + acc.set(uuid.v4(), parsedRule); + } else { + const { rule_id: ruleId } = parsedRule; + if (ruleId != null) { + acc.set(ruleId, parsedRule); + } else { + acc.set(uuid.v4(), parsedRule); + } + } + return acc; + }, // using map (preserves ordering) + new Map() + ) + .values() ); - return reduced; + + const chunkParseObjects = chunk(CHUNK_PARSED_OBJECT_SIZE, uniqueParsedObjects); + let importRuleResponse: ImportRuleResponse[] = []; + + while (chunkParseObjects.length) { + const batchParseObjects = chunkParseObjects.shift() ?? []; + const newImportRuleResponse = await Promise.all( + batchParseObjects.reduce>>((accum, parsedRule) => { + const importsWorkerPromise = new Promise( + async (resolve, reject) => { + if (parsedRule instanceof Error) { + // If the JSON object had a validation or parse error then we return + // early with the error and an (unknown) for the ruleId + resolve( + createBulkErrorObject({ + ruleId: '(unknown)', + statusCode: 400, + message: parsedRule.message, + }) + ); + return null; + } + const { + description, + false_positives: falsePositives, + from, + immutable, + query, + language, + output_index: outputIndex, + saved_id: savedId, + meta, + filters, + rule_id: ruleId, + index, + interval, + max_signals: maxSignals, + risk_score: riskScore, + name, + severity, + tags, + threat, + to, + type, + references, + timeline_id: timelineId, + timeline_title: timelineTitle, + version, + } = parsedRule; + try { + const finalIndex = getIndex(request, server); + const callWithRequest = callWithRequestFactory(request, server); + const indexExists = await getIndexExists(callWithRequest, finalIndex); + if (!indexExists) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: 409, + message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, + }) + ); + } + const rule = await readRules({ alertsClient, ruleId }); + if (rule == null) { + await createRules({ + alertsClient, + actionsClient, + description, + enabled: false, + falsePositives, + from, + immutable, + query, + language, + outputIndex: finalIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + }); + resolve({ rule_id: ruleId, status_code: 200 }); + } else if (rule != null && request.query.overwrite) { + await updateRules({ + alertsClient, + actionsClient, + savedObjectsClient, + description, + enabled: false, + falsePositives, + from, + immutable, + query, + language, + outputIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + id: undefined, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + }); + resolve({ rule_id: ruleId, status_code: 200 }); + } else if (rule != null) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: 409, + message: `This Rule "${rule.name}" already exists`, + }) + ); + } + } catch (err) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: 400, + message: err.message, + }) + ); + } + } + ); + return [...accum, importsWorkerPromise]; + }, []) + ); + importRuleResponse = [...importRuleResponse, ...newImportRuleResponse]; + } + + const errorsResp = importRuleResponse.filter(resp => !isEmpty(resp.error)); + return { + success: errorsResp.length === 0, + success_count: importRuleResponse.filter(resp => resp.status_code === 200).length, + errors: errorsResp, + }; }, }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index f51cea0753f1ab..590307e06a26a9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -39,8 +39,8 @@ export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = language, output_index: outputIndex, saved_id: savedId, - timeline_id: timelineId, - timeline_title: timelineTitle, + timeline_id: timelineId = null, + timeline_title: timelineTitle = null, meta, filters, rule_id: ruleId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 19cd972b60e1a8..416c76b5d4eb5d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -52,6 +52,16 @@ export const createBulkErrorObject = ({ }; }; +export interface ImportRuleResponse { + rule_id: string; + status_code?: number; + message?: string; + error?: { + status_code: number; + message: string; + }; +} + export interface ImportSuccessError { success: boolean; success_count: number; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 05e455efb3f22e..236d04acc782b8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -66,6 +66,7 @@ describe('get_export_by_object_ids', () => { const objects = [{ rule_id: 'rule-1' }]; const exports = await getRulesFromObjects(unsafeCast, objects); const expected: RulesErrors = { + exportedCount: 1, missingRules: [], rules: [ { @@ -141,6 +142,7 @@ describe('get_export_by_object_ids', () => { const objects = [{ rule_id: 'rule-1' }]; const exports = await getRulesFromObjects(unsafeCast, objects); const expected: RulesErrors = { + exportedCount: 0, missingRules: [{ rule_id: 'rule-1' }], rules: [], }; @@ -164,6 +166,7 @@ describe('get_export_by_object_ids', () => { const objects = [{ rule_id: 'rule-1' }]; const exports = await getRulesFromObjects(unsafeCast, objects); const expected: RulesErrors = { + exportedCount: 0, missingRules: [{ rule_id: 'rule-1' }], rules: [], }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts index a5cf1bbfb78588..7e0d61d040617e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts @@ -11,7 +11,20 @@ import { readRules } from './read_rules'; import { transformRulesToNdjson, transformAlertToRule } from '../routes/rules/utils'; import { OutputRuleAlertRest } from '../types'; +interface ExportSuccesRule { + statusCode: 200; + rule: Partial; +} + +interface ExportFailedRule { + statusCode: 404; + missingRuleId: { rule_id: string }; +} + +type ExportRules = ExportSuccesRule | ExportFailedRule; + export interface RulesErrors { + exportedCount: number; missingRules: Array<{ rule_id: string }>; rules: Array>; } @@ -33,28 +46,44 @@ export const getRulesFromObjects = async ( alertsClient: AlertsClient, objects: Array<{ rule_id: string }> ): Promise => { - const alertsAndErrors = await objects.reduce>( - async (accumPromise, object) => { - const accum = await accumPromise; - const rule = await readRules({ alertsClient, ruleId: object.rule_id }); - if (rule != null && isAlertType(rule) && rule.params.immutable !== true) { - const transformedRule = transformAlertToRule(rule); - return { - missingRules: accum.missingRules, - rules: [...accum.rules, transformedRule], - }; - } else { - return { - missingRules: [...accum.missingRules, { rule_id: object.rule_id }], - rules: accum.rules, - }; - } - }, - Promise.resolve({ - exportedCount: 0, - missingRules: [], - rules: [], - }) + const alertsAndErrors = await Promise.all( + objects.reduce>>((accumPromise, object) => { + const exportWorkerPromise = new Promise(async resolve => { + try { + const rule = await readRules({ alertsClient, ruleId: object.rule_id }); + if (rule != null && isAlertType(rule) && rule.params.immutable !== true) { + const transformedRule = transformAlertToRule(rule); + resolve({ + statusCode: 200, + rule: transformedRule, + }); + } else { + resolve({ + statusCode: 404, + missingRuleId: { rule_id: object.rule_id }, + }); + } + } catch { + resolve({ + statusCode: 404, + missingRuleId: { rule_id: object.rule_id }, + }); + } + }); + return [...accumPromise, exportWorkerPromise]; + }, []) ); - return alertsAndErrors; + + const missingRules = alertsAndErrors.filter( + resp => resp.statusCode === 404 + ) as ExportFailedRule[]; + const exportedRules = alertsAndErrors.filter( + resp => resp.statusCode === 200 + ) as ExportSuccesRule[]; + + return { + exportedCount: exportedRules.length, + missingRules: missingRules.map(mr => mr.missingRuleId), + rules: exportedRules.map(er => er.rule), + }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts index d6a3da5a393f8c..bf25ab8bfd7ea0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts @@ -5,14 +5,29 @@ */ import moment from 'moment'; +import sinon from 'sinon'; -import { generateId, parseInterval, getDriftTolerance, getGapBetweenRuns } from './utils'; +import { + generateId, + parseInterval, + parseScheduleDates, + getDriftTolerance, + getGapBetweenRuns, +} from './utils'; describe('utils', () => { + const anchor = '2020-01-01T06:06:06.666Z'; + const unix = moment(anchor).valueOf(); let nowDate = moment('2020-01-01T00:00:00.000Z'); + let clock: sinon.SinonFakeTimers; beforeEach(() => { nowDate = moment('2020-01-01T00:00:00.000Z'); + clock = sinon.useFakeTimers(unix); + }); + + afterEach(() => { + clock.restore(); }); describe('generateId', () => { @@ -27,7 +42,7 @@ describe('utils', () => { }); }); - describe('getIntervalMilliseconds', () => { + describe('parseInterval', () => { test('it returns a duration when given one that is valid', () => { const duration = parseInterval('5m'); expect(duration).not.toBeNull(); @@ -40,8 +55,36 @@ describe('utils', () => { }); }); - describe('getDriftToleranceMilliseconds', () => { - test('it returns a drift tolerance in milliseconds of 1 minute when from overlaps to by 1 minute and the interval is 5 minutes', () => { + describe('parseScheduleDates', () => { + test('it returns a moment when given an ISO string', () => { + const result = parseScheduleDates('2020-01-01T00:00:00.000Z'); + expect(result).not.toBeNull(); + expect(result).toEqual(moment('2020-01-01T00:00:00.000Z')); + }); + + test('it returns a moment when given `now`', () => { + const result = parseScheduleDates('now'); + + expect(result).not.toBeNull(); + expect(moment.isMoment(result)).toBeTruthy(); + }); + + test('it returns a moment when given `now-x`', () => { + const result = parseScheduleDates('now-6m'); + + expect(result).not.toBeNull(); + expect(moment.isMoment(result)).toBeTruthy(); + }); + + test('it returns null when given a string that is not an ISO string, `now` or `now-x`', () => { + const result = parseScheduleDates('invalid'); + + expect(result).toBeNull(); + }); + }); + + describe('getDriftTolerance', () => { + test('it returns a drift tolerance in milliseconds of 1 minute when "from" overlaps "to" by 1 minute and the interval is 5 minutes', () => { const drift = getDriftTolerance({ from: 'now-6m', to: 'now', @@ -51,7 +94,7 @@ describe('utils', () => { expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); }); - test('it returns a drift tolerance of 0 when from equals the interval', () => { + test('it returns a drift tolerance of 0 when "from" equals the interval', () => { const drift = getDriftTolerance({ from: 'now-5m', to: 'now', @@ -60,7 +103,7 @@ describe('utils', () => { expect(drift?.asMilliseconds()).toEqual(0); }); - test('it returns a drift tolerance of 5 minutes when from is 10 minutes but the interval is 5 minutes', () => { + test('it returns a drift tolerance of 5 minutes when "from" is 10 minutes but the interval is 5 minutes', () => { const drift = getDriftTolerance({ from: 'now-10m', to: 'now', @@ -70,7 +113,7 @@ describe('utils', () => { expect(drift?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds()); }); - test('it returns a drift tolerance of 10 minutes when from is 10 minutes ago and the interval is 0', () => { + test('it returns a drift tolerance of 10 minutes when "from" is 10 minutes ago and the interval is 0', () => { const drift = getDriftTolerance({ from: 'now-10m', to: 'now', @@ -80,36 +123,61 @@ describe('utils', () => { expect(drift?.asMilliseconds()).toEqual(moment.duration(10, 'minutes').asMilliseconds()); }); - test('returns null if the "to" is not "now" since we have limited support for date math', () => { + test('returns a drift tolerance of 1 minute when "from" is invalid and defaults to "now-6m" and interval is 5 minutes', () => { const drift = getDriftTolerance({ - from: 'now-6m', - to: 'invalid', // if not set to "now" this function returns null - interval: moment.duration(1000, 'milliseconds'), + from: 'invalid', + to: 'now', + interval: moment.duration(5, 'minutes'), }); - expect(drift).toBeNull(); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); }); - test('returns null if the "from" does not start with "now-" since we have limited support for date math', () => { + test('returns a drift tolerance of 1 minute when "from" does not include `now` and defaults to "now-6m" and interval is 5 minutes', () => { const drift = getDriftTolerance({ - from: 'valid', // if not set to "now-x" where x is an interval such as 6m + from: '10m', to: 'now', - interval: moment.duration(1000, 'milliseconds'), + interval: moment.duration(5, 'minutes'), + }); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); + }); + + test('returns a drift tolerance of 4 minutes when "to" is "now-x", from is a valid input and interval is 5 minute', () => { + const drift = getDriftTolerance({ + from: 'now-10m', + to: 'now-1m', + interval: moment.duration(5, 'minutes'), }); - expect(drift).toBeNull(); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(4, 'minutes').asMilliseconds()); }); - test('returns null if the "from" starts with "now-" but has a string instead of an integer', () => { + test('it returns expected drift tolerance when "from" is an ISO string', () => { const drift = getDriftTolerance({ - from: 'now-dfdf', // if not set to "now-x" where x is an interval such as 6m + from: moment() + .subtract(10, 'minutes') + .toISOString(), to: 'now', - interval: moment.duration(1000, 'milliseconds'), + interval: moment.duration(5, 'minutes'), }); - expect(drift).toBeNull(); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds()); + }); + + test('it returns expected drift tolerance when "to" is an ISO string', () => { + const drift = getDriftTolerance({ + from: 'now-6m', + to: moment().toISOString(), + interval: moment.duration(5, 'minutes'), + }); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); }); }); describe('getGapBetweenRuns', () => { - test('it returns a gap of 0 when from and interval match each other and the previous started was from the previous interval time', () => { + test('it returns a gap of 0 when "from" and interval match each other and the previous started was from the previous interval time', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(5, 'minutes'), interval: '5m', @@ -121,7 +189,7 @@ describe('utils', () => { expect(gap?.asMilliseconds()).toEqual(0); }); - test('it returns a negative gap of 1 minute when from overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { + test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(5, 'minutes'), interval: '5m', @@ -133,7 +201,7 @@ describe('utils', () => { expect(gap?.asMilliseconds()).toEqual(moment.duration(-1, 'minute').asMilliseconds()); }); - test('it returns a negative gap of 5 minutes when from overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { + test('it returns a negative gap of 5 minutes when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(5, 'minutes'), interval: '5m', @@ -145,7 +213,7 @@ describe('utils', () => { expect(gap?.asMilliseconds()).toEqual(moment.duration(-5, 'minute').asMilliseconds()); }); - test('it returns a negative gap of 1 minute when from overlaps to by 1 minute and the previousStartedAt was 10 minutes ago and so was the interval', () => { + test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 10 minutes ago and so was the interval', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(10, 'minutes'), interval: '10m', @@ -233,26 +301,28 @@ describe('utils', () => { expect(gap).toBeNull(); }); - test('it returns null if from is an invalid string such as "invalid"', () => { + test('it returns the expected result when "from" is an invalid string such as "invalid"', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone(), + previousStartedAt: nowDate.clone().subtract(7, 'minutes'), interval: '5m', - from: 'invalid', // if not set to "now-x" where x is an interval such as 6m + from: 'invalid', to: 'now', now: nowDate.clone(), }); - expect(gap).toBeNull(); + expect(gap?.asMilliseconds()).not.toBeNull(); + expect(gap?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); }); - test('it returns null if to is an invalid string such as "invalid"', () => { + test('it returns the expected result when "to" is an invalid string such as "invalid"', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone(), + previousStartedAt: nowDate.clone().subtract(7, 'minutes'), interval: '5m', - from: 'now-5m', - to: 'invalid', // if not set to "now" this function returns null + from: 'now-6m', + to: 'invalid', now: nowDate.clone(), }); - expect(gap).toBeNull(); + expect(gap?.asMilliseconds()).not.toBeNull(); + expect(gap?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts index 5a4c67ebaaa362..940ea8be2ac36a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts @@ -5,6 +5,7 @@ */ import { createHash } from 'crypto'; import moment from 'moment'; +import dateMath from '@elastic/datemath'; import { parseDuration } from '../../../../../alerting/server/lib'; @@ -26,25 +27,34 @@ export const parseInterval = (intervalString: string): moment.Duration | null => } }; +export const parseScheduleDates = (time: string): moment.Moment | null => { + const isValidDateString = !isNaN(Date.parse(time)); + const isValidInput = isValidDateString || time.trim().startsWith('now'); + const formattedDate = isValidDateString + ? moment(time) + : isValidInput + ? dateMath.parse(time) + : null; + + return formattedDate ?? null; +}; + export const getDriftTolerance = ({ from, to, interval, + now = moment(), }: { from: string; to: string; interval: moment.Duration; + now?: moment.Moment; }): moment.Duration | null => { - if (to.trim() !== 'now') { - // we only support 'now' for drift detection - return null; - } - if (!from.trim().startsWith('now-')) { - // we only support from tha starts with now for drift detection - return null; - } - const split = from.split('-'); - const duration = parseInterval(split[1]); + const toDate = parseScheduleDates(to) ?? now; + const fromDate = parseScheduleDates(from) ?? dateMath.parse('now-6m'); + const timeSegment = toDate.diff(fromDate); + const duration = moment.duration(timeSegment); + if (duration !== null) { return duration.subtract(interval); } else { diff --git a/x-pack/legacy/plugins/siem/server/plugin.ts b/x-pack/legacy/plugins/siem/server/plugin.ts index 96eef2f44e5a06..94314367be59cf 100644 --- a/x-pack/legacy/plugins/siem/server/plugin.ts +++ b/x-pack/legacy/plugins/siem/server/plugin.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, PluginInitializerContext, Logger } from 'src/core/server'; -import { PluginSetupContract as SecurityPlugin } from '../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { PluginSetupContract as FeaturesSetupContract } from '../../../../plugins/features/server'; import { initServer } from './init_server'; import { compose } from './lib/compose/kibana'; @@ -17,7 +17,7 @@ import { ruleStatusSavedObjectType, } from './saved_objects'; -export type SiemPluginSecurity = Pick; +export type SiemPluginSecurity = Pick; export interface PluginsSetup { features: FeaturesSetupContract; diff --git a/x-pack/legacy/plugins/siem/server/types.ts b/x-pack/legacy/plugins/siem/server/types.ts index 3fa2268afe92c3..7c07e63404eaa6 100644 --- a/x-pack/legacy/plugins/siem/server/types.ts +++ b/x-pack/legacy/plugins/siem/server/types.ts @@ -8,6 +8,7 @@ import { Legacy } from 'kibana'; export interface ServerFacade { config: Legacy.Server['config']; + usingEphemeralEncryptionKey: boolean; plugins: { // eslint-disable-next-line @typescript-eslint/no-explicit-any actions: any; // We have to do this at the moment because the types are not compatible diff --git a/x-pack/legacy/plugins/siem/server/utils/beat_schema/8.0.0/filebeat.ts b/x-pack/legacy/plugins/siem/server/utils/beat_schema/8.0.0/filebeat.ts index b46cecdc762b75..a5877f6c34b8f2 100644 --- a/x-pack/legacy/plugins/siem/server/utils/beat_schema/8.0.0/filebeat.ts +++ b/x-pack/legacy/plugins/siem/server/utils/beat_schema/8.0.0/filebeat.ts @@ -3132,7 +3132,7 @@ export const filebeatSchema: Schema = [ { name: 'user.roles', description: 'Roles to which the principal belongs', - example: ['kibana_user', 'beats_admin'], + example: ['kibana_admin', 'beats_admin'], type: 'keyword', }, { diff --git a/x-pack/legacy/plugins/transform/common/constants.ts b/x-pack/legacy/plugins/transform/common/constants.ts index c85408d3c5ce60..39138c12c8299e 100644 --- a/x-pack/legacy/plugins/transform/common/constants.ts +++ b/x-pack/legacy/plugins/transform/common/constants.ts @@ -39,11 +39,11 @@ export const API_BASE_PATH = '/api/transform/'; // - dest index: index, create_index (can be applied to a pattern e.g. df-*) // // In the UI additional privileges are required: -// - kibana_user (builtin) +// - kibana_admin (builtin) // - dest index: monitor (applied to df-*) // - cluster: monitor // -// Note that users with kibana_user can see all Kibana index patterns and saved searches +// Note that users with kibana_admin can see all Kibana index patterns and saved searches // in the source selection modal when creating a transform, but the wizard will trigger // error callouts when there are no sufficient privileges to read the actual source indices. diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx index 57e6fc4a9e18b1..7d9a963c9c6b32 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx @@ -13,12 +13,13 @@ import { IUiSettingsClient, ApplicationStart, } from 'kibana/public'; -import { BASE_PATH, Section } from './constants'; +import { BASE_PATH, Section, routeToAlertDetails } from './constants'; import { TriggersActionsUIHome } from './home'; import { AppContextProvider, useAppDependencies } from './app_context'; import { hasShowAlertsCapability } from './lib/capabilities'; import { LegacyDependencies, ActionTypeModel, AlertTypeModel } from '../types'; import { TypeRegistry } from './type_registry'; +import { AlertDetailsRouteWithApi as AlertDetailsRoute } from './sections/alert_details/components/alert_details_route'; export interface AppDeps { chrome: ChromeStart; @@ -53,11 +54,8 @@ export const AppWithoutRouter = ({ sectionsRegex }: any) => { const DEFAULT_SECTION: Section = canShowAlerts ? 'alerts' : 'connectors'; return ( - + + {canShowAlerts && } ); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts index a8364ffe21019d..11b094dea0e624 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts @@ -13,6 +13,7 @@ export type Section = 'connectors' | 'alerts'; export const routeToHome = `${BASE_PATH}`; export const routeToConnectors = `${BASE_PATH}/connectors`; export const routeToAlerts = `${BASE_PATH}/alerts`; +export const routeToAlertDetails = `${BASE_PATH}/alert/:alertId`; export { TIME_UNITS } from './time_units'; export enum SORT_ORDERS { diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts index bc2949917edea7..00a55bb2588bb3 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts @@ -24,6 +24,7 @@ describe('loadActionTypes', () => { { id: 'test', name: 'Test', + enabled: true, }, ]; http.get.mockResolvedValueOnce(resolvedValue); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts index 0106970cf9c38c..35d1a095188dee 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts @@ -8,15 +8,22 @@ import { Alert, AlertType } from '../../types'; import { httpServiceMock } from '../../../../../../../../src/core/public/mocks'; import { createAlert, + deleteAlert, deleteAlerts, disableAlerts, enableAlerts, + disableAlert, + enableAlert, + loadAlert, loadAlerts, loadAlertTypes, muteAlerts, unmuteAlerts, + muteAlert, + unmuteAlert, updateAlert, } from './alert_api'; +import uuid from 'uuid'; const http = httpServiceMock.createStartContract(); @@ -42,6 +49,31 @@ describe('loadAlertTypes', () => { }); }); +describe('loadAlert', () => { + test('should call get API with base parameters', async () => { + const alertId = uuid.v4(); + const resolvedValue = { + id: alertId, + name: 'name', + tags: [], + enabled: true, + alertTypeId: '.noop', + schedule: { interval: '1s' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + expect(await loadAlert({ http, alertId })).toEqual(resolvedValue); + expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}`); + }); +}); + describe('loadAlerts', () => { test('should call find API with base parameters', async () => { const resolvedValue = { @@ -230,6 +262,19 @@ describe('loadAlerts', () => { }); }); +describe('deleteAlert', () => { + test('should call delete API for alert', async () => { + const id = '1'; + const result = await deleteAlert({ http, id }); + expect(result).toEqual(undefined); + expect(http.delete.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alert/1", + ] + `); + }); +}); + describe('deleteAlerts', () => { test('should call delete API for each alert', async () => { const ids = ['1', '2', '3']; @@ -335,6 +380,62 @@ describe('updateAlert', () => { }); }); +describe('enableAlert', () => { + test('should call enable alert API', async () => { + const result = await enableAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alert/1/_enable", + ], + ] + `); + }); +}); + +describe('disableAlert', () => { + test('should call disable alert API', async () => { + const result = await disableAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alert/1/_disable", + ], + ] + `); + }); +}); + +describe('muteAlert', () => { + test('should call mute alert API', async () => { + const result = await muteAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alert/1/_mute_all", + ], + ] + `); + }); +}); + +describe('unmuteAlert', () => { + test('should call unmute alert API', async () => { + const result = await unmuteAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alert/1/_unmute_all", + ], + ] + `); + }); +}); + describe('enableAlerts', () => { test('should call enable alert API per alert', async () => { const ids = ['1', '2', '3']; diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts index 0b4f5731c13153..acc318bd5fbea5 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts @@ -12,6 +12,16 @@ export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise { + return await http.get(`${BASE_ALERT_API_PATH}/${alertId}`); +} + export async function loadAlerts({ http, page, @@ -55,6 +65,10 @@ export async function loadAlerts({ }); } +export async function deleteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.delete(`${BASE_ALERT_API_PATH}/${id}`); +} + export async function deleteAlerts({ ids, http, @@ -62,7 +76,7 @@ export async function deleteAlerts({ ids: string[]; http: HttpSetup; }): Promise { - await Promise.all(ids.map(id => http.delete(`${BASE_ALERT_API_PATH}/${id}`))); + await Promise.all(ids.map(id => deleteAlert({ http, id }))); } export async function createAlert({ @@ -91,6 +105,10 @@ export async function updateAlert({ }); } +export async function enableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERT_API_PATH}/${id}/_enable`); +} + export async function enableAlerts({ ids, http, @@ -98,7 +116,11 @@ export async function enableAlerts({ ids: string[]; http: HttpSetup; }): Promise { - await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_enable`))); + await Promise.all(ids.map(id => enableAlert({ id, http }))); +} + +export async function disableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERT_API_PATH}/${id}/_disable`); } export async function disableAlerts({ @@ -108,11 +130,19 @@ export async function disableAlerts({ ids: string[]; http: HttpSetup; }): Promise { - await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_disable`))); + await Promise.all(ids.map(id => disableAlert({ id, http }))); +} + +export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERT_API_PATH}/${id}/_mute_all`); } export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup }): Promise { - await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_mute_all`))); + await Promise.all(ids.map(id => muteAlert({ http, id }))); +} + +export async function unmuteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERT_API_PATH}/${id}/_unmute_all`); } export async function unmuteAlerts({ @@ -122,5 +152,5 @@ export async function unmuteAlerts({ ids: string[]; http: HttpSetup; }): Promise { - await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_unmute_all`))); + await Promise.all(ids.map(id => unmuteAlert({ id, http }))); } diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.test.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.test.ts new file mode 100644 index 00000000000000..90f575d9391b32 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.test.ts @@ -0,0 +1,77 @@ +/* + * 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 { throwIfAbsent, throwIfIsntContained } from './value_validators'; +import uuid from 'uuid'; + +describe('throwIfAbsent', () => { + test('throws if value is absent', () => { + [undefined, null].forEach(val => { + expect(() => { + throwIfAbsent('OMG no value')(val); + }).toThrowErrorMatchingInlineSnapshot(`"OMG no value"`); + }); + }); + + test('doesnt throws if value is present but falsey', () => { + [false, ''].forEach(val => { + expect(throwIfAbsent('OMG no value')(val)).toEqual(val); + }); + }); + + test('doesnt throw if value is present', () => { + expect(throwIfAbsent('OMG no value')({})).toEqual({}); + }); +}); + +describe('throwIfIsntContained', () => { + test('throws if value is absent', () => { + expect(() => { + throwIfIsntContained(new Set([uuid.v4()]), 'OMG no value', val => val)([uuid.v4()]); + }).toThrowErrorMatchingInlineSnapshot(`"OMG no value"`); + }); + + test('throws if value is absent using custom message', () => { + const id = uuid.v4(); + expect(() => { + throwIfIsntContained( + new Set([id]), + (value: string) => `OMG no ${value}`, + val => val + )([uuid.v4()]); + }).toThrow(`OMG no ${id}`); + }); + + test('returns values if value is present', () => { + const id = uuid.v4(); + const values = [uuid.v4(), uuid.v4(), id, uuid.v4()]; + expect(throwIfIsntContained(new Set([id]), 'OMG no value', val => val)(values)).toEqual( + values + ); + }); + + test('returns values if multiple values is present', () => { + const [firstId, secondId] = [uuid.v4(), uuid.v4()]; + const values = [uuid.v4(), uuid.v4(), secondId, uuid.v4(), firstId]; + expect( + throwIfIsntContained(new Set([firstId, secondId]), 'OMG no value', val => val)(values) + ).toEqual(values); + }); + + test('allows a custom value extractor', () => { + const [firstId, secondId] = [uuid.v4(), uuid.v4()]; + const values = [ + { id: firstId, some: 'prop' }, + { id: secondId, someOther: 'prop' }, + ]; + expect( + throwIfIsntContained<{ id: string }>( + new Set([firstId, secondId]), + 'OMG no value', + (val: { id: string }) => val.id + )(values) + ).toEqual(values); + }); +}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.ts new file mode 100644 index 00000000000000..7ee73590864065 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { constant } from 'lodash'; + +export function throwIfAbsent(message: string) { + return (value: T | undefined): T => { + if (value === undefined || value === null) { + throw new Error(message); + } + return value; + }; +} + +export function throwIfIsntContained( + requiredValues: Set, + message: string | ((requiredValue: string) => string), + valueExtractor: (value: T) => string +) { + const toError = typeof message === 'function' ? message : constant(message); + return (values: T[]) => { + const availableValues = new Set(values.map(valueExtractor)); + for (const value of requiredValues.values()) { + if (!availableValues.has(value)) { + throw new Error(toError(value)); + } + } + return values; + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx index 6896ac954bb068..f27f7d8c3054df 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx @@ -82,7 +82,11 @@ describe('action_connector_form', () => { editFlyoutVisible: false, setEditFlyoutVisibility: () => {}, actionTypesIndex: { - 'my-action-type': { id: 'my-action-type', name: 'my-action-type-name' }, + 'my-action-type': { + id: 'my-action-type', + name: 'my-action-type-name', + enabled: true, + }, }, reloadConnectors: () => { return new Promise(() => {}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx index 6ef2f62315d9a4..6d98a5e3d120f4 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx @@ -75,8 +75,8 @@ describe('connector_add_flyout', () => { editFlyoutVisible: false, setEditFlyoutVisibility: state => {}, actionTypesIndex: { - 'first-action-type': { id: 'first-action-type', name: 'first' }, - 'second-action-type': { id: 'second-action-type', name: 'second' }, + 'first-action-type': { id: 'first-action-type', name: 'first', enabled: true }, + 'second-action-type': { id: 'second-action-type', name: 'second', enabled: true }, }, reloadConnectors: () => { return new Promise(() => {}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx index 71ba52f047d617..a03296c7c36792 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx @@ -58,7 +58,9 @@ describe('connector_add_flyout', () => { setAddFlyoutVisibility: state => {}, editFlyoutVisible: false, setEditFlyoutVisibility: state => {}, - actionTypesIndex: { 'my-action-type': { id: 'my-action-type', name: 'test' } }, + actionTypesIndex: { + 'my-action-type': { id: 'my-action-type', name: 'test', enabled: true }, + }, reloadConnectors: () => { return new Promise(() => {}); }, diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx index 57e950a98eb2ae..0dc38523bfab83 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx @@ -84,7 +84,7 @@ describe('connector_edit_flyout', () => { editFlyoutVisible: true, setEditFlyoutVisibility: state => {}, actionTypesIndex: { - 'test-action-type-id': { id: 'test-action-type-id', name: 'test' }, + 'test-action-type-id': { id: 'test-action-type-id', name: 'test', enabled: true }, }, reloadConnectors: () => { return new Promise(() => {}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.test.tsx new file mode 100644 index 00000000000000..228bceb87cad7d --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.test.tsx @@ -0,0 +1,519 @@ +/* + * 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 uuid from 'uuid'; +import { shallow } from 'enzyme'; +import { AlertDetails } from './alert_details'; +import { Alert, ActionType } from '../../../../types'; +import { EuiTitle, EuiBadge, EuiFlexItem, EuiButtonEmpty, EuiSwitch } from '@elastic/eui'; +import { times, random } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; + +jest.mock('../../../app_context', () => ({ + useAppDependencies: jest.fn(() => ({ + http: jest.fn(), + legacy: { + capabilities: { + get: jest.fn(() => ({})), + }, + }, + })), +})); + +jest.mock('../../../lib/capabilities', () => ({ + hasSaveAlertsCapability: jest.fn(() => true), +})); + +const mockAlertApis = { + muteAlert: jest.fn(), + unmuteAlert: jest.fn(), + enableAlert: jest.fn(), + disableAlert: jest.fn(), +}; + +// const AlertDetails = withBulkAlertOperations(RawAlertDetails); +describe('alert_details', () => { + // mock Api handlers + + it('renders the alert name as a title', () => { + const alert = mockAlert(); + const alertType = { + id: '.noop', + name: 'No Op', + }; + + expect( + shallow( + + ).containsMatchingElement( + +

{alert.name}

+
+ ) + ).toBeTruthy(); + }); + + it('renders the alert type badge', () => { + const alert = mockAlert(); + const alertType = { + id: '.noop', + name: 'No Op', + }; + + expect( + shallow( + + ).containsMatchingElement({alertType.name}) + ).toBeTruthy(); + }); + + describe('actions', () => { + it('renders an alert action', () => { + const alert = mockAlert({ + actions: [ + { + group: 'default', + id: uuid.v4(), + params: {}, + actionTypeId: '.server-log', + }, + ], + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const actionTypes: ActionType[] = [ + { + id: '.server-log', + name: 'Server log', + enabled: true, + }, + ]; + + expect( + shallow( + + ).containsMatchingElement( + + {actionTypes[0].name} + + ) + ).toBeTruthy(); + }); + + it('renders a counter for multiple alert action', () => { + const actionCount = random(1, 10); + const alert = mockAlert({ + actions: [ + { + group: 'default', + id: uuid.v4(), + params: {}, + actionTypeId: '.server-log', + }, + ...times(actionCount, () => ({ + group: 'default', + id: uuid.v4(), + params: {}, + actionTypeId: '.email', + })), + ], + }); + const alertType = { + id: '.noop', + name: 'No Op', + }; + const actionTypes: ActionType[] = [ + { + id: '.server-log', + name: 'Server log', + enabled: true, + }, + { + id: '.email', + name: 'Send email', + enabled: true, + }, + ]; + + const details = shallow( + + ); + + expect( + details.containsMatchingElement( + + {actionTypes[0].name} + + ) + ).toBeTruthy(); + + expect( + details.containsMatchingElement( + + {`+${actionCount}`} + + ) + ).toBeTruthy(); + }); + }); + + describe('links', () => { + it('links to the Edit flyout', () => { + const alert = mockAlert(); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + expect( + shallow( + + ).containsMatchingElement( + + + + ) + ).toBeTruthy(); + }); + + it('links to the app that created the alert', () => { + const alert = mockAlert(); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + expect( + shallow( + + ).containsMatchingElement( + + + + ) + ).toBeTruthy(); + }); + + it('links to the activity log', () => { + const alert = mockAlert(); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + expect( + shallow( + + ).containsMatchingElement( + + + + ) + ).toBeTruthy(); + }); + }); +}); + +describe('enable button', () => { + it('should render an enable button when alert is enabled', () => { + const alert = mockAlert({ + enabled: true, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const enableButton = shallow( + + ) + .find(EuiSwitch) + .find('[name="enable"]') + .first(); + + expect(enableButton.props()).toMatchObject({ + checked: true, + disabled: false, + }); + }); + + it('should render an enable button when alert is disabled', () => { + const alert = mockAlert({ + enabled: false, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const enableButton = shallow( + + ) + .find(EuiSwitch) + .find('[name="enable"]') + .first(); + + expect(enableButton.props()).toMatchObject({ + checked: false, + disabled: false, + }); + }); + + it('should enable the alert when alert is disabled and button is clicked', () => { + const alert = mockAlert({ + enabled: true, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const disableAlert = jest.fn(); + const enableButton = shallow( + + ) + .find(EuiSwitch) + .find('[name="enable"]') + .first(); + + enableButton.simulate('click'); + const handler = enableButton.prop('onChange'); + expect(typeof handler).toEqual('function'); + expect(disableAlert).toHaveBeenCalledTimes(0); + handler!({} as React.FormEvent); + expect(disableAlert).toHaveBeenCalledTimes(1); + }); + + it('should disable the alert when alert is enabled and button is clicked', () => { + const alert = mockAlert({ + enabled: false, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const enableAlert = jest.fn(); + const enableButton = shallow( + + ) + .find(EuiSwitch) + .find('[name="enable"]') + .first(); + + enableButton.simulate('click'); + const handler = enableButton.prop('onChange'); + expect(typeof handler).toEqual('function'); + expect(enableAlert).toHaveBeenCalledTimes(0); + handler!({} as React.FormEvent); + expect(enableAlert).toHaveBeenCalledTimes(1); + }); +}); + +describe('mute button', () => { + it('should render an mute button when alert is enabled', () => { + const alert = mockAlert({ + enabled: true, + muteAll: false, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const enableButton = shallow( + + ) + .find(EuiSwitch) + .find('[name="mute"]') + .first(); + + expect(enableButton.props()).toMatchObject({ + checked: false, + disabled: false, + }); + }); + + it('should render an muted button when alert is muted', () => { + const alert = mockAlert({ + enabled: true, + muteAll: true, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const enableButton = shallow( + + ) + .find(EuiSwitch) + .find('[name="mute"]') + .first(); + + expect(enableButton.props()).toMatchObject({ + checked: true, + disabled: false, + }); + }); + + it('should mute the alert when alert is unmuted and button is clicked', () => { + const alert = mockAlert({ + enabled: true, + muteAll: false, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const muteAlert = jest.fn(); + const enableButton = shallow( + + ) + .find(EuiSwitch) + .find('[name="mute"]') + .first(); + + enableButton.simulate('click'); + const handler = enableButton.prop('onChange'); + expect(typeof handler).toEqual('function'); + expect(muteAlert).toHaveBeenCalledTimes(0); + handler!({} as React.FormEvent); + expect(muteAlert).toHaveBeenCalledTimes(1); + }); + + it('should unmute the alert when alert is muted and button is clicked', () => { + const alert = mockAlert({ + enabled: true, + muteAll: true, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const unmuteAlert = jest.fn(); + const enableButton = shallow( + + ) + .find(EuiSwitch) + .find('[name="mute"]') + .first(); + + enableButton.simulate('click'); + const handler = enableButton.prop('onChange'); + expect(typeof handler).toEqual('function'); + expect(unmuteAlert).toHaveBeenCalledTimes(0); + handler!({} as React.FormEvent); + expect(unmuteAlert).toHaveBeenCalledTimes(1); + }); + + it('should disabled mute button when alert is disabled', () => { + const alert = mockAlert({ + enabled: false, + muteAll: false, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const enableButton = shallow( + + ) + .find(EuiSwitch) + .find('[name="mute"]') + .first(); + + expect(enableButton.props()).toMatchObject({ + checked: false, + disabled: true, + }); + }); +}); + +function mockAlert(overloads: Partial = {}): Alert { + return { + id: uuid.v4(), + enabled: true, + name: `alert-${uuid.v4()}`, + tags: [], + alertTypeId: '.noop', + consumer: 'consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + ...overloads, + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.tsx new file mode 100644 index 00000000000000..ffdf846efd49d5 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.tsx @@ -0,0 +1,176 @@ +/* + * 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 { indexBy } from 'lodash'; +import { + EuiPageBody, + EuiPageContent, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiPage, + EuiPageContentBody, + EuiButtonEmpty, + EuiSwitch, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useAppDependencies } from '../../../app_context'; +import { hasSaveAlertsCapability } from '../../../lib/capabilities'; +import { Alert, AlertType, ActionType } from '../../../../types'; +import { + ComponentOpts as BulkOperationsComponentOpts, + withBulkAlertOperations, +} from '../../common/components/with_bulk_alert_api_operations'; + +type AlertDetailsProps = { + alert: Alert; + alertType: AlertType; + actionTypes: ActionType[]; +} & Pick; + +export const AlertDetails: React.FunctionComponent = ({ + alert, + alertType, + actionTypes, + disableAlert, + enableAlert, + unmuteAlert, + muteAlert, +}) => { + const { capabilities } = useAppDependencies(); + + const canSave = hasSaveAlertsCapability(capabilities); + + const actionTypesByTypeId = indexBy(actionTypes, 'id'); + const [firstAction, ...otherActions] = alert.actions; + + const [isEnabled, setIsEnabled] = useState(alert.enabled); + const [isMuted, setIsMuted] = useState(alert.muteAll); + + return ( + + + + + + +

{alert.name}

+
+
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + {alertType.name} + + {firstAction && ( + + + {actionTypesByTypeId[firstAction.actionTypeId].name ?? + firstAction.actionTypeId} + + + )} + {otherActions.length ? ( + + +{otherActions.length} + + ) : null} + + + + + + { + if (isEnabled) { + setIsEnabled(false); + await disableAlert(alert); + } else { + setIsEnabled(true); + await enableAlert(alert); + } + }} + label={ + + } + /> + + + { + if (isMuted) { + setIsMuted(false); + await unmuteAlert(alert); + } else { + setIsMuted(true); + await muteAlert(alert); + } + }} + label={ + + } + /> + + + + + +
+
+
+ ); +}; + +export const AlertDetailsWithApi = withBulkAlertOperations(AlertDetails); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.test.tsx new file mode 100644 index 00000000000000..7a40104e97d9fd --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.test.tsx @@ -0,0 +1,409 @@ +/* + * 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 uuid from 'uuid'; +import { shallow } from 'enzyme'; +import { createMemoryHistory, createLocation } from 'history'; +import { ToastsApi } from 'kibana/public'; +import { AlertDetailsRoute, getAlertData } from './alert_details_route'; +import { Alert } from '../../../../types'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +jest.mock('../../../app_context', () => { + const toastNotifications = jest.fn(); + return { + useAppDependencies: jest.fn(() => ({ toastNotifications })), + }; +}); +describe('alert_details_route', () => { + it('render a loader while fetching data', () => { + const alert = mockAlert(); + + expect( + shallow( + + ).containsMatchingElement() + ).toBeTruthy(); + }); +}); + +describe('getAlertData useEffect handler', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('fetches alert', async () => { + const alert = mockAlert(); + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementationOnce(async () => alert); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + + expect(loadAlert).toHaveBeenCalledWith(alert.id); + expect(setAlert).toHaveBeenCalledWith(alert); + }); + + it('fetches alert and action types', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + const alertType = { + id: alert.alertTypeId, + name: 'type name', + }; + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + loadAlertTypes.mockImplementation(async () => [alertType]); + loadActionTypes.mockImplementation(async () => [actionType]); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + + expect(loadAlertTypes).toHaveBeenCalledTimes(1); + expect(loadActionTypes).toHaveBeenCalledTimes(1); + + expect(setAlertType).toHaveBeenCalledWith(alertType); + expect(setActionTypes).toHaveBeenCalledWith([actionType]); + }); + + it('displays an error if the alert isnt found', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => { + throw new Error('OMG'); + }); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: 'Unable to load alert: OMG', + }); + }); + + it('displays an error if the alert type isnt loaded', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + + loadAlertTypes.mockImplementation(async () => { + throw new Error('OMG no alert type'); + }); + loadActionTypes.mockImplementation(async () => [actionType]); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: 'Unable to load alert: OMG no alert type', + }); + }); + + it('displays an error if the action type isnt loaded', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + const alertType = { + id: alert.alertTypeId, + name: 'type name', + }; + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + + loadAlertTypes.mockImplementation(async () => [alertType]); + loadActionTypes.mockImplementation(async () => { + throw new Error('OMG no action type'); + }); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: 'Unable to load alert: OMG no action type', + }); + }); + + it('displays an error if the alert type isnt found', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + + const alertType = { + id: uuid.v4(), + name: 'type name', + }; + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + loadAlertTypes.mockImplementation(async () => [alertType]); + loadActionTypes.mockImplementation(async () => [actionType]); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: `Unable to load alert: Invalid Alert Type: ${alert.alertTypeId}`, + }); + }); + + it('displays an error if an action type isnt found', async () => { + const availableActionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const missingActionType = { + id: '.noop', + name: 'No Op', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: availableActionType.id, + params: {}, + }, + { + group: '', + id: uuid.v4(), + actionTypeId: missingActionType.id, + params: {}, + }, + ], + }); + + const alertType = { + id: uuid.v4(), + name: 'type name', + }; + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + loadAlertTypes.mockImplementation(async () => [alertType]); + loadActionTypes.mockImplementation(async () => [availableActionType]); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: `Unable to load alert: Invalid Action Type: ${missingActionType.id}`, + }); + }); +}); + +function mockApis() { + return { + loadAlert: jest.fn(), + loadAlertTypes: jest.fn(), + loadActionTypes: jest.fn(), + }; +} + +function mockStateSetter() { + return { + setAlert: jest.fn(), + setAlertType: jest.fn(), + setActionTypes: jest.fn(), + }; +} + +function mockRouterProps(alert: Alert) { + return { + match: { + isExact: false, + path: `/alert/${alert.id}`, + url: '', + params: { alertId: alert.id }, + }, + history: createMemoryHistory(), + location: createLocation(`/alert/${alert.id}`), + }; +} +function mockAlert(overloads: Partial = {}): Alert { + return { + id: uuid.v4(), + enabled: true, + name: `alert-${uuid.v4()}`, + tags: [], + alertTypeId: '.noop', + consumer: 'consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + ...overloads, + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.tsx new file mode 100644 index 00000000000000..4e00ea304d987d --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.tsx @@ -0,0 +1,118 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { useState, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { ToastsApi } from 'kibana/public'; +import { Alert, AlertType, ActionType } from '../../../../types'; +import { useAppDependencies } from '../../../app_context'; +import { AlertDetailsWithApi as AlertDetails } from './alert_details'; +import { throwIfAbsent, throwIfIsntContained } from '../../../lib/value_validators'; +import { + ComponentOpts as AlertApis, + withBulkAlertOperations, +} from '../../common/components/with_bulk_alert_api_operations'; +import { + ComponentOpts as ActionApis, + withActionOperations, +} from '../../common/components/with_actions_api_operations'; + +type AlertDetailsRouteProps = RouteComponentProps<{ + alertId: string; +}> & + Pick & + Pick; + +export const AlertDetailsRoute: React.FunctionComponent = ({ + match: { + params: { alertId }, + }, + loadAlert, + loadAlertTypes, + loadActionTypes, +}) => { + const { http, toastNotifications } = useAppDependencies(); + + const [alert, setAlert] = useState(null); + const [alertType, setAlertType] = useState(null); + const [actionTypes, setActionTypes] = useState(null); + + useEffect(() => { + getAlertData( + alertId, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + }, [alertId, http, loadActionTypes, loadAlert, loadAlertTypes, toastNotifications]); + + return alert && alertType && actionTypes ? ( + + ) : ( +
+ +
+ ); +}; + +export async function getAlertData( + alertId: string, + loadAlert: AlertApis['loadAlert'], + loadAlertTypes: AlertApis['loadAlertTypes'], + loadActionTypes: ActionApis['loadActionTypes'], + setAlert: React.Dispatch>, + setAlertType: React.Dispatch>, + setActionTypes: React.Dispatch>, + toastNotifications: Pick +) { + try { + const loadedAlert = await loadAlert(alertId); + setAlert(loadedAlert); + + const [loadedAlertType, loadedActionTypes] = await Promise.all([ + loadAlertTypes() + .then(types => types.find(type => type.id === loadedAlert.alertTypeId)) + .then(throwIfAbsent(`Invalid Alert Type: ${loadedAlert.alertTypeId}`)), + loadActionTypes().then( + throwIfIsntContained( + new Set(loadedAlert.actions.map(action => action.actionTypeId)), + (requiredActionType: string) => `Invalid Action Type: ${requiredActionType}`, + (action: ActionType) => action.id + ) + ), + ]); + + setAlertType(loadedAlertType); + setActionTypes(loadedActionTypes); + } catch (e) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertMessage', + { + defaultMessage: 'Unable to load alert: {message}', + values: { + message: e.message, + }, + } + ), + }); + } +} + +export const AlertDetailsRouteWithApi = withActionOperations( + withBulkAlertOperations(AlertDetailsRoute) +); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx index ff1510ea873d3b..f410fff44172fa 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -21,7 +21,14 @@ jest.mock('../../../lib/alert_api', () => ({ loadAlerts: jest.fn(), loadAlertTypes: jest.fn(), })); - +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + push: jest.fn(), + }), + useLocation: () => ({ + pathname: '/triggersActions/alerts/', + }), +})); const actionTypeRegistry = actionTypeRegistryMock.create(); const alertTypeRegistry = alertTypeRegistryMock.create(); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx index 12122983161bd8..32de924f63e807 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx @@ -15,19 +15,23 @@ import { EuiFlexItem, EuiIcon, EuiSpacer, + EuiLink, } from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; import { AlertsContextProvider } from '../../../context/alerts_context'; import { useAppDependencies } from '../../../app_context'; import { ActionType, Alert, AlertTableItem, AlertTypeIndex, Pagination } from '../../../../types'; import { AlertAdd } from '../../alert_add'; -import { BulkActionPopover } from './bulk_action_popover'; -import { CollapsedItemActions } from './collapsed_item_actions'; +import { BulkOperationPopover } from '../../common/components/bulk_operation_popover'; +import { AlertQuickEditButtonsWithApi as AlertQuickEditButtons } from '../../common/components/alert_quick_edit_buttons'; +import { CollapsedItemActionsWithApi as CollapsedItemActions } from './collapsed_item_actions'; import { TypeFilter } from './type_filter'; import { ActionTypeFilter } from './action_type_filter'; import { loadAlerts, loadAlertTypes } from '../../../lib/alert_api'; import { loadActionTypes } from '../../../lib/action_connector_api'; import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities'; +import { routeToAlertDetails } from '../../../constants'; const ENTER_KEY = 13; @@ -43,6 +47,7 @@ interface AlertState { } export const AlertsList: React.FunctionComponent = () => { + const history = useHistory(); const { http, injectedMetadata, toastNotifications, capabilities } = useAppDependencies(); const canDelete = hasDeleteAlertsCapability(capabilities); const canSave = hasSaveAlertsCapability(capabilities); @@ -151,6 +156,18 @@ export const AlertsList: React.FunctionComponent = () => { sortable: false, truncateText: true, 'data-test-subj': 'alertsTableCell-name', + render: (name: string, alert: AlertTableItem) => { + return ( + { + history.push(routeToAlertDetails.replace(`:alertId`, alert.id)); + }} + > + {name} + + ); + }, }, { field: 'tagsText', @@ -236,17 +253,19 @@ export const AlertsList: React.FunctionComponent = () => { {selectedIds.length > 0 && canDelete && ( - setIsPerformingAction(true)} - onActionPerformed={() => { - loadAlertsData(); - setIsPerformingAction(false); - }} - /> + + setIsPerformingAction(true)} + onActionPerformed={() => { + loadAlertsData(); + setIsPerformingAction(false); + }} + /> + )} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx index aa1c6dd7c5b9ac..2bac159ed79ede 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx @@ -20,23 +20,25 @@ import { AlertTableItem } from '../../../../types'; import { useAppDependencies } from '../../../app_context'; import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities'; import { - deleteAlerts, - disableAlerts, - enableAlerts, - muteAlerts, - unmuteAlerts, -} from '../../../lib/alert_api'; + ComponentOpts as BulkOperationsComponentOpts, + withBulkAlertOperations, +} from '../../common/components/with_bulk_alert_api_operations'; -export interface ComponentOpts { +export type ComponentOpts = { item: AlertTableItem; onAlertChanged: () => void; -} +} & BulkOperationsComponentOpts; export const CollapsedItemActions: React.FunctionComponent = ({ item, onAlertChanged, + disableAlert, + enableAlert, + unmuteAlert, + muteAlert, + deleteAlert, }: ComponentOpts) => { - const { http, capabilities } = useAppDependencies(); + const { capabilities } = useAppDependencies(); const canDelete = hasDeleteAlertsCapability(capabilities); const canSave = hasSaveAlertsCapability(capabilities); @@ -71,9 +73,9 @@ export const CollapsedItemActions: React.FunctionComponent = ({ data-test-subj="enableSwitch" onChange={async () => { if (item.enabled) { - await disableAlerts({ http, ids: [item.id] }); + await disableAlert(item); } else { - await enableAlerts({ http, ids: [item.id] }); + await enableAlert(item); } onAlertChanged(); }} @@ -93,9 +95,9 @@ export const CollapsedItemActions: React.FunctionComponent = ({ data-test-subj="muteSwitch" onChange={async () => { if (item.muteAll) { - await unmuteAlerts({ http, ids: [item.id] }); + await unmuteAlert(item); } else { - await muteAlerts({ http, ids: [item.id] }); + await muteAlert(item); } onAlertChanged(); }} @@ -115,7 +117,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({ color="text" data-test-subj="deleteAlert" onClick={async () => { - await deleteAlerts({ http, ids: [item.id] }); + await deleteAlert(item); onAlertChanged(); }} > @@ -129,3 +131,5 @@ export const CollapsedItemActions: React.FunctionComponent = ({ ); }; + +export const CollapsedItemActionsWithApi = withBulkAlertOperations(CollapsedItemActions); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/bulk_action_popover.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/alert_quick_edit_buttons.tsx similarity index 52% rename from x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/bulk_action_popover.tsx rename to x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/alert_quick_edit_buttons.tsx index 59ec52ac83a6c5..9635e6cd119836 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/bulk_action_popover.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/alert_quick_edit_buttons.tsx @@ -5,34 +5,35 @@ */ import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; +import React, { useState, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton, EuiButtonEmpty, EuiFormRow, EuiPopover } from '@elastic/eui'; +import { EuiButtonEmpty } from '@elastic/eui'; -import { AlertTableItem } from '../../../../types'; +import { Alert } from '../../../../types'; import { useAppDependencies } from '../../../app_context'; import { - deleteAlerts, - disableAlerts, - enableAlerts, - muteAlerts, - unmuteAlerts, -} from '../../../lib/alert_api'; + withBulkAlertOperations, + ComponentOpts as BulkOperationsComponentOpts, +} from './with_bulk_alert_api_operations'; -export interface ComponentOpts { - selectedItems: AlertTableItem[]; - onPerformingAction: () => void; - onActionPerformed: () => void; -} +export type ComponentOpts = { + selectedItems: Alert[]; + onPerformingAction?: () => void; + onActionPerformed?: () => void; +} & BulkOperationsComponentOpts; -export const BulkActionPopover: React.FunctionComponent = ({ +export const AlertQuickEditButtons: React.FunctionComponent = ({ selectedItems, - onPerformingAction, - onActionPerformed, + onPerformingAction = noop, + onActionPerformed = noop, + muteAlerts, + unmuteAlerts, + enableAlerts, + disableAlerts, + deleteAlerts, }: ComponentOpts) => { - const { http, toastNotifications } = useAppDependencies(); + const { toastNotifications } = useAppDependencies(); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isMutingAlerts, setIsMutingAlerts] = useState(false); const [isUnmutingAlerts, setIsUnmutingAlerts] = useState(false); const [isEnablingAlerts, setIsEnablingAlerts] = useState(false); @@ -47,9 +48,8 @@ export const BulkActionPopover: React.FunctionComponent = ({ async function onmMuteAllClick() { onPerformingAction(); setIsMutingAlerts(true); - const ids = selectedItems.filter(item => !isAlertMuted(item)).map(item => item.id); try { - await muteAlerts({ http, ids }); + await muteAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -68,9 +68,8 @@ export const BulkActionPopover: React.FunctionComponent = ({ async function onUnmuteAllClick() { onPerformingAction(); setIsUnmutingAlerts(true); - const ids = selectedItems.filter(isAlertMuted).map(item => item.id); try { - await unmuteAlerts({ http, ids }); + await unmuteAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -89,9 +88,8 @@ export const BulkActionPopover: React.FunctionComponent = ({ async function onEnableAllClick() { onPerformingAction(); setIsEnablingAlerts(true); - const ids = selectedItems.filter(isAlertDisabled).map(item => item.id); try { - await enableAlerts({ http, ids }); + await enableAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -110,9 +108,8 @@ export const BulkActionPopover: React.FunctionComponent = ({ async function onDisableAllClick() { onPerformingAction(); setIsDisablingAlerts(true); - const ids = selectedItems.filter(item => !isAlertDisabled(item)).map(item => item.id); try { - await disableAlerts({ http, ids }); + await disableAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -131,9 +128,8 @@ export const BulkActionPopover: React.FunctionComponent = ({ async function deleteSelectedItems() { onPerformingAction(); setIsDeletingAlerts(true); - const ids = selectedItems.map(item => item.id); try { - await deleteAlerts({ http, ids }); + await deleteAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -150,104 +146,83 @@ export const BulkActionPopover: React.FunctionComponent = ({ } return ( - setIsPopoverOpen(false)} - data-test-subj="bulkAction" - button={ - setIsPopoverOpen(!isPopoverOpen)} + + {!allAlertsMuted && ( + - - } - > - {!allAlertsMuted && ( - - - - - + )} {allAlertsMuted && ( - - - - - + + + )} {allAlertsDisabled && ( - - - - - + + + )} {!allAlertsDisabled && ( - - - - - - )} - - - + )} + + + + + ); }; -function isAlertDisabled(alert: AlertTableItem) { +export const AlertQuickEditButtonsWithApi = withBulkAlertOperations(AlertQuickEditButtons); + +function isAlertDisabled(alert: Alert) { return alert.enabled === false; } -function isAlertMuted(alert: AlertTableItem) { +function isAlertMuted(alert: Alert) { return alert.muteAll === true; } + +function noop() {} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/bulk_operation_popover.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/bulk_operation_popover.tsx new file mode 100644 index 00000000000000..d0fd0e17928183 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/bulk_operation_popover.tsx @@ -0,0 +1,42 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButton, EuiFormRow, EuiPopover } from '@elastic/eui'; + +export const BulkOperationPopover: React.FunctionComponent = ({ children }) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + return ( + setIsPopoverOpen(false)} + data-test-subj="bulkAction" + button={ + setIsPopoverOpen(!isPopoverOpen)} + > + + + } + > + {children && + React.Children.map(children, child => + React.isValidElement(child) ? ( + {React.cloneElement(child, {})} + ) : ( + child + ) + )} + + ); +}; diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.test.tsx new file mode 100644 index 00000000000000..dd6b8775ba3d09 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as React from 'react'; +import { shallow, mount } from 'enzyme'; +import { withActionOperations, ComponentOpts } from './with_actions_api_operations'; +import * as actionApis from '../../../lib/action_connector_api'; +import { useAppDependencies } from '../../../app_context'; + +jest.mock('../../../lib/action_connector_api'); + +jest.mock('../../../app_context', () => { + const http = jest.fn(); + return { + useAppDependencies: jest.fn(() => ({ + http, + })), + }; +}); + +describe('with_action_api_operations', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('extends any component with Action Api methods', () => { + const ComponentToExtend = (props: ComponentOpts) => { + expect(typeof props.loadActionTypes).toEqual('function'); + return
; + }; + + const ExtendedComponent = withActionOperations(ComponentToExtend); + expect(shallow().type()).toEqual(ComponentToExtend); + }); + + it('loadActionTypes calls the loadActionTypes api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ loadActionTypes }: ComponentOpts) => { + return ; + }; + + const ExtendedComponent = withActionOperations(ComponentToExtend); + const component = mount(); + component.find('button').simulate('click'); + + expect(actionApis.loadActionTypes).toHaveBeenCalledTimes(1); + expect(actionApis.loadActionTypes).toHaveBeenCalledWith({ http }); + }); +}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.tsx new file mode 100644 index 00000000000000..45e6c6b10532ce --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.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 React from 'react'; + +import { ActionType } from '../../../../types'; +import { useAppDependencies } from '../../../app_context'; +import { loadActionTypes } from '../../../lib/action_connector_api'; + +export interface ComponentOpts { + loadActionTypes: () => Promise; +} + +export type PropsWithOptionalApiHandlers = Omit & Partial; + +export function withActionOperations( + WrappedComponent: React.ComponentType +): React.FunctionComponent> { + return (props: PropsWithOptionalApiHandlers) => { + const { http } = useAppDependencies(); + return ( + loadActionTypes({ http })} /> + ); + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx new file mode 100644 index 00000000000000..30a065479ce33c --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx @@ -0,0 +1,269 @@ +/* + * 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 { shallow, mount } from 'enzyme'; +import uuid from 'uuid'; +import { withBulkAlertOperations, ComponentOpts } from './with_bulk_alert_api_operations'; +import * as alertApi from '../../../lib/alert_api'; +import { useAppDependencies } from '../../../app_context'; +import { Alert } from '../../../../types'; + +jest.mock('../../../lib/alert_api'); + +jest.mock('../../../app_context', () => { + const http = jest.fn(); + return { + useAppDependencies: jest.fn(() => ({ + http, + legacy: { + capabilities: { + get: jest.fn(() => ({})), + }, + }, + })), + }; +}); + +describe('with_bulk_alert_api_operations', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('extends any component with AlertApi methods', () => { + const ComponentToExtend = (props: ComponentOpts) => { + expect(typeof props.muteAlerts).toEqual('function'); + expect(typeof props.unmuteAlerts).toEqual('function'); + expect(typeof props.enableAlerts).toEqual('function'); + expect(typeof props.disableAlerts).toEqual('function'); + expect(typeof props.deleteAlerts).toEqual('function'); + expect(typeof props.muteAlert).toEqual('function'); + expect(typeof props.unmuteAlert).toEqual('function'); + expect(typeof props.enableAlert).toEqual('function'); + expect(typeof props.disableAlert).toEqual('function'); + expect(typeof props.deleteAlert).toEqual('function'); + expect(typeof props.loadAlert).toEqual('function'); + expect(typeof props.loadAlertTypes).toEqual('function'); + return
; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + expect(shallow().type()).toEqual(ComponentToExtend); + }); + + // single alert + it('muteAlert calls the muteAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ muteAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert(); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.muteAlert).toHaveBeenCalledTimes(1); + expect(alertApi.muteAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('unmuteAlert calls the unmuteAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ unmuteAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert({ muteAll: true }); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.unmuteAlert).toHaveBeenCalledTimes(1); + expect(alertApi.unmuteAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('enableAlert calls the muteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ enableAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert({ enabled: false }); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.enableAlert).toHaveBeenCalledTimes(1); + expect(alertApi.enableAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('disableAlert calls the disableAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ disableAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert(); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.disableAlert).toHaveBeenCalledTimes(1); + expect(alertApi.disableAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('deleteAlert calls the deleteAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ deleteAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert(); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.deleteAlert).toHaveBeenCalledTimes(1); + expect(alertApi.deleteAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + // bulk alerts + it('muteAlerts calls the muteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ muteAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert(), mockAlert()]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.muteAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.muteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http }); + }); + + it('unmuteAlerts calls the unmuteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ unmuteAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert({ muteAll: true }), mockAlert({ muteAll: true })]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.unmuteAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.unmuteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http }); + }); + + it('enableAlerts calls the muteAlertss api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ enableAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [ + mockAlert({ enabled: false }), + mockAlert({ enabled: true }), + mockAlert({ enabled: false }), + ]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.enableAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.enableAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[2].id], http }); + }); + + it('disableAlerts calls the disableAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ disableAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert(), mockAlert()]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.disableAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.disableAlerts).toHaveBeenCalledWith({ + ids: [alerts[0].id, alerts[1].id], + http, + }); + }); + + it('deleteAlerts calls the deleteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ deleteAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert(), mockAlert()]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.deleteAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.deleteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http }); + }); + + it('loadAlert calls the loadAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ + loadAlert, + alertId, + }: ComponentOpts & { alertId: Alert['id'] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alertId = uuid.v4(); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.loadAlert).toHaveBeenCalledTimes(1); + expect(alertApi.loadAlert).toHaveBeenCalledWith({ alertId, http }); + }); + + it('loadAlertTypes calls the loadAlertTypes api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ loadAlertTypes }: ComponentOpts) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.loadAlertTypes).toHaveBeenCalledTimes(1); + expect(alertApi.loadAlertTypes).toHaveBeenCalledWith({ http }); + }); +}); + +function mockAlert(overloads: Partial = {}): Alert { + return { + id: uuid.v4(), + enabled: true, + name: `alert-${uuid.v4()}`, + tags: [], + alertTypeId: '.noop', + consumer: 'consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + ...overloads, + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx new file mode 100644 index 00000000000000..c61ba631ab8685 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx @@ -0,0 +1,103 @@ +/* + * 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 { Alert, AlertType } from '../../../../types'; +import { useAppDependencies } from '../../../app_context'; +import { + deleteAlerts, + disableAlerts, + enableAlerts, + muteAlerts, + unmuteAlerts, + deleteAlert, + disableAlert, + enableAlert, + muteAlert, + unmuteAlert, + loadAlert, + loadAlertTypes, +} from '../../../lib/alert_api'; + +export interface ComponentOpts { + muteAlerts: (alerts: Alert[]) => Promise; + unmuteAlerts: (alerts: Alert[]) => Promise; + enableAlerts: (alerts: Alert[]) => Promise; + disableAlerts: (alerts: Alert[]) => Promise; + deleteAlerts: (alerts: Alert[]) => Promise; + muteAlert: (alert: Alert) => Promise; + unmuteAlert: (alert: Alert) => Promise; + enableAlert: (alert: Alert) => Promise; + disableAlert: (alert: Alert) => Promise; + deleteAlert: (alert: Alert) => Promise; + loadAlert: (id: Alert['id']) => Promise; + loadAlertTypes: () => Promise; +} + +export type PropsWithOptionalApiHandlers = Omit & Partial; + +export function withBulkAlertOperations( + WrappedComponent: React.ComponentType +): React.FunctionComponent> { + return (props: PropsWithOptionalApiHandlers) => { + const { http } = useAppDependencies(); + return ( + + muteAlerts({ http, ids: items.filter(item => !isAlertMuted(item)).map(item => item.id) }) + } + unmuteAlerts={async (items: Alert[]) => + unmuteAlerts({ http, ids: items.filter(isAlertMuted).map(item => item.id) }) + } + enableAlerts={async (items: Alert[]) => + enableAlerts({ http, ids: items.filter(isAlertDisabled).map(item => item.id) }) + } + disableAlerts={async (items: Alert[]) => + disableAlerts({ + http, + ids: items.filter(item => !isAlertDisabled(item)).map(item => item.id), + }) + } + deleteAlerts={async (items: Alert[]) => + deleteAlerts({ http, ids: items.map(item => item.id) }) + } + muteAlert={async (alert: Alert) => { + if (!isAlertMuted(alert)) { + return muteAlert({ http, id: alert.id }); + } + }} + unmuteAlert={async (alert: Alert) => { + if (isAlertMuted(alert)) { + return unmuteAlert({ http, id: alert.id }); + } + }} + enableAlert={async (alert: Alert) => { + if (isAlertDisabled(alert)) { + return enableAlert({ http, id: alert.id }); + } + }} + disableAlert={async (alert: Alert) => { + if (!isAlertDisabled(alert)) { + return disableAlert({ http, id: alert.id }); + } + }} + deleteAlert={async (alert: Alert) => deleteAlert({ http, id: alert.id })} + loadAlert={async (alertId: Alert['id']) => loadAlert({ http, alertId })} + loadAlertTypes={async () => loadAlertTypes({ http })} + /> + ); + }; +} + +function isAlertDisabled(alert: Alert) { + return alert.enabled === false; +} + +function isAlertMuted(alert: Alert) { + return alert.muteAll === true; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts index ed63ade903104c..7fb7d0bf48e4d2 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { TypeRegistry } from './application/type_registry'; -import { SanitizedAlert as Alert } from '../../../alerting/common'; -export { SanitizedAlert as Alert, AlertAction } from '../../../alerting/common'; +import { SanitizedAlert as Alert, AlertAction } from '../../../alerting/common'; +import { ActionType } from '../../../../../plugins/actions/common'; + +export { Alert, AlertAction }; +export { ActionType }; export type ActionTypeIndex = Record; export type AlertTypeIndex = Record; @@ -47,11 +50,6 @@ export interface ValidationResult { errors: Record; } -export interface ActionType { - id: string; - name: string; -} - export interface ActionConnector { secrets: Record; id: string; diff --git a/x-pack/legacy/plugins/uptime/common/constants/constants.ts b/x-pack/legacy/plugins/uptime/common/constants/constants.ts deleted file mode 100644 index 0c35bc97344864..00000000000000 --- a/x-pack/legacy/plugins/uptime/common/constants/constants.ts +++ /dev/null @@ -1,7 +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. - */ - -export const UNNAMED_LOCATION = 'Unnamed-location'; diff --git a/x-pack/legacy/plugins/uptime/common/constants/index.ts b/x-pack/legacy/plugins/uptime/common/constants/index.ts index 0a95960825f022..9d5ad4607491c3 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/index.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/index.ts @@ -11,4 +11,4 @@ export { INDEX_NAMES } from './index_names'; export * from './capabilities'; export { PLUGIN } from './plugin'; export { QUERY, STATES } from './query'; -export * from './constants'; +export * from './ui'; diff --git a/x-pack/legacy/plugins/uptime/common/constants/ui.ts b/x-pack/legacy/plugins/uptime/common/constants/ui.ts new file mode 100644 index 00000000000000..c91a2f66251942 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/constants/ui.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum STATUS { + UP = 'up', + DOWN = 'down', +} + +export const UNNAMED_LOCATION = 'Unnamed-location'; + +export const SHORT_TS_LOCALE = 'en-short-locale'; + +export const SHORT_TIMESPAN_LOCALE = { + relativeTime: { + future: 'in %s', + past: '%s ago', + s: '%ds', + ss: '%ss', + m: '%dm', + mm: '%dm', + h: '%dh', + hh: '%dh', + d: '%dd', + dd: '%dd', + M: '%d Mon', + MM: '%d Mon', + y: '%d Yr', + yy: '%d Yr', + }, +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap index 78b2bfdecb87af..c3b99c9785cbe0 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap @@ -5,6 +5,7 @@ exports[`MonitorBarSeries component renders a series when there are down items 1 style={ Object { "height": 50, + "marginRight": 15, "maxWidth": "1200px", "width": "100%", } diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx index a0cbdc59221237..ce91bf5b1638f4 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx @@ -53,7 +53,7 @@ export const MonitorBarSeries = ({ const id = 'downSeries'; return seriesHasDownValues(histogramSeries) ? ( -
+
{ const prevLocal: string = moment.locale() ?? 'en'; const renderTags = () => { - moment.defineLocale('en-tag', { - relativeTime: { - future: 'in %s', - past: '%s ago', - s: '%ds', - ss: '%ss', - m: '%dm', - mm: '%dm', - h: '%dh', - hh: '%dh', - d: '%dd', - dd: '%dd', - M: '%d Mon', - MM: '%d Mon', - y: '%d Yr', - yy: '%d Yr', - }, - }); + const shortLocale = moment.locale(SHORT_TS_LOCALE) === SHORT_TS_LOCALE; + if (!shortLocale) { + moment.defineLocale(SHORT_TS_LOCALE, SHORT_TIMESPAN_LOCALE); + } + const tags = ( {downLocations.map((item, ind) => tagLabel(item, ind, danger))} diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index 1de49f12236992..f779efca7b18a7 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -24,20 +24,24 @@ exports[`MonitorList component renders a no items message when no data is provid Object { "align": "left", "field": "state.monitor.status", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Status", "render": [Function], - "width": "20%", }, Object { "align": "left", "field": "state.monitor.name", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Name", "render": [Function], "sortable": true, - "width": "30%", }, Object { - "aligh": "left", + "align": "left", "field": "state.url.full", "name": "Url", "render": [Function], @@ -58,6 +62,7 @@ exports[`MonitorList component renders a no items message when no data is provid "name": "", "render": [Function], "sortable": true, + "width": "24px", }, ] } @@ -83,6 +88,7 @@ exports[`MonitorList component renders a no items message when no data is provid @@ -122,20 +129,24 @@ exports[`MonitorList component renders the monitor list 1`] = ` Object { "align": "left", "field": "state.monitor.status", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Status", "render": [Function], - "width": "20%", }, Object { "align": "left", "field": "state.monitor.name", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Name", "render": [Function], "sortable": true, - "width": "30%", }, Object { - "aligh": "left", + "align": "left", "field": "state.url.full", "name": "Url", "render": [Function], @@ -156,6 +167,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` "name": "", "render": [Function], "sortable": true, + "width": "24px", }, ] } @@ -243,6 +255,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap index aa9d3a3ed0d8c8..03a5a15eea1a47 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap @@ -24,20 +24,24 @@ exports[`MonitorList component renders a no items message when no data is provid Object { "align": "left", "field": "state.monitor.status", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Status", "render": [Function], - "width": "20%", }, Object { "align": "left", "field": "state.monitor.name", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Name", "render": [Function], "sortable": true, - "width": "30%", }, Object { - "aligh": "left", + "align": "left", "field": "state.url.full", "name": "Url", "render": [Function], @@ -58,6 +62,7 @@ exports[`MonitorList component renders a no items message when no data is provid "name": "", "render": [Function], "sortable": true, + "width": "24px", }, ] } @@ -83,6 +88,7 @@ exports[`MonitorList component renders a no items message when no data is provid @@ -122,20 +129,24 @@ exports[`MonitorList component renders the monitor list 1`] = ` Object { "align": "left", "field": "state.monitor.status", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Status", "render": [Function], - "width": "20%", }, Object { "align": "left", "field": "state.monitor.name", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Name", "render": [Function], "sortable": true, - "width": "30%", }, Object { - "aligh": "left", + "align": "left", "field": "state.url.full", "name": "Url", "render": [Function], @@ -156,6 +167,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` "name": "", "render": [Function], "sortable": true, + "width": "24px", }, ] } diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap index 69a0616a7bed8b..f3b90fbf6ee913 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap @@ -1,11 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`MonitorListStatusColumn can handle a non-numeric timestamp value 1`] = ` - - + Up - + + Thu May 09 2019 10:15:11 GMT-0400 + + } + delay="regular" + position="top" + > - Thu May 09 2019 10:15:11 GMT-0400 + a few seconds ago - } - delay="regular" - position="top" + + + + + - - a few seconds ago - - + in 0/0 Location + - + `; exports[`MonitorListStatusColumn provides expected tooltip and display times 1`] = ` - - + Up - + + Thu May 09 2019 10:15:11 GMT-0400 + + } + delay="regular" + position="top" + > - Thu May 09 2019 10:15:11 GMT-0400 + a few seconds ago + + + + + + in 0/0 Location + + + +`; + +exports[`MonitorListStatusColumn will display location status 1`] = ` + + + - + + + Thu May 09 2019 10:15:11 GMT-0400 + + } + delay="regular" + position="top" > - a few seconds ago - - + + a few seconds ago + + + + + + + in 1/3 Locations + - + +`; + +exports[`MonitorListStatusColumn will render display location status 1`] = ` +.c1 { + padding-left: 17px; +} + +@media (max-width:574px) { + .c0 { + min-width: 230px; + } +} + +
+
+
+
+
+ +
+
+ Up +
+
+
+ + +
+
+ a few seconds ago +
+
+
+
+
+
+
+ in 1/3 Locations +
+
+
`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_status_column.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_status_column.test.tsx index 2a834377fee8e3..406e18535f34c3 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_status_column.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_status_column.test.tsx @@ -6,8 +6,10 @@ import React from 'react'; import moment from 'moment'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { MonitorListStatusColumn } from '../monitor_list_status_column'; +import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { getLocationStatus, MonitorListStatusColumn } from '../monitor_list_status_column'; +import { Check } from '../../../../../common/graphql/types'; +import { STATUS } from '../../../../../common/constants'; describe('MonitorListStatusColumn', () => { beforeAll(() => { @@ -18,15 +20,246 @@ describe('MonitorListStatusColumn', () => { Date.prototype.toString = jest.fn(() => 'Tue, 01 Jan 2019 00:00:00 GMT'); }); + let upChecks: Check[]; + + let downChecks: Check[]; + + let checks: Check[]; + + beforeEach(() => { + upChecks = [ + { + agent: { id: '6a2f2a1c-e346-49ed-8418-6d48af8841d6' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'up', + }, + observer: { + geo: { + name: 'Berlin', + location: { + lat: 40.73060997761786, + lon: -73.93524203449488, + }, + }, + }, + timestamp: '1579794631464', + }, + { + agent: { id: '1117fd01-bc1a-4aa5-bfab-40ab455eadf9' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'up', + }, + observer: { + geo: { + name: 'Islamabad', + location: { + lat: 40.73060997761786, + lon: -73.93524203449488, + }, + }, + }, + timestamp: '1579794634220', + }, + { + agent: { id: 'eda59510-45e8-4dfe-b0f8-959c449e3565' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'up', + }, + observer: { + geo: { + name: 'st-paul', + location: { + lat: 52.48744798824191, + lon: 13.394797928631306, + }, + }, + }, + timestamp: '1579794628368', + }, + ]; + + downChecks = [ + { + agent: { id: '6a2f2a1c-e346-49ed-8418-6d48af8841d6' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'down', + }, + observer: { + geo: { + name: 'Berlin', + location: { + lat: 40.73060997761786, + lon: -73.93524203449488, + }, + }, + }, + timestamp: '1579794631464', + }, + { + agent: { id: '1117fd01-bc1a-4aa5-bfab-40ab455eadf9' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'down', + }, + observer: { + geo: { + name: 'Islamabad', + location: { + lat: 40.73060997761786, + lon: -73.93524203449488, + }, + }, + }, + timestamp: '1579794634220', + }, + { + agent: { id: 'eda59510-45e8-4dfe-b0f8-959c449e3565' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'down', + }, + observer: { + geo: { + name: 'st-paul', + location: { + lat: 52.48744798824191, + lon: 13.394797928631306, + }, + }, + }, + timestamp: '1579794628368', + }, + ]; + + checks = [ + { + agent: { id: '6a2f2a1c-e346-49ed-8418-6d48af8841d6' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'up', + }, + observer: { + geo: { + name: 'Berlin', + location: { + lat: 40.73060997761786, + lon: -73.93524203449488, + }, + }, + }, + timestamp: '1579794631464', + }, + { + agent: { id: '1117fd01-bc1a-4aa5-bfab-40ab455eadf9' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'down', + }, + observer: { + geo: { + name: 'Islamabad', + location: { + lat: 40.73060997761786, + lon: -73.93524203449488, + }, + }, + }, + timestamp: '1579794634220', + }, + { + agent: { id: 'eda59510-45e8-4dfe-b0f8-959c449e3565' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'down', + }, + observer: { + geo: { + name: 'st-paul', + location: { + lat: 52.48744798824191, + lon: 13.394797928631306, + }, + }, + }, + timestamp: '1579794628368', + }, + ]; + }); + it('provides expected tooltip and display times', () => { - const component = shallowWithIntl(); + const component = shallowWithIntl( + + ); expect(component).toMatchSnapshot(); }); it('can handle a non-numeric timestamp value', () => { const component = shallowWithIntl( - + + ); + expect(component).toMatchSnapshot(); + }); + + it('will display location status', () => { + const component = shallowWithIntl( + ); expect(component).toMatchSnapshot(); }); + + it('will render display location status', () => { + const component = renderWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); + + it(' will test getLocationStatus location', () => { + let statusMessage = getLocationStatus(checks, STATUS.UP); + + expect(statusMessage).toBe('in 1/3 Locations'); + + statusMessage = getLocationStatus(checks, STATUS.DOWN); + + expect(statusMessage).toBe('in 2/3 Locations'); + + statusMessage = getLocationStatus(upChecks, STATUS.UP); + + expect(statusMessage).toBe('in 3/3 Locations'); + + statusMessage = getLocationStatus(downChecks, STATUS.UP); + + expect(statusMessage).toBe('in 0/3 Locations'); + }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx index 1d0930f1faaefc..c8385440a7d49a 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx @@ -16,8 +16,7 @@ import { EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { get } from 'lodash'; -import React, { useState, Fragment } from 'react'; +import React, { useState } from 'react'; import styled from 'styled-components'; import { withUptimeGraphQL, UptimeGraphQLQueryProps } from '../../higher_order'; import { monitorStatesQuery } from '../../../queries/monitor_states_query'; @@ -68,10 +67,11 @@ export const MonitorListComponent = (props: Props) => { loading, } = props; const [drawerIds, updateDrawerIds] = useState([]); - const items = get(data, 'monitorStates.summaries', []); - const nextPagePagination = get(data, 'monitorStates.nextPagePagination'); - const prevPagePagination = get(data, 'monitorStates.prevPagePagination'); + const items = data?.monitorStates?.summaries ?? []; + + const nextPagePagination = data?.monitorStates?.nextPagePagination ?? ''; + const prevPagePagination = data?.monitorStates?.prevPagePagination ?? ''; const getExpandedRowMap = () => { return drawerIds.reduce((map: ExpandedRowMap, id: string) => { @@ -89,18 +89,24 @@ export const MonitorListComponent = (props: Props) => { const columns = [ { align: 'left' as const, - width: '20%', field: 'state.monitor.status', name: labels.STATUS_COLUMN_LABEL, - render: (status: string, { state: { timestamp } }: MonitorSummary) => { - return ; + mobileOptions: { + fullWidth: true, + }, + render: (status: string, { state: { timestamp, checks } }: MonitorSummary) => { + return ( + + ); }, }, { align: 'left' as const, - width: '30%', field: 'state.monitor.name', name: labels.NAME_COLUMN_LABEL, + mobileOptions: { + fullWidth: true, + }, render: (name: string, summary: MonitorSummary) => ( {name ? name : `Unnamed - ${summary.monitor_id}`} @@ -109,7 +115,7 @@ export const MonitorListComponent = (props: Props) => { sortable: true, }, { - aligh: 'left' as const, + align: 'left' as const, field: 'state.url.full', name: labels.URL, render: (url: string, summary: MonitorSummary) => ( @@ -140,6 +146,7 @@ export const MonitorListComponent = (props: Props) => { name: '', sortable: true, isExpander: true, + width: '24px', render: (id: string) => { return ( { ]; return ( - + <>
@@ -211,7 +218,7 @@ export const MonitorListComponent = (props: Props) => { - + ); }; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_list.tsx index 04c5dc7d713714..a2042e379dd80f 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_list.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_list.tsx @@ -11,7 +11,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Check } from '../../../../../common/graphql/types'; import { LocationLink } from './location_link'; import { MonitorStatusRow } from './monitor_status_row'; -import { UNNAMED_LOCATION } from '../../../../../common/constants'; +import { STATUS, UNNAMED_LOCATION } from '../../../../../common/constants'; interface MonitorStatusListProps { /** @@ -20,9 +20,6 @@ interface MonitorStatusListProps { checks: Check[]; } -export const UP = 'up'; -export const DOWN = 'down'; - export const MonitorStatusList = ({ checks }: MonitorStatusListProps) => { const upChecks: Set = new Set(); const downChecks: Set = new Set(); @@ -31,9 +28,9 @@ export const MonitorStatusList = ({ checks }: MonitorStatusListProps) => { // Doing this way because name is either string or null, get() default value only works on undefined value const location = get(check, 'observer.geo.name', null) || UNNAMED_LOCATION; - if (check.monitor.status === UP) { + if (check.monitor.status === STATUS.UP) { upChecks.add(capitalize(location)); - } else if (check.monitor.status === DOWN) { + } else if (check.monitor.status === STATUS.DOWN) { downChecks.add(capitalize(location)); } }); @@ -43,8 +40,8 @@ export const MonitorStatusList = ({ checks }: MonitorStatusListProps) => { return ( <> - - + + {(downChecks.has(UNNAMED_LOCATION) || upChecks.has(UNNAMED_LOCATION)) && ( <> diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_row.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_row.tsx index e724986c2505e2..50028e1ddea180 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_row.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_row.tsx @@ -8,8 +8,7 @@ import React, { useContext } from 'react'; import { EuiHealth, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { UptimeThemeContext } from '../../../../contexts'; -import { UP } from './monitor_status_list'; -import { UNNAMED_LOCATION } from '../../../../../common/constants'; +import { UNNAMED_LOCATION, STATUS } from '../../../../../common/constants'; interface MonitorStatusRowProps { /** @@ -27,7 +26,7 @@ export const MonitorStatusRow = ({ locationNames, status }: MonitorStatusRowProp colors: { success, danger }, } = useContext(UptimeThemeContext); - const color = status === UP ? success : danger; + const color = status === STATUS.UP ? success : danger; let checkListArray = [...locationNames]; // If un-named location exists, move it to end @@ -44,7 +43,7 @@ export const MonitorStatusRow = ({ locationNames, status }: MonitorStatusRowProp return ( <> - {status === UP ? ( + {status === STATUS.UP ? ( { switch (status) { - case 'up': + case STATUS.UP: return 'success'; - case 'down': + case STATUS.DOWN: return 'danger'; default: return ''; @@ -27,42 +50,98 @@ const getHealthColor = (status: string): string => { const getHealthMessage = (status: string): string | null => { switch (status) { - case 'up': - return i18n.translate('xpack.uptime.monitorList.statusColumn.upLabel', { - defaultMessage: 'Up', - }); - case 'down': - return i18n.translate('xpack.uptime.monitorList.statusColumn.downLabel', { - defaultMessage: 'Down', - }); + case STATUS.UP: + return labels.UP; + case STATUS.DOWN: + return labels.DOWN; default: return null; } }; +const getRelativeShortTimeStamp = (timeStamp: any) => { + const prevLocale: string = moment.locale() ?? 'en'; + + const shortLocale = moment.locale(SHORT_TS_LOCALE) === SHORT_TS_LOCALE; + + if (!shortLocale) { + moment.defineLocale(SHORT_TS_LOCALE, SHORT_TIMESPAN_LOCALE); + } + + const shortTimestamp = parseTimestamp(timeStamp).fromNow(); + + // Reset it so, it does't impact other part of the app + moment.locale(prevLocale); + return shortTimestamp; +}; + +export const getLocationStatus = (checks: Check[], status: string) => { + const upChecks: Set = new Set(); + const downChecks: Set = new Set(); + + checks.forEach((check: Check) => { + const location = check?.observer?.geo?.name ?? UNNAMED_LOCATION; + + if (check.monitor.status === STATUS.UP) { + upChecks.add(capitalize(location)); + } else if (check.monitor.status === STATUS.DOWN) { + downChecks.add(capitalize(location)); + } + }); + + // if monitor is down in one dns, it will be considered down so removing it from up list + const absUpChecks: Set = new Set([...upChecks].filter(item => !downChecks.has(item))); + + const totalLocations = absUpChecks.size + downChecks.size; + let statusMessage = ''; + if (status === STATUS.DOWN) { + statusMessage = `${downChecks.size}/${totalLocations}`; + } else { + statusMessage = `${absUpChecks.size}/${totalLocations}`; + } + + if (totalLocations > 1) { + return i18n.translate('xpack.uptime.monitorList.statusColumn.locStatusMessage.multiple', { + defaultMessage: 'in {noLoc} Locations', + values: { noLoc: statusMessage }, + }); + } + + return i18n.translate('xpack.uptime.monitorList.statusColumn.locStatusMessage', { + defaultMessage: 'in {noLoc} Location', + values: { noLoc: statusMessage }, + }); +}; + export const MonitorListStatusColumn = ({ status, + checks = [], timestamp: tsString, }: MonitorListStatusColumnProps) => { const timestamp = parseTimestamp(tsString); return ( - - + + {getHealthMessage(status)} - - {timestamp.toLocaleString()} + + + {timestamp.toLocaleString()} + + } + > + + {getRelativeShortTimeStamp(tsString)} - } - > - - {timestamp.fromNow()} - - + + + + + {getLocationStatus(checks, status)} - + ); }; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/translations.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/translations.ts index beacdec1ae2654..5252d90215e954 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/translations.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/translations.ts @@ -56,3 +56,11 @@ export const NO_DATA_MESSAGE = i18n.translate('xpack.uptime.monitorList.noItemMe export const URL = i18n.translate('xpack.uptime.monitorList.table.url.name', { defaultMessage: 'Url', }); + +export const UP = i18n.translate('xpack.uptime.monitorList.statusColumn.upLabel', { + defaultMessage: 'Up', +}); + +export const DOWN = i18n.translate('xpack.uptime.monitorList.statusColumn.downLabel', { + defaultMessage: 'Down', +}); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts index c1382d455313f0..f01448d9e37acd 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts @@ -84,7 +84,8 @@ export const getSupportedUrlParams = (params: { ), absoluteDateRangeEnd: parseAbsoluteDate( dateRangeEnd || DATE_RANGE_END, - ABSOLUTE_DATE_RANGE_END + ABSOLUTE_DATE_RANGE_END, + { roundUp: true } ), autorefreshInterval: parseUrlInt(autorefreshInterval, AUTOREFRESH_INTERVAL), autorefreshIsPaused: parseIsPaused(autorefreshIsPaused, AUTOREFRESH_IS_PAUSED), diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts index eaf720a8d2f7e1..2b0921f07abc97 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts @@ -6,8 +6,8 @@ import DateMath from '@elastic/datemath'; -export const parseAbsoluteDate = (date: string, defaultValue: number): number => { - const momentWrapper = DateMath.parse(date); +export const parseAbsoluteDate = (date: string, defaultValue: number, options = {}): number => { + const momentWrapper = DateMath.parse(date, options); if (momentWrapper) { return momentWrapper.valueOf(); } diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts index e34bc6ab805c06..634d6369531d8c 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts @@ -25,7 +25,6 @@ export const findPotentialMatches = async ( size: number ) => { const queryResult = await query(queryContext, searchAfter, size); - const checkGroups = new Set(); const monitorIds: string[] = []; get(queryResult, 'aggregations.monitors.buckets', []).forEach((b: any) => { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts index d97b7653402a32..961cc94dcea19f 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import DateMath from '@elastic/datemath'; import { APICaller } from 'kibana/server'; import { CursorPagination } from '../adapter_types'; import { INDEX_NAMES } from '../../../../../common/constants'; +import { parseRelativeDate } from '../../../helper/get_histogram_interval'; export class QueryContext { callES: APICaller; @@ -95,8 +95,9 @@ export class QueryContext { // latencies and slowdowns that's dangerous. Making this value larger makes things // only slower, but only marginally so, and prevents people from seeing weird // behavior. - const tsStart = DateMath.parse(this.dateRangeEnd)!.subtract(5, 'minutes'); - const tsEnd = DateMath.parse(this.dateRangeEnd)!; + + const tsEnd = parseRelativeDate(this.dateRangeEnd, { roundUp: true })!; + const tsStart = tsEnd.subtract(5, 'minutes'); return { range: { diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts new file mode 100644 index 00000000000000..ec6e48c62c62e0 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts @@ -0,0 +1,23 @@ +/* + * 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 { parseRelativeDate } from '../get_histogram_interval'; +import { Moment } from 'moment'; + +describe('Parsing a relative end date properly', () => { + it('converts the upper range of relative end dates to now', async () => { + const thisWeekEndDate = 'now/w'; + + let endDate = parseRelativeDate(thisWeekEndDate, { roundUp: true }); + expect(Date.now() - (endDate as Moment).valueOf()).toBeLessThan(1000); + + const todayEndDate = 'now/d'; + + endDate = parseRelativeDate(todayEndDate, { roundUp: true }); + + expect(Date.now() - (endDate as Moment).valueOf()).toBeLessThan(1000); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts index 0dedc3e456f513..26515fb4b4c63b 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts @@ -7,18 +7,39 @@ import DateMath from '@elastic/datemath'; import { QUERY } from '../../../common/constants'; +export const parseRelativeDate = (dateStr: string, options = {}) => { + // We need this this parsing because if user selects This week or this date + // That represents end date in future, if week or day is still in the middle + // Uptime data can never be collected in future, so we will reset date to now + // in That case. Example case we select this week range will be to='now/w' and from = 'now/w'; + + const parsedDate = DateMath.parse(dateStr, options); + const dateTimestamp = parsedDate?.valueOf() ?? 0; + if (dateTimestamp > Date.now()) { + return DateMath.parse('now'); + } + return parsedDate; +}; + export const getHistogramInterval = ( dateRangeStart: string, dateRangeEnd: string, bucketCount?: number ): number => { - const from = DateMath.parse(dateRangeStart); - const to = DateMath.parse(dateRangeEnd); + const from = parseRelativeDate(dateRangeStart); + + // roundUp is required for relative date like now/w to get the end of the week + const to = parseRelativeDate(dateRangeEnd, { roundUp: true }); if (from === undefined) { throw Error('Invalid dateRangeStart value'); } if (to === undefined) { throw Error('Invalid dateRangeEnd value'); } - return Math.round((to.valueOf() - from.valueOf()) / (bucketCount || QUERY.DEFAULT_BUCKET_COUNT)); + const interval = Math.round( + (to.valueOf() - from.valueOf()) / (bucketCount || QUERY.DEFAULT_BUCKET_COUNT) + ); + + // Interval can never be zero, if it's 0 we return at least 1ms interval + return interval > 0 ? interval : 1; }; diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index be6916a74fe88d..03a892a42792e2 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -12,7 +12,7 @@ import { GetServicesFunction, RawAction, } from '../types'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { SpacesServiceSetup } from '../../../spaces/server'; import { EVENT_LOG_ACTIONS } from '../plugin'; import { IEvent, IEventLogger } from '../../../event_log/server'; @@ -21,7 +21,7 @@ export interface ActionExecutorContext { logger: Logger; spaces?: SpacesServiceSetup; getServices: GetServicesFunction; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; actionTypeRegistry: ActionTypeRegistryContract; eventLogger: IEventLogger; } diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index c3e89e0c16efcc..c78b43f4ef3ba3 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -8,12 +8,12 @@ import { ActionExecutorContract } from './action_executor'; import { ExecutorError } from './executor_error'; import { Logger, CoreStart } from '../../../../../src/core/server'; import { RunContext } from '../../../task_manager/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { ActionTaskParams, GetBasePathFunction, SpaceIdToNamespaceFunction } from '../types'; export interface TaskRunnerContext { logger: Logger; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceIdToNamespace: SpaceIdToNamespaceFunction; getBasePath: GetBasePathFunction; getScopedSavedObjectsClient: CoreStart['savedObjects']['getScopedClient']; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index cb0e3347541fd7..dab09fc455ecf6 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -20,8 +20,8 @@ import { } from '../../../../src/core/server'; import { - PluginSetupContract as EncryptedSavedObjectsSetupContract, - PluginStartContract as EncryptedSavedObjectsStartContract, + EncryptedSavedObjectsPluginSetup, + EncryptedSavedObjectsPluginStart, } from '../../encrypted_saved_objects/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { LicensingPluginSetup } from '../../licensing/server'; @@ -67,13 +67,13 @@ export interface PluginStartContract { export interface ActionsPluginsSetup { taskManager: TaskManagerSetupContract; - encryptedSavedObjects: EncryptedSavedObjectsSetupContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; licensing: LicensingPluginSetup; spaces?: SpacesPluginSetup; event_log: IEventLogService; } export interface ActionsPluginsStart { - encryptedSavedObjects: EncryptedSavedObjectsStartContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; taskManager: TaskManagerStartContract; } diff --git a/x-pack/plugins/canvas/server/routes/index.ts b/x-pack/plugins/canvas/server/routes/index.ts index e9afab5680332d..fce278e94bf32f 100644 --- a/x-pack/plugins/canvas/server/routes/index.ts +++ b/x-pack/plugins/canvas/server/routes/index.ts @@ -5,9 +5,10 @@ */ import { IRouter, Logger } from 'src/core/server'; -import { initWorkpadRoutes } from './workpad'; import { initCustomElementsRoutes } from './custom_elements'; import { initESFieldsRoutes } from './es_fields'; +import { initShareablesRoutes } from './shareables'; +import { initWorkpadRoutes } from './workpad'; export interface RouteInitializerDeps { router: IRouter; @@ -15,7 +16,8 @@ export interface RouteInitializerDeps { } export function initRoutes(deps: RouteInitializerDeps) { - initWorkpadRoutes(deps); initCustomElementsRoutes(deps); initESFieldsRoutes(deps); + initShareablesRoutes(deps); + initWorkpadRoutes(deps); } diff --git a/x-pack/plugins/canvas/server/routes/shareables/download.test.ts b/x-pack/plugins/canvas/server/routes/shareables/download.test.ts new file mode 100644 index 00000000000000..be4765217d7aa8 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/download.test.ts @@ -0,0 +1,55 @@ +/* + * 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. + */ + +jest.mock('fs'); + +import fs from 'fs'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { httpServiceMock, httpServerMock, loggingServiceMock } from 'src/core/server/mocks'; +import { initializeDownloadShareableWorkpadRoute } from './download'; + +const mockRouteContext = {} as RequestHandlerContext; +const path = `api/canvas/workpad/find`; +const mockRuntime = 'Canvas shareable runtime'; + +describe('Download Canvas shareables runtime', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeDownloadShareableWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it(`returns 200 with canvas shareables runtime`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path, + }); + + const readFileSyncMock = fs.readFileSync as jest.Mock; + readFileSyncMock.mockReturnValueOnce(mockRuntime); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(`"Canvas shareable runtime"`); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/shareables/download.ts b/x-pack/plugins/canvas/server/routes/shareables/download.ts new file mode 100644 index 00000000000000..08bec1e4881aeb --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/download.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { readFileSync } from 'fs'; +import { SHAREABLE_RUNTIME_FILE } from '../../../../../legacy/plugins/canvas/shareable_runtime/constants'; +import { RouteInitializerDeps } from '../'; +import { API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD } from '../../../../../legacy/plugins/canvas/common/lib/constants'; + +export function initializeDownloadShareableWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD, + validate: false, + }, + async (_context, _request, response) => { + // TODO: check if this is still an issue on cloud after migrating to NP + // + // The option setting is not for typical use. We're using it here to avoid + // problems in Cloud environments. See elastic/kibana#47405. + // @ts-ignore No type for inert Hapi handler + // const file = handler.file(SHAREABLE_RUNTIME_FILE, { confine: false }); + const file = readFileSync(SHAREABLE_RUNTIME_FILE); + return response.ok({ + headers: { 'content-type': 'application/octet-stream' }, + body: file, + }); + } + ); +} diff --git a/x-pack/plugins/canvas/server/routes/shareables/index.ts b/x-pack/plugins/canvas/server/routes/shareables/index.ts new file mode 100644 index 00000000000000..0aabd8b955b211 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteInitializerDeps } from '../'; +import { initializeZipShareableWorkpadRoute } from './zip'; +import { initializeDownloadShareableWorkpadRoute } from './download'; + +export function initShareablesRoutes(deps: RouteInitializerDeps) { + initializeDownloadShareableWorkpadRoute(deps); + initializeZipShareableWorkpadRoute(deps); +} diff --git a/x-pack/plugins/canvas/server/routes/shareables/mock_shareable_workpad.json b/x-pack/plugins/canvas/server/routes/shareables/mock_shareable_workpad.json new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/x-pack/plugins/canvas/server/routes/shareables/rendered_workpad_schema.ts b/x-pack/plugins/canvas/server/routes/shareables/rendered_workpad_schema.ts new file mode 100644 index 00000000000000..792200354724e4 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/rendered_workpad_schema.ts @@ -0,0 +1,77 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +export const PositionSchema = schema.object({ + angle: schema.number(), + height: schema.number(), + left: schema.number(), + parent: schema.nullable(schema.string()), + top: schema.number(), + width: schema.number(), +}); + +export const ContainerStyleSchema = schema.object({ + type: schema.maybe(schema.string()), + border: schema.maybe(schema.string()), + borderRadius: schema.maybe(schema.string()), + padding: schema.maybe(schema.string()), + backgroundColor: schema.maybe(schema.string()), + backgroundImage: schema.maybe(schema.string()), + backgroundSize: schema.maybe(schema.string()), + backgroundRepeat: schema.maybe(schema.string()), + opacity: schema.maybe(schema.number()), + overflow: schema.maybe(schema.string()), +}); + +export const RenderableSchema = schema.object({ + error: schema.nullable(schema.string()), + state: schema.string(), + value: schema.object({ + as: schema.string(), + containerStyle: ContainerStyleSchema, + css: schema.maybe(schema.string()), + type: schema.string(), + value: schema.any(), + }), +}); + +export const RenderedWorkpadElementSchema = schema.object({ + expressionRenderable: RenderableSchema, + id: schema.string(), + position: PositionSchema, +}); + +export const RenderedWorkpadPageSchema = schema.object({ + id: schema.string(), + elements: schema.arrayOf(RenderedWorkpadElementSchema), + groups: schema.maybe(schema.arrayOf(schema.arrayOf(RenderedWorkpadElementSchema))), + style: schema.recordOf(schema.string(), schema.string()), + transition: schema.maybe( + schema.oneOf([ + schema.object({}), + schema.object({ + name: schema.string(), + }), + ]) + ), +}); + +export const RenderedWorkpadSchema = schema.object({ + '@created': schema.maybe(schema.string()), + '@timestamp': schema.maybe(schema.string()), + assets: schema.maybe(schema.recordOf(schema.string(), RenderedWorkpadPageSchema)), + colors: schema.arrayOf(schema.string()), + css: schema.string(), + height: schema.number(), + id: schema.string(), + isWriteable: schema.maybe(schema.boolean()), + name: schema.string(), + page: schema.number(), + pages: schema.arrayOf(RenderedWorkpadPageSchema), + width: schema.number(), +}); diff --git a/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts b/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts new file mode 100644 index 00000000000000..edb59694a74005 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts @@ -0,0 +1,78 @@ +/* + * 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. + */ + +jest.mock('archiver'); + +const archiver = require('archiver') as jest.Mock; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { httpServiceMock, httpServerMock, loggingServiceMock } from 'src/core/server/mocks'; +import { initializeZipShareableWorkpadRoute } from './zip'; +import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../legacy/plugins/canvas/common/lib'; +import { + SHAREABLE_RUNTIME_FILE, + SHAREABLE_RUNTIME_SRC, + SHAREABLE_RUNTIME_NAME, +} from '../../../../../legacy/plugins/canvas/shareable_runtime/constants'; + +const mockRouteContext = {} as RequestHandlerContext; +const mockWorkpad = {}; +const routePath = API_ROUTE_SHAREABLE_ZIP; + +describe('Zips Canvas shareables runtime together with workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeZipShareableWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it(`returns 200 with zip file with runtime and workpad`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: routePath, + body: mockWorkpad, + }); + + const mockArchive = { + append: jest.fn(), + file: jest.fn(), + finalize: jest.fn(), + }; + + archiver.mockReturnValueOnce(mockArchive); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toBe(mockArchive); + expect(mockArchive.append).toHaveBeenCalledWith(JSON.stringify(mockWorkpad), { + name: 'workpad.json', + }); + expect(mockArchive.file).toHaveBeenCalledTimes(2); + expect(mockArchive.file).nthCalledWith(1, `${SHAREABLE_RUNTIME_SRC}/template.html`, { + name: 'index.html', + }); + expect(mockArchive.file).nthCalledWith(2, SHAREABLE_RUNTIME_FILE, { + name: `${SHAREABLE_RUNTIME_NAME}.js`, + }); + expect(mockArchive.finalize).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/shareables/zip.ts b/x-pack/plugins/canvas/server/routes/shareables/zip.ts new file mode 100644 index 00000000000000..e25b96cce96ff3 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/zip.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import archiver from 'archiver'; +import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../legacy/plugins/canvas/common/lib'; +import { + SHAREABLE_RUNTIME_FILE, + SHAREABLE_RUNTIME_NAME, + SHAREABLE_RUNTIME_SRC, +} from '../../../../../legacy/plugins/canvas/shareable_runtime/constants'; +import { RenderedWorkpadSchema } from './rendered_workpad_schema'; +import { RouteInitializerDeps } from '..'; + +export function initializeZipShareableWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.post( + { + path: API_ROUTE_SHAREABLE_ZIP, + validate: { body: RenderedWorkpadSchema }, + }, + async (_context, request, response) => { + const workpad = request.body; + const archive = archiver('zip'); + archive.append(JSON.stringify(workpad), { name: 'workpad.json' }); + archive.file(`${SHAREABLE_RUNTIME_SRC}/template.html`, { name: 'index.html' }); + archive.file(SHAREABLE_RUNTIME_FILE, { name: `${SHAREABLE_RUNTIME_NAME}.js` }); + + const result = { headers: { 'content-type': 'application/zip' }, body: archive }; + archive.finalize(); + + return response.ok(result); + } + ); +} diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index c52461cade0586..37d087433a2ed1 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -9,7 +9,7 @@ import { CoreSetup, Logger, PluginInitializerContext } from 'kibana/server'; import { ConfigType } from './config'; import { initCaseApi } from './routes/api'; import { CaseService } from './services'; -import { PluginSetupContract as SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup } from '../../security/server'; function createConfig$(context: PluginInitializerContext) { return context.config.create().pipe(map(config => config)); diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index 684d905a5c71fe..531d5fa5b87e5b 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -21,10 +21,7 @@ import { UpdatedCaseType, UpdatedCommentType, } from '../routes/api/types'; -import { - AuthenticatedUser, - PluginSetupContract as SecurityPluginSetup, -} from '../../../security/server'; +import { AuthenticatedUser, SecurityPluginSetup } from '../../../security/server'; interface ClientArgs { client: SavedObjectsClientContract; diff --git a/x-pack/plugins/encrypted_saved_objects/server/config.test.ts b/x-pack/plugins/encrypted_saved_objects/server/config.test.ts index 7d6632aa56cb18..e05d8d687d05a3 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/config.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/config.test.ts @@ -49,7 +49,7 @@ describe('config schema', () => { }); describe('createConfig$()', () => { - it('should log a warning and set xpack.encryptedSavedObjects.encryptionKey if not set', async () => { + it('should log a warning, set xpack.encryptedSavedObjects.encryptionKey and usingEphemeralEncryptionKey=true when encryptionKey is not set', async () => { const mockRandomBytes = jest.requireMock('crypto').randomBytes; mockRandomBytes.mockReturnValue('ab'.repeat(16)); @@ -57,7 +57,10 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'ab'.repeat(16) }); + expect(config).toEqual({ + config: { encryptionKey: 'ab'.repeat(16) }, + usingEphemeralEncryptionKey: true, + }); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -67,4 +70,19 @@ describe('createConfig$()', () => { ] `); }); + + it('should not log a warning and set usingEphemeralEncryptionKey=false when encryptionKey is set', async () => { + const contextMock = coreMock.createPluginInitializerContext({ + encryptionKey: 'supersecret', + }); + const config = await createConfig$(contextMock) + .pipe(first()) + .toPromise(); + expect(config).toEqual({ + config: { encryptionKey: 'supersecret' }, + usingEphemeralEncryptionKey: false, + }); + + expect(loggingServiceMock.collect(contextMock.logger).warn).toEqual([]); + }); }); diff --git a/x-pack/plugins/encrypted_saved_objects/server/config.ts b/x-pack/plugins/encrypted_saved_objects/server/config.ts index c755b7dd9f205f..2f018505207242 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/config.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/config.ts @@ -5,15 +5,10 @@ */ import crypto from 'crypto'; -import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from 'src/core/server'; -export type ConfigType = ReturnType extends Observable - ? P - : ReturnType; - export const ConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), encryptionKey: schema.conditional( @@ -30,6 +25,7 @@ export function createConfig$(context: PluginInitializerContext) { const logger = context.logger.get('config'); let encryptionKey = config.encryptionKey; + const usingEphemeralEncryptionKey = encryptionKey === undefined; if (encryptionKey === undefined) { logger.warn( 'Generating a random key for xpack.encryptedSavedObjects.encryptionKey. ' + @@ -40,7 +36,10 @@ export function createConfig$(context: PluginInitializerContext) { encryptionKey = crypto.randomBytes(16).toString('hex'); } - return { ...config, encryptionKey }; + return { + config: { ...config, encryptionKey }, + usingEphemeralEncryptionKey, + }; }) ); } diff --git a/x-pack/plugins/encrypted_saved_objects/server/index.ts b/x-pack/plugins/encrypted_saved_objects/server/index.ts index 5e6edb95ec37a6..3b4b91de355c74 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/index.ts @@ -9,7 +9,7 @@ import { ConfigSchema } from './config'; import { Plugin } from './plugin'; export { EncryptedSavedObjectTypeRegistration, EncryptionError } from './crypto'; -export { PluginSetupContract, PluginStartContract } from './plugin'; +export { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } from './plugin'; export const config = { schema: ConfigSchema }; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts index 87c36381a841aa..13d7127db78353 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts @@ -4,20 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginSetupContract, PluginStartContract } from './plugin'; +import { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } from './plugin'; function createEncryptedSavedObjectsSetupMock() { return { registerType: jest.fn(), __legacyCompat: { registerLegacyAPI: jest.fn() }, - } as jest.Mocked; + usingEphemeralEncryptionKey: true, + } as jest.Mocked; } function createEncryptedSavedObjectsStartMock() { return { isEncryptionError: jest.fn(), getDecryptedAsInternalUser: jest.fn(), - } as jest.Mocked; + } as jest.Mocked; } export const encryptedSavedObjectsMock = { diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts index 534ed13ba0acbd..5228734e4a7732 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts @@ -18,6 +18,7 @@ describe('EncryptedSavedObjects Plugin', () => { "registerLegacyAPI": [Function], }, "registerType": [Function], + "usingEphemeralEncryptionKey": true, } `); }); diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index ecd917ff90d003..a0218c51c2723c 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -20,12 +20,13 @@ import { import { EncryptedSavedObjectsAuditLogger } from './audit'; import { SavedObjectsSetup, setupSavedObjects } from './saved_objects'; -export interface PluginSetupContract { +export interface EncryptedSavedObjectsPluginSetup { registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => void; __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => void }; + usingEphemeralEncryptionKey: boolean; } -export interface PluginStartContract extends SavedObjectsSetup { +export interface EncryptedSavedObjectsPluginStart extends SavedObjectsSetup { isEncryptionError: (error: Error) => boolean; } @@ -58,8 +59,8 @@ export class Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup(core: CoreSetup): Promise { - const config = await createConfig$(this.initializerContext) + public async setup(core: CoreSetup): Promise { + const { config, usingEphemeralEncryptionKey } = await createConfig$(this.initializerContext) .pipe(first()) .toPromise(); @@ -81,6 +82,7 @@ export class Plugin { registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => service.registerType(typeRegistration), __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => (this.legacyAPI = legacyAPI) }, + usingEphemeralEncryptionKey, }; } diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap index 07034aca18ec2d..1fd565c7d07f3e 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap @@ -24,14 +24,14 @@ exports[` with user profile disabling "manageSpaces"

+ "kibanaAdmin": , diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx index 21cadfafe1790b..b2b92356e51267 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx @@ -95,13 +95,13 @@ class SpaceAwarePrivilegeSectionUI extends Component { ), diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 17e49b8cf40d30..c0e86b289fe545 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -12,7 +12,7 @@ import { RecursiveReadonly, } from '../../../../src/core/server'; import { ConfigSchema } from './config'; -import { Plugin, PluginSetupContract, PluginSetupDependencies } from './plugin'; +import { Plugin, SecurityPluginSetup, PluginSetupDependencies } from './plugin'; // These exports are part of public Security plugin contract, any change in signature of exported // functions or removal of exports should be considered as a breaking change. @@ -24,7 +24,7 @@ export { InvalidateAPIKeyParams, InvalidateAPIKeyResult, } from './authentication'; -export { PluginSetupContract }; +export { SecurityPluginSetup }; export { AuthenticatedUser } from '../common/model'; export const config: PluginConfigDescriptor> = { @@ -35,7 +35,7 @@ export const config: PluginConfigDescriptor> = { ], }; export const plugin: PluginInitializer< - RecursiveReadonly, + RecursiveReadonly, void, PluginSetupDependencies > = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext); diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index d5c08d5ab1ab94..ababf12c2be606 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginSetupContract } from './plugin'; +import { SecurityPluginSetup } from './plugin'; import { authenticationMock } from './authentication/index.mock'; import { authorizationMock } from './authorization/index.mock'; @@ -19,7 +19,7 @@ function createSetupMock() { mode: mockAuthz.mode, }, registerSpacesService: jest.fn(), - __legacyCompat: {} as PluginSetupContract['__legacyCompat'], + __legacyCompat: {} as SecurityPluginSetup['__legacyCompat'], }; } diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index ce682d8b30eb76..57644182347398 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -49,7 +49,7 @@ export interface LegacyAPI { /** * Describes public Security plugin contract returned at the `setup` stage. */ -export interface PluginSetupContract { +export interface SecurityPluginSetup { authc: Authentication; authz: Pick; @@ -166,7 +166,7 @@ export class Plugin { csp: core.http.csp, }); - return deepFreeze({ + return deepFreeze({ authc, authz: { diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index 776275715921be..92be88b91c6523 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -108,7 +108,10 @@ describe('onPostAuthInterceptor', () => { availableSpaces: any[], testOptions = { simulateGetSpacesFailure: false, simulateGetSingleSpaceFailure: false } ) { - const { http } = await root.setup(); + const { http, elasticsearch } = await root.setup(); + + // Mock esNodesCompatibility$ to prevent `root.start()` from blocking on ES version check + elasticsearch.esNodesCompatibility$ = elasticsearchServiceMock.createInternalSetup().esNodesCompatibility$; const loggingMock = loggingServiceMock .create() diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts index d6ff4a20052e41..5e6cf67ee8c907 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts @@ -17,6 +17,7 @@ import { import * as kbnTestServer from '../../../../../../src/test_utils/kbn_server'; import { LegacyAPI } from '../../plugin'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; describe('onRequestInterceptor', () => { let root: ReturnType; @@ -104,7 +105,9 @@ describe('onRequestInterceptor', () => { routes: 'legacy' | 'new-platform'; } async function setup(opts: SetupOpts = { basePath: '/', routes: 'legacy' }) { - const { http } = await root.setup(); + const { http, elasticsearch } = await root.setup(); + // Mock esNodesCompatibility$ to prevent `root.start()` from blocking on ES version check + elasticsearch.esNodesCompatibility$ = elasticsearchServiceMock.createInternalSetup().esNodesCompatibility$; initSpacesOnRequestInterceptor({ getLegacyAPI: () => diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts index 24a994e836e87d..74e75fb8f12c79 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginSetupContract as SecuritySetupContract } from '../../../../security/server'; +import { SecurityPluginSetup } from '../../../../security/server'; import { SpacesClient } from './spaces_client'; import { ConfigType, ConfigSchema } from '../../config'; import { GetSpacePurpose } from '../../../common/model/types'; @@ -224,17 +224,17 @@ describe('#getAll', () => { [ { purpose: undefined, - expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) => + expectedPrivilege: (mockAuthorization: SecurityPluginSetup['authz']) => mockAuthorization.actions.login, }, { purpose: 'any', - expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) => + expectedPrivilege: (mockAuthorization: SecurityPluginSetup['authz']) => mockAuthorization.actions.login, }, { purpose: 'copySavedObjectsIntoSpace', - expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) => + expectedPrivilege: (mockAuthorization: SecurityPluginSetup['authz']) => mockAuthorization.actions.ui.get('savedObjectsManagement', 'copyIntoSpace'), }, ].forEach(scenario => { diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts index f964ae7d7ac32d..22c34c03368e36 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { omit } from 'lodash'; import { KibanaRequest } from 'src/core/server'; -import { PluginSetupContract as SecurityPluginSetupContract } from '../../../../security/server'; +import { SecurityPluginSetup } from '../../../../security/server'; import { isReservedSpace } from '../../../common/is_reserved_space'; import { Space } from '../../../common/model/space'; import { SpacesAuditLogger } from '../audit_logger'; @@ -17,7 +17,7 @@ const SUPPORTED_GET_SPACE_PURPOSES: GetSpacePurpose[] = ['any', 'copySavedObject const PURPOSE_PRIVILEGE_MAP: Record< GetSpacePurpose, - (authorization: SecurityPluginSetupContract['authz']) => string + (authorization: SecurityPluginSetup['authz']) => string > = { any: authorization => authorization.actions.login, copySavedObjectsIntoSpace: authorization => @@ -28,7 +28,7 @@ export class SpacesClient { constructor( private readonly auditLogger: SpacesAuditLogger, private readonly debugLogger: (message: string) => void, - private readonly authorization: SecurityPluginSetupContract['authz'] | null, + private readonly authorization: SecurityPluginSetup['authz'] | null, private readonly callWithRequestSavedObjectRepository: any, private readonly config: ConfigType, private readonly internalSavedObjectRepository: any, diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index b8ef81c05f7aa4..52ff7eaee3d68c 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -14,7 +14,7 @@ import { PluginInitializerContext, } from '../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { XPackMainPlugin } from '../../../legacy/plugins/xpack_main/server/xpack_main'; import { createDefaultSpace } from './lib/create_default_space'; diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts index f8ed58fa575518..95bda96d894615 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts @@ -8,7 +8,7 @@ import { map, take } from 'rxjs/operators'; import { Observable, Subscription } from 'rxjs'; import { Legacy } from 'kibana'; import { Logger, KibanaRequest, CoreSetup } from '../../../../../src/core/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../../security/server'; +import { SecurityPluginSetup } from '../../../security/server'; import { LegacyAPI } from '../plugin'; import { SpacesClient } from '../lib/spaces_client'; import { ConfigType } from '../config'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 817aa03db31bd0..47e11817ffa5d4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8908,7 +8908,7 @@ "xpack.ml.validateJob.validateJobButtonLabel": "ジョブを検証", "xpack.monitoring.accessDenied.backToKibanaButtonLabel": "Kibana に戻る", "xpack.monitoring.accessDenied.clusterNotConfiguredDescription": "専用の監視クラスターへのアクセスを試みている場合、監視クラスターで構成されていないユーザーとしてログインしていることが原因である可能性があります。", - "xpack.monitoring.accessDenied.notAuthorizedDescription": "監視アクセスが許可されていません。監視を利用するには、「{kibanaUser}」と「{monitoringUser}」の両方のロールからの権限が必要です。", + "xpack.monitoring.accessDenied.notAuthorizedDescription": "監視アクセスが許可されていません。監視を利用するには、「{kibanaAdmin}」と「{monitoringUser}」の両方のロールからの権限が必要です。", "xpack.monitoring.accessDeniedTitle": "アクセス拒否", "xpack.monitoring.ajaxErrorHandler.httpErrorMessage": "HTTP {errStatus}", "xpack.monitoring.ajaxErrorHandler.requestErrorNotificationTitle": "監視リクエストエラー", @@ -10644,11 +10644,11 @@ "xpack.security.management.editRole.spaceAwarePrivilegeDisplay.spaceBasePrivilegeSource": "スペースベース権限", "xpack.security.management.editRole.spaceAwarePrivilegeDisplay.spaceFeaturePrivilegeSource": "スペース機能権限", "xpack.security.management.editRole.spaceAwarePrivilegeDisplay.unknownPrivilegeSource": "**不明**", - "xpack.security.management.editRole.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "{kibanaUser} ロールによりアカウントにすべての権限が提供されていることを確認し、再試行してください。", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "{kibanaAdmin} ロールによりアカウントにすべての権限が提供されていることを確認し、再試行してください。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.globalSpacesName": "* グローバル (すべてのスペース)", "xpack.security.management.editRole.spaceAwarePrivilegeForm.howToViewAllAvailableSpacesDescription": "利用可能なすべてのスペースを表示する権限がありません。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription": "権限が不十分です", - "xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaUserTitle": "kibana_user", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaAdminTitle": "kibana_admin", "xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDetails": "選択されたスペースの全機能への完全アクセスを許可します。", "xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDisplay": "すべて", "xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDropdownDisplay": "すべて", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d8012bbb526c99..86d9a69dc0900b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8907,7 +8907,7 @@ "xpack.ml.validateJob.validateJobButtonLabel": "验证作业", "xpack.monitoring.accessDenied.backToKibanaButtonLabel": "返回 Kibana", "xpack.monitoring.accessDenied.clusterNotConfiguredDescription": "如果您尝试访问专用监测集群,则这可能是因为该监测集群上未配置您登录时所用的用户帐户。", - "xpack.monitoring.accessDenied.notAuthorizedDescription": "您无权访问 Monitoring。要使用 Monitoring,您同时需要 `{kibanaUser}` 和 `{monitoringUser}` 角色授予的权限。", + "xpack.monitoring.accessDenied.notAuthorizedDescription": "您无权访问 Monitoring。要使用 Monitoring,您同时需要 `{kibanaAdmin}` 和 `{monitoringUser}` 角色授予的权限。", "xpack.monitoring.accessDeniedTitle": "访问被拒绝", "xpack.monitoring.ajaxErrorHandler.httpErrorMessage": "HTTP {errStatus}", "xpack.monitoring.ajaxErrorHandler.requestErrorNotificationTitle": "Monitoring 请求错误", @@ -10643,11 +10643,11 @@ "xpack.security.management.editRole.spaceAwarePrivilegeDisplay.spaceBasePrivilegeSource": "工作区基本权限", "xpack.security.management.editRole.spaceAwarePrivilegeDisplay.spaceFeaturePrivilegeSource": "全局功能权限", "xpack.security.management.editRole.spaceAwarePrivilegeDisplay.unknownPrivilegeSource": "**未知**", - "xpack.security.management.editRole.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "请确保您的帐户具有 {kibanaUser} 角色授予的所有权限,然后重试。", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "请确保您的帐户具有 {kibanaAdmin} 角色授予的所有权限,然后重试。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.globalSpacesName": "* 全局(所有工作区)", "xpack.security.management.editRole.spaceAwarePrivilegeForm.howToViewAllAvailableSpacesDescription": "您无权查看所有可用工作区。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription": "权限不足", - "xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaUserTitle": "kibana_user", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaAdminTitle": "kibana_admin", "xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDetails": "授予对选定工作区所有功能的完全访问权限。", "xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDisplay": "全部", "xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDropdownDisplay": "全部", diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts index d7bee93f5c94b9..7194c642e7015e 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts @@ -8,7 +8,7 @@ import Joi from 'joi'; import Hapi from 'hapi'; import { Legacy } from 'kibana'; import KbnServer from '../../../../../../../src/legacy/server/kbn_server'; -import { PluginStartContract } from '../../../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../../../plugins/encrypted_saved_objects/server'; interface CheckAADRequest extends Hapi.Request { payload: { @@ -25,7 +25,8 @@ export default function(kibana: any) { name: 'aad-fixtures', init(server: Legacy.Server) { const newPlatform = ((server as unknown) as KbnServer).newPlatform; - const esoPlugin = newPlatform.start.plugins.encryptedSavedObjects as PluginStartContract; + const esoPlugin = newPlatform.start.plugins + .encryptedSavedObjects as EncryptedSavedObjectsPluginStart; server.route({ method: 'POST', diff --git a/x-pack/test/api_integration/apis/console/feature_controls.ts b/x-pack/test/api_integration/apis/console/feature_controls.ts index 3f9a0867794378..ce926f0d032c8b 100644 --- a/x-pack/test/api_integration/apis/console/feature_controls.ts +++ b/x-pack/test/api_integration/apis/console/feature_controls.ts @@ -43,6 +43,29 @@ export default function securityTests({ getService }: FtrProviderContext) { } }); + it('can be accessed by kibana_admin role', async () => { + const username = 'kibana_admin'; + const roleName = 'kibana_admin'; + try { + const password = `${username}-password`; + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana admin', + }); + + await supertest + .post(`/api/console/proxy?method=GET&path=${encodeURIComponent('/_cat')}`) + .auth(username, password) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + } finally { + await security.user.delete(username); + } + }); + it('can be accessed by global all role', async () => { const username = 'global_all'; const roleName = 'global_all'; diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/security.js b/x-pack/test/api_integration/apis/monitoring/setup/collection/security.js index 7e6a2dbe319659..4da08d7cb97265 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/security.js +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/security.js @@ -44,7 +44,7 @@ export default function({ getService }) { await security.user.create(username, { password: password, full_name: 'Limited User', - roles: ['kibana_user', 'monitoring_user'], + roles: ['kibana_admin', 'monitoring_user'], }); const { body } = await supertestWithoutAuth diff --git a/x-pack/test/api_integration/apis/short_urls/feature_controls.ts b/x-pack/test/api_integration/apis/short_urls/feature_controls.ts index db5e11ef367ad7..35a6f2c2b382a6 100644 --- a/x-pack/test/api_integration/apis/short_urls/feature_controls.ts +++ b/x-pack/test/api_integration/apis/short_urls/feature_controls.ts @@ -12,8 +12,8 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const security = getService('security'); describe('feature controls', () => { - const kibanaUsername = 'kibana_user'; - const kibanaUserRoleName = 'kibana_user'; + const kibanaUsername = 'kibana_admin'; + const kibanaUserRoleName = 'kibana_admin'; const kibanaUserPassword = `${kibanaUsername}-password`; diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js index 1189fe909ca320..b521c47585d58b 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js @@ -92,7 +92,7 @@ export default function({ getService, getPageObjects }) { await testSubjects.setValue('userFormFullNameInput', 'mixeduser'); await testSubjects.setValue('userFormEmailInput', 'example@example.com'); await PageObjects.security.assignRoleToUser('kibana_dashboard_only_user'); - await PageObjects.security.assignRoleToUser('kibana_user'); + await PageObjects.security.assignRoleToUser('kibana_admin'); await PageObjects.security.assignRoleToUser('logstash-data'); await PageObjects.security.clickSaveEditUser(); diff --git a/x-pack/test/functional/apps/security/doc_level_security_roles.js b/x-pack/test/functional/apps/security/doc_level_security_roles.js index 5761369f9e4685..480fa6599e0365 100644 --- a/x-pack/test/functional/apps/security/doc_level_security_roles.js +++ b/x-pack/test/functional/apps/security/doc_level_security_roles.js @@ -58,11 +58,11 @@ export default function({ getService, getPageObjects }) { fullname: 'dls EAST', email: 'dlstest@elastic.com', save: true, - roles: ['kibana_user', 'myroleEast'], + roles: ['kibana_admin', 'myroleEast'], }); const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); - expect(users.userEast.roles).to.eql(['kibana_user', 'myroleEast']); + expect(users.userEast.roles).to.eql(['kibana_admin', 'myroleEast']); expect(users.userEast.reserved).to.be(false); }); diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.js index 16e9d755bf261b..93a6f9cd9e0c3d 100644 --- a/x-pack/test/functional/apps/security/field_level_security.js +++ b/x-pack/test/functional/apps/security/field_level_security.js @@ -79,11 +79,11 @@ export default function({ getService, getPageObjects }) { fullname: 'customer one', email: 'flstest@elastic.com', save: true, - roles: ['kibana_user', 'a_viewssnrole'], + roles: ['kibana_admin', 'a_viewssnrole'], }); const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); - expect(users.customer1.roles).to.eql(['kibana_user', 'a_viewssnrole']); + expect(users.customer1.roles).to.eql(['kibana_admin', 'a_viewssnrole']); }); it('should add new user customer2 ', async function() { @@ -95,11 +95,11 @@ export default function({ getService, getPageObjects }) { fullname: 'customer two', email: 'flstest@elastic.com', save: true, - roles: ['kibana_user', 'a_view_no_ssn_role'], + roles: ['kibana_admin', 'a_view_no_ssn_role'], }); const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); - expect(users.customer2.roles).to.eql(['kibana_user', 'a_view_no_ssn_role']); + expect(users.customer2.roles).to.eql(['kibana_admin', 'a_view_no_ssn_role']); }); it('user customer1 should see ssn', async function() { diff --git a/x-pack/test/functional/apps/security/secure_roles_perm.js b/x-pack/test/functional/apps/security/secure_roles_perm.js index ece289b4a666ee..4e155872d1041a 100644 --- a/x-pack/test/functional/apps/security/secure_roles_perm.js +++ b/x-pack/test/functional/apps/security/secure_roles_perm.js @@ -61,13 +61,13 @@ export default function({ getService, getPageObjects }) { fullname: 'RashmiFirst RashmiLast', email: 'rashmi@myEmail.com', save: true, - roles: ['logstash_reader', 'kibana_user'], + roles: ['logstash_reader', 'kibana_admin'], }); log.debug('After Add user: , userObj.userName'); const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); log.debug('roles: ', users.Rashmi.roles); - expect(users.Rashmi.roles).to.eql(['logstash_reader', 'kibana_user']); + expect(users.Rashmi.roles).to.eql(['logstash_reader', 'kibana_admin']); expect(users.Rashmi.fullname).to.eql('RashmiFirst RashmiLast'); expect(users.Rashmi.reserved).to.be(false); await PageObjects.security.forceLogout(); diff --git a/x-pack/test/functional/apps/security/user_email.js b/x-pack/test/functional/apps/security/user_email.js index 492eddcfb9f746..a007c40a06b626 100644 --- a/x-pack/test/functional/apps/security/user_email.js +++ b/x-pack/test/functional/apps/security/user_email.js @@ -27,11 +27,11 @@ export default function({ getService, getPageObjects }) { fullname: 'newuserFirst newuserLast', email: 'newuser@myEmail.com', save: true, - roles: ['kibana_user', 'superuser'], + roles: ['kibana_admin', 'superuser'], }); const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); - expect(users.newuser.roles).to.eql(['kibana_user', 'superuser']); + expect(users.newuser.roles).to.eql(['kibana_admin', 'superuser']); expect(users.newuser.fullname).to.eql('newuserFirst newuserLast'); expect(users.newuser.email).to.eql('newuser@myEmail.com'); expect(users.newuser.reserved).to.be(false); diff --git a/x-pack/test/functional/apps/security/users.js b/x-pack/test/functional/apps/security/users.js index 3eed74881e9571..9dc42553f0fdfe 100644 --- a/x-pack/test/functional/apps/security/users.js +++ b/x-pack/test/functional/apps/security/users.js @@ -42,11 +42,11 @@ export default function({ getService, getPageObjects }) { fullname: 'LeeFirst LeeLast', email: 'lee@myEmail.com', save: true, - roles: ['kibana_user'], + roles: ['kibana_admin'], }); const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); - expect(users.Lee.roles).to.eql(['kibana_user']); + expect(users.Lee.roles).to.eql(['kibana_admin']); expect(users.Lee.fullname).to.eql('LeeFirst LeeLast'); expect(users.Lee.email).to.eql('lee@myEmail.com'); expect(users.Lee.reserved).to.be(false); @@ -85,7 +85,7 @@ export default function({ getService, getPageObjects }) { expect(roles.apm_user.reserved).to.be(true); expect(roles.beats_admin.reserved).to.be(true); expect(roles.beats_system.reserved).to.be(true); - expect(roles.kibana_user.reserved).to.be(true); + expect(roles.kibana_admin.reserved).to.be(true); expect(roles.kibana_system.reserved).to.be(true); expect(roles.logstash_system.reserved).to.be(true); expect(roles.monitoring_user.reserved).to.be(true); diff --git a/x-pack/test/functional/page_objects/monitoring_page.js b/x-pack/test/functional/page_objects/monitoring_page.js index 6920010d67187f..8de5b5e69d34d7 100644 --- a/x-pack/test/functional/page_objects/monitoring_page.js +++ b/x-pack/test/functional/page_objects/monitoring_page.js @@ -14,7 +14,7 @@ export function MonitoringPageProvider({ getPageObjects, getService }) { // always create this because our tear down tries to delete it await security.user.create('basic_monitoring_user', { password: 'monitoring_user_password', - roles: ['monitoring_user', 'kibana_user'], + roles: ['monitoring_user', 'kibana_admin'], full_name: 'basic monitoring', }); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts new file mode 100644 index 00000000000000..e8ed54571c77cd --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -0,0 +1,146 @@ +/* + * 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 expect from '@kbn/expect'; +import uuid from 'uuid'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const testSubjects = getService('testSubjects'); + const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header', 'alertDetailsUI']); + const browser = getService('browser'); + const alerting = getService('alerting'); + + describe('Alert Details', function() { + const testRunUuid = uuid.v4(); + + before(async () => { + await pageObjects.common.navigateToApp('triggersActions'); + + const actions = await Promise.all([ + alerting.actions.createAction({ + name: `server-log-${testRunUuid}-${0}`, + actionTypeId: '.server-log', + config: {}, + secrets: {}, + }), + alerting.actions.createAction({ + name: `server-log-${testRunUuid}-${1}`, + actionTypeId: '.server-log', + config: {}, + secrets: {}, + }), + ]); + + const alert = await alerting.alerts.createAlwaysFiringWithActions( + `test-alert-${testRunUuid}`, + actions.map(action => ({ + id: action.id, + group: 'default', + params: { + message: 'from alert 1s', + level: 'warn', + }, + })) + ); + + // refresh to see alert + await browser.refresh(); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + // Verify content + await testSubjects.existOrFail('alertsList'); + + // click on first alert + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + }); + + it('renders the alert details', async () => { + const headingText = await pageObjects.alertDetailsUI.getHeadingText(); + expect(headingText).to.be(`test-alert-${testRunUuid}`); + + const alertType = await pageObjects.alertDetailsUI.getAlertType(); + expect(alertType).to.be(`Always Firing`); + + const { actionType, actionCount } = await pageObjects.alertDetailsUI.getActionsLabels(); + expect(actionType).to.be(`Server log`); + expect(actionCount).to.be(`+1`); + }); + + it('should disable the alert', async () => { + const enableSwitch = await testSubjects.find('enableSwitch'); + + const isChecked = await enableSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('true'); + + await enableSwitch.click(); + + const enabledSwitchAfterDisabling = await testSubjects.find('enableSwitch'); + const isCheckedAfterDisabling = await enabledSwitchAfterDisabling.getAttribute( + 'aria-checked' + ); + expect(isCheckedAfterDisabling).to.eql('false'); + }); + + it('shouldnt allow you to mute a disabled alert', async () => { + const disabledEnableSwitch = await testSubjects.find('enableSwitch'); + expect(await disabledEnableSwitch.getAttribute('aria-checked')).to.eql('false'); + + const muteSwitch = await testSubjects.find('muteSwitch'); + expect(await muteSwitch.getAttribute('aria-checked')).to.eql('false'); + + await muteSwitch.click(); + + const muteSwitchAfterTryingToMute = await testSubjects.find('muteSwitch'); + const isDisabledMuteAfterDisabling = await muteSwitchAfterTryingToMute.getAttribute( + 'aria-checked' + ); + expect(isDisabledMuteAfterDisabling).to.eql('false'); + }); + + it('should reenable a disabled the alert', async () => { + const enableSwitch = await testSubjects.find('enableSwitch'); + + const isChecked = await enableSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('false'); + + await enableSwitch.click(); + + const enabledSwitchAfterReenabling = await testSubjects.find('enableSwitch'); + const isCheckedAfterDisabling = await enabledSwitchAfterReenabling.getAttribute( + 'aria-checked' + ); + expect(isCheckedAfterDisabling).to.eql('true'); + }); + + it('should mute the alert', async () => { + const muteSwitch = await testSubjects.find('muteSwitch'); + + const isChecked = await muteSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('false'); + + await muteSwitch.click(); + + const muteSwitchAfterDisabling = await testSubjects.find('muteSwitch'); + const isCheckedAfterDisabling = await muteSwitchAfterDisabling.getAttribute('aria-checked'); + expect(isCheckedAfterDisabling).to.eql('true'); + }); + + it('should unmute the alert', async () => { + const muteSwitch = await testSubjects.find('muteSwitch'); + + const isChecked = await muteSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('true'); + + await muteSwitch.click(); + + const muteSwitchAfterUnmuting = await testSubjects.find('muteSwitch'); + const isCheckedAfterDisabling = await muteSwitchAfterUnmuting.getAttribute('aria-checked'); + expect(isCheckedAfterDisabling).to.eql('false'); + }); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts index 13f50a505b0b6f..307f39382a2363 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts @@ -12,6 +12,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); const log = getService('log'); const browser = getService('browser'); + const alerting = getService('alerting'); describe('Home page', function() { before(async () => { @@ -55,6 +56,43 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify content await testSubjects.existOrFail('alertsList'); }); + + it('navigates to an alert details page', async () => { + const action = await alerting.actions.createAction({ + name: `server-log-${Date.now()}`, + actionTypeId: '.server-log', + config: {}, + secrets: {}, + }); + + const alert = await alerting.alerts.createAlwaysFiringWithAction( + `test-alert-${Date.now()}`, + { + id: action.id, + group: 'default', + params: { + message: 'from alert 1s', + level: 'warn', + }, + } + ); + + // refresh to see alert + await browser.refresh(); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + // Verify content + await testSubjects.existOrFail('alertsList'); + + // click on first alert + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + + // Verify url + expect(await browser.getCurrentUrl()).to.contain(`/alert/${alert.id}`); + + await alerting.alerts.deleteAlert(alert.id); + }); }); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts index c76f477c8cfbef..a771fbf85e0b6e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts @@ -12,5 +12,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./home_page')); loadTestFile(require.resolve('./connectors')); loadTestFile(require.resolve('./alerts')); + loadTestFile(require.resolve('./details')); }); }; diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts index df651c67c2c281..43162e92563703 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts @@ -12,13 +12,42 @@ export default function(kibana: any) { require: ['alerting'], name: 'alerts', init(server: any) { - const noopAlertType: AlertType = { - id: 'test.noop', - name: 'Test: Noop', - actionGroups: ['default'], - async executor() {}, - }; - server.plugins.alerting.setup.registerType(noopAlertType); + createNoopAlertType(server.plugins.alerting.setup); + createAlwaysFiringAlertType(server.plugins.alerting.setup); }, }); } + +function createNoopAlertType(setupContract: any) { + const noopAlertType: AlertType = { + id: 'test.noop', + name: 'Test: Noop', + actionGroups: ['default'], + async executor() {}, + }; + setupContract.registerType(noopAlertType); +} + +function createAlwaysFiringAlertType(setupContract: any) { + // Alert types + const alwaysFiringAlertType: any = { + id: 'test.always-firing', + name: 'Always Firing', + actionGroups: ['default', 'other'], + async executor(alertExecutorOptions: any) { + const { services, state } = alertExecutorOptions; + + services + .alertInstanceFactory('1') + .replaceState({ instanceStateValue: true }) + .scheduleActions('default', { + instanceContextValue: true, + }); + return { + globalStateValue: true, + groupInSeriesIndex: (state.groupInSeriesIndex || 0) + 1, + }; + }, + }; + setupContract.registerType(alwaysFiringAlertType); +} diff --git a/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts new file mode 100644 index 00000000000000..6d2038a6ba04c3 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts @@ -0,0 +1,26 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; + +export function AlertDetailsPageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async getHeadingText() { + return await testSubjects.getVisibleText('alertDetailsTitle'); + }, + async getAlertType() { + return await testSubjects.getVisibleText('alertTypeLabel'); + }, + async getActionsLabels() { + return { + actionType: await testSubjects.getVisibleText('actionTypeLabel'), + actionCount: await testSubjects.getVisibleText('actionCountLabel'), + }; + }, + }; +} diff --git a/x-pack/test/functional_with_es_ssl/page_objects/index.ts b/x-pack/test/functional_with_es_ssl/page_objects/index.ts index a068ba7dfe81d3..cfc44221a9c17f 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/index.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/index.ts @@ -6,8 +6,10 @@ import { pageObjects as xpackFunctionalPageObjects } from '../../functional/page_objects'; import { TriggersActionsPageProvider } from './triggers_actions_ui_page'; +import { AlertDetailsPageProvider } from './alert_details'; export const pageObjects = { ...xpackFunctionalPageObjects, triggersActionsUI: TriggersActionsPageProvider, + alertDetailsUI: AlertDetailsPageProvider, }; diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts index a04ecc969a7e1f..ae66ac0ddddfb7 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts @@ -92,6 +92,10 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) }; }); }, + async clickOnAlertInAlertsList(name: string) { + await this.searchAlerts(name); + await find.clickDisplayedByCssSelector(`[data-test-subj="alertsList"] [title="${name}"]`); + }, async changeTabs(tab: 'alertsTab' | 'connectorsTab') { return await testSubjects.click(tab); }, diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/actions.ts b/x-pack/test/functional_with_es_ssl/services/alerting/actions.ts new file mode 100644 index 00000000000000..9454a32757068b --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/services/alerting/actions.ts @@ -0,0 +1,48 @@ +/* + * 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 axios, { AxiosInstance } from 'axios'; +import util from 'util'; +import { ToolingLog } from '@kbn/dev-utils'; + +export class Actions { + private log: ToolingLog; + private axios: AxiosInstance; + + constructor(url: string, log: ToolingLog) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/alerting/actions' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we do our own validation below and throw better error messages + }); + } + + public async createAction(actionParams: { + name: string; + actionTypeId: string; + config: Record; + secrets: Record; + }) { + this.log.debug(`creating action ${actionParams.name}`); + + const { data: action, status: actionStatus, actionStatusText } = await this.axios.post( + `/api/action`, + actionParams + ); + if (actionStatus !== 200) { + throw new Error( + `Expected status code of 200, received ${actionStatus} ${actionStatusText}: ${util.inspect( + action + )}` + ); + } + + this.log.debug(`created action ${action.id}`); + return action; + } +} diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts new file mode 100644 index 00000000000000..1a31d4796d5bc4 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts @@ -0,0 +1,79 @@ +/* + * 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 axios, { AxiosInstance } from 'axios'; +import util from 'util'; +import { ToolingLog } from '@kbn/dev-utils'; + +export class Alerts { + private log: ToolingLog; + private axios: AxiosInstance; + + constructor(url: string, log: ToolingLog) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/alerting/alerts' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we do our own validation below and throw better error messages + }); + } + + public async createAlwaysFiringWithActions( + name: string, + actions: Array<{ + id: string; + group: string; + params: Record; + }> + ) { + this.log.debug(`creating alert ${name}`); + + const { data: alert, status, statusText } = await this.axios.post(`/api/alert`, { + enabled: true, + name, + tags: ['foo'], + alertTypeId: 'test.always-firing', + consumer: 'bar', + schedule: { interval: '1m' }, + throttle: '1m', + actions, + params: {}, + }); + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(alert)}` + ); + } + + this.log.debug(`created alert ${alert.id}`); + + return alert; + } + + public async createAlwaysFiringWithAction( + name: string, + action: { + id: string; + group: string; + params: Record; + } + ) { + return this.createAlwaysFiringWithActions(name, [action]); + } + + public async deleteAlert(id: string) { + this.log.debug(`deleting alert ${id}`); + + const { data: alert, status, statusText } = await this.axios.delete(`/api/alert/${id}`); + if (status !== 204) { + throw new Error( + `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(alert)}` + ); + } + this.log.debug(`deleted alert ${alert.id}`); + } +} diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/index.ts b/x-pack/test/functional_with_es_ssl/services/alerting/index.ts new file mode 100644 index 00000000000000..e0aa827316c01a --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/services/alerting/index.ts @@ -0,0 +1,22 @@ +/* + * 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 { format as formatUrl } from 'url'; + +import { Alerts } from './alerts'; +import { Actions } from './actions'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function AlertsServiceProvider({ getService }: FtrProviderContext) { + const log = getService('log'); + const config = getService('config'); + const url = formatUrl(config.get('servers.kibana')); + + return new (class AlertingService { + actions = new Actions(url, log); + alerts = new Alerts(url, log); + })(); +} diff --git a/x-pack/test/functional_with_es_ssl/services/index.ts b/x-pack/test/functional_with_es_ssl/services/index.ts index 6e96921c25a316..f04c2c980055d3 100644 --- a/x-pack/test/functional_with_es_ssl/services/index.ts +++ b/x-pack/test/functional_with_es_ssl/services/index.ts @@ -5,7 +5,9 @@ */ import { services as xpackFunctionalServices } from '../../functional/services'; +import { AlertsServiceProvider } from './alerting'; export const services = { ...xpackFunctionalServices, + alerting: AlertsServiceProvider, }; diff --git a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts index 0346da334d2f2d..203f90c55aa82b 100644 --- a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts +++ b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts @@ -38,7 +38,7 @@ export default function({ getService }: FtrProviderContext) { await getService('esSupertest') .post('/_security/role_mapping/krb5') .send({ - roles: ['kibana_user'], + roles: ['kibana_admin'], enabled: true, rules: { field: { 'realm.name': 'kerb1' } }, }) @@ -119,7 +119,7 @@ export default function({ getService }: FtrProviderContext) { .set('Cookie', sessionCookie.cookieString()) .expect(200, { username: 'tester@TEST.ELASTIC.CO', - roles: ['kibana_user'], + roles: ['kibana_admin'], full_name: null, email: null, metadata: { diff --git a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts index 4eee900e68bec9..186ed824b3b6c7 100644 --- a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts +++ b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts @@ -48,7 +48,7 @@ export default function({ getService }: FtrProviderContext) { .post('/_security/role_mapping/first_client_pki') .ca(CA_CERT) .send({ - roles: ['kibana_user'], + roles: ['kibana_admin'], enabled: true, rules: { field: { dn: 'CN=first_client' } }, }) @@ -107,7 +107,7 @@ export default function({ getService }: FtrProviderContext) { expect(response.body).to.eql({ username: 'first_client', - roles: ['kibana_user'], + roles: ['kibana_admin'], full_name: null, email: null, enabled: true, diff --git a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts index a194e477da7559..e61b8f24a1f69b 100644 --- a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts +++ b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts @@ -8,8 +8,8 @@ import { Request } from 'hapi'; import { boomify, badRequest } from 'boom'; import { Legacy } from 'kibana'; import { - PluginSetupContract, - PluginStartContract, + EncryptedSavedObjectsPluginSetup, + EncryptedSavedObjectsPluginStart, } from '../../../../plugins/encrypted_saved_objects/server'; const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret'; @@ -26,7 +26,7 @@ export default function esoPlugin(kibana: any) { path: '/api/saved_objects/get-decrypted-as-internal-user/{id}', async handler(request: Request) { const encryptedSavedObjectsStart = server.newPlatform.start.plugins - .encryptedSavedObjects as PluginStartContract; + .encryptedSavedObjects as EncryptedSavedObjectsPluginStart; const namespace = server.plugins.spaces && server.plugins.spaces.getSpaceId(request); try { return await encryptedSavedObjectsStart.getDecryptedAsInternalUser( @@ -44,7 +44,8 @@ export default function esoPlugin(kibana: any) { }, }); - (server.newPlatform.setup.plugins.encryptedSavedObjects as PluginSetupContract).registerType({ + (server.newPlatform.setup.plugins + .encryptedSavedObjects as EncryptedSavedObjectsPluginSetup).registerType({ type: SAVED_OBJECT_WITH_SECRET_TYPE, attributesToEncrypt: new Set(['privateProperty']), attributesToExcludeFromAAD: new Set(['publicPropertyExcludedFromAAD']), diff --git a/x-pack/test_utils/kbn_server_config.ts b/x-pack/test_utils/kbn_server_config.ts index 75f5ac736b7c07..3cac6ed5df0147 100644 --- a/x-pack/test_utils/kbn_server_config.ts +++ b/x-pack/test_utils/kbn_server_config.ts @@ -26,9 +26,9 @@ export const TestKbnServerConfig = { }, users: [ { - username: 'kibana_user', + username: 'kibana_admin', password: 'x-pack-test-password', - roles: ['kibana_user'], + roles: ['kibana_admin'], }, ], }; diff --git a/yarn.lock b/yarn.lock index 3563ee3fc2733f..a3acc2ae216c5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19371,7 +19371,7 @@ lodash.clone@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" integrity sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y= -lodash.clonedeep@^4.3.2, lodash.clonedeep@^4.5.0: +lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= @@ -19516,11 +19516,6 @@ lodash.merge@^4.4.0, lodash.merge@^4.6.1: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.mergewith@^4.6.0: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" - integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== - lodash.omit@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60" @@ -20947,7 +20942,12 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.10.0, nan@^2.9.2: +nan@^2.13.2: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + +nan@^2.9.2: version "2.10.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== @@ -21350,10 +21350,10 @@ node-releases@^1.1.25: dependencies: semver "^5.3.0" -node-sass@^4.9.4: - version "4.9.4" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.9.4.tgz#349bd7f1c89422ffe7e1e4b60f2055a69fbc5512" - integrity sha512-MXyurANsUoE4/6KmfMkwGcBzAnJQ5xJBGW7Ei6ea8KnUKuzHr/SguVBIi3uaUAHtZCPUYkvlJ3Ef5T5VAwVpaA== +node-sass@^4.13.1: + version "4.13.1" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.13.1.tgz#9db5689696bb2eec2c32b98bfea4c7a2e992d0a3" + integrity sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw== dependencies: async-foreach "^0.1.3" chalk "^1.1.1" @@ -21362,12 +21362,10 @@ node-sass@^4.9.4: get-stdin "^4.0.1" glob "^7.0.3" in-publish "^2.0.0" - lodash.assign "^4.2.0" - lodash.clonedeep "^4.3.2" - lodash.mergewith "^4.6.0" + lodash "^4.17.15" meow "^3.7.0" mkdirp "^0.5.1" - nan "^2.10.0" + nan "^2.13.2" node-gyp "^3.8.0" npmlog "^4.0.0" request "^2.88.0"