From a98ad7a55e4f73922a18320c8c3f9e831cd79d1e Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 7 Feb 2020 19:22:36 +0000 Subject: [PATCH 01/19] chore(NA): removes use of parallel option in the terser minimizer (#57077) * chore(NA): removes use of parallel option in the terser minimizer * docs(NA): update note --- src/optimize/base_optimizer.js | 2 +- .../dynamic_dll_plugin/dll_config_model.js | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index d9df2a1955df39..539c55c9696538 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -459,7 +459,7 @@ export default class BaseOptimizer { optimization: { minimizer: [ new TerserPlugin({ - parallel: this.getThreadLoaderPoolConfig().workers, + parallel: false, sourceMap: false, cache: false, extractComments: false, diff --git a/src/optimize/dynamic_dll_plugin/dll_config_model.js b/src/optimize/dynamic_dll_plugin/dll_config_model.js index 2e74cb6af86d4f..9ca6071b8f5158 100644 --- a/src/optimize/dynamic_dll_plugin/dll_config_model.js +++ b/src/optimize/dynamic_dll_plugin/dll_config_model.js @@ -214,16 +214,20 @@ function common(config) { return webpackMerge(generateDLL(config)); } -function optimized(config) { +function optimized() { return webpackMerge({ mode: 'production', optimization: { minimizer: [ new TerserPlugin({ - // Apply the same logic used to calculate the - // threadLoaderPool workers number to spawn - // the parallel processes on terser - parallel: config.threadLoaderPoolConfig.workers, + // NOTE: we should not enable that option for now + // Since 2.0.0 terser-webpack-plugin is using jest-worker + // to run tasks in a pool of workers. Currently it looks like + // is requiring too much memory and break on large entry points + // compilations (like this) one. Also the gain we have enabling + // that option was barely noticed. + // https://github.com/webpack-contrib/terser-webpack-plugin/issues/143 + parallel: false, sourceMap: false, cache: false, extractComments: false, @@ -250,5 +254,5 @@ export function configModel(rawConfig = {}) { return webpackMerge(common(config), unoptimized()); } - return webpackMerge(common(config), optimized(config)); + return webpackMerge(common(config), optimized()); } From e6be475de436cc334969dac89301d46c0f931286 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Fri, 7 Feb 2020 22:05:54 +0100 Subject: [PATCH 02/19] =?UTF-8?q?Update=20`markdown-to-jsx`=20(`6.9.3`=20?= =?UTF-8?q?=E2=86=92=20`6.11.0`)=20and=20`url-parse`=20(`1.4.4`=20?= =?UTF-8?q?=E2=86=92=20`1.4.7`)=20dependencies.=20(#57126)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yarn.lock | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/yarn.lock b/yarn.lock index a35cd1d541762a..5dc4db12c5db45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20051,18 +20051,10 @@ markdown-it@^10.0.0: mdurl "^1.0.1" uc.micro "^1.0.5" -markdown-to-jsx@^6.9.1: - version "6.9.3" - resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-6.9.3.tgz#31719e3c54517ba9805db81d53701b89f5d2ed7e" - integrity sha512-iXteiv317VZd1vk/PBH5MWMD4r0XWekoWCHRVVadBcnCtxavhtfV1UaEaQgq9KyckTv31L60ASh5ZVVrOh37Qg== - dependencies: - prop-types "^15.6.2" - unquote "^1.1.0" - -markdown-to-jsx@^6.9.3: - version "6.10.3" - resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-6.10.3.tgz#7f0946684acd321125ff2de7fd258a9b9c7c40b7" - integrity sha512-PSoUyLnW/xoW6RsxZrquSSz5eGEOTwa15H5eqp3enmrp8esmgDJmhzd6zmQ9tgAA9TxJzx1Hmf3incYU/IamoQ== +markdown-to-jsx@^6.9.1, markdown-to-jsx@^6.9.3: + version "6.11.0" + resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-6.11.0.tgz#a2e3f2bc781c3402d8bb0f8e0a12a186474623b0" + integrity sha512-RH7LCJQ4RFmPqVeZEesKaO1biRzB/k4utoofmTCp3Eiw6D7qfvK8fzZq/2bjEJAtVkfPrM5SMt5APGf2rnaKMg== dependencies: prop-types "^15.6.2" unquote "^1.1.0" @@ -23809,10 +23801,10 @@ querystring@0.2.0, querystring@^0.2.0: resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= -querystringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.0.tgz#7ded8dfbf7879dcc60d0a644ac6754b283ad17ef" - integrity sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg== +querystringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" + integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== quick-lru@^1.0.0: version "1.1.0" @@ -30206,11 +30198,11 @@ url-parse-lax@^3.0.0: prepend-http "^2.0.0" url-parse@^1.4.3: - version "1.4.4" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.4.tgz#cac1556e95faa0303691fec5cf9d5a1bc34648f8" - integrity sha512-/92DTTorg4JjktLNLe6GPS2/RvAd/RGr6LuktmWSMLEOa6rjnlrFXNgSbSmkNvCoL2T028A0a1JaJLzRMlFoHg== + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== dependencies: - querystringify "^2.0.0" + querystringify "^2.1.1" requires-port "^1.0.0" url-pattern@^1.0.3: From d72a7159d460f7cc5b92bad7e425c5056b599bf8 Mon Sep 17 00:00:00 2001 From: Charlie Pichette <56399229+charlie-pichette@users.noreply.github.com> Date: Fri, 7 Feb 2020 16:25:19 -0500 Subject: [PATCH 03/19] Add Test to Verify Endpoint App Landing Page (#57129) --- x-pack/test/functional/apps/endpoint/index.ts | 1 + .../functional/apps/endpoint/landing_page.ts | 24 +++++++++++++++++++ .../functional/page_objects/endpoint_page.ts | 17 +++++++++++++ x-pack/test/functional/page_objects/index.ts | 2 ++ 4 files changed, 44 insertions(+) create mode 100644 x-pack/test/functional/apps/endpoint/landing_page.ts create mode 100644 x-pack/test/functional/page_objects/endpoint_page.ts diff --git a/x-pack/test/functional/apps/endpoint/index.ts b/x-pack/test/functional/apps/endpoint/index.ts index 1a0d3e973285ba..e44a4cb846f2c4 100644 --- a/x-pack/test/functional/apps/endpoint/index.ts +++ b/x-pack/test/functional/apps/endpoint/index.ts @@ -10,5 +10,6 @@ export default function({ loadTestFile }: FtrProviderContext) { this.tags('ciGroup7'); loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./landing_page')); }); } diff --git a/x-pack/test/functional/apps/endpoint/landing_page.ts b/x-pack/test/functional/apps/endpoint/landing_page.ts new file mode 100644 index 00000000000000..65af91feae4079 --- /dev/null +++ b/x-pack/test/functional/apps/endpoint/landing_page.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects }: FtrProviderContext) => { + const pageObjects = getPageObjects(['common', 'endpoint']); + + describe('Endpoint landing page', function() { + this.tags('ciGroup7'); + before(async () => { + await pageObjects.common.navigateToApp('endpoint'); + }); + + it('Loads the endpoint app', async () => { + const welcomeEndpointMessage = await pageObjects.endpoint.welcomeEndpointTitle(); + expect(welcomeEndpointMessage).to.be('Hello World'); + }); + }); +}; diff --git a/x-pack/test/functional/page_objects/endpoint_page.ts b/x-pack/test/functional/page_objects/endpoint_page.ts new file mode 100644 index 00000000000000..f02a899f6d37d3 --- /dev/null +++ b/x-pack/test/functional/page_objects/endpoint_page.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function EndpointPageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async welcomeEndpointTitle() { + return await testSubjects.getVisibleText('welcomeTitle'); + }, + }; +} diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 91d4a3663fa659..19a626536f1bdc 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -46,6 +46,7 @@ import { CopySavedObjectsToSpacePageProvider } from './copy_saved_objects_to_spa import { LensPageProvider } from './lens_page'; import { InfraMetricExplorerProvider } from './infra_metric_explorer'; import { RoleMappingsPageProvider } from './role_mappings_page'; +import { EndpointPageProvider } from './endpoint_page'; // just like services, PageObjects are defined as a map of // names to Providers. Merge in Kibana's or pick specific ones @@ -78,4 +79,5 @@ export const pageObjects = { copySavedObjectsToSpace: CopySavedObjectsToSpacePageProvider, lens: LensPageProvider, roleMappings: RoleMappingsPageProvider, + endpoint: EndpointPageProvider, }; From e4e46d1f9469f38b64577b894467190631a1300c Mon Sep 17 00:00:00 2001 From: Peter Schretlen Date: Fri, 7 Feb 2020 17:30:02 -0500 Subject: [PATCH 04/19] Add docs for alerting and action settings (#57035) --- docs/settings/alert-action-settings.asciidoc | 47 ++++++++++++++++++++ docs/settings/settings-xkb.asciidoc | 1 + docs/setup/settings.asciidoc | 11 +---- 3 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 docs/settings/alert-action-settings.asciidoc diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc new file mode 100644 index 00000000000000..e3b9e61667bdf7 --- /dev/null +++ b/docs/settings/alert-action-settings.asciidoc @@ -0,0 +1,47 @@ +[role="xpack"] +[[alert-action-settings-kb]] +=== Alerting and action settings in Kibana +++++ +Alerting and action settings +++++ + +Alerts and actions are enabled by default in {kib}, but require you configure the following in order to use them: + +. <>. +. <>. +. <>. + +You can configure the following settings in the `kibana.yml` file. + + +[float] +[[general-alert-action-settings]] +==== General settings + +`xpack.encrypted_saved_objects.encryptionKey`:: + +A string of 32 or more characters used to encrypt sensitive properties on alerts and actions before they're stored in {es}. Third party credentials — such as the username and password used to connect to an SMTP service — are an example of encrypted properties. ++ +If not set, {kib} will generate a random key on startup, but all alert and action functions will be blocked. Generated keys are not allowed for alerts and actions because when a new key is generated on restart, existing encrypted data becomes inaccessible. For the same reason, alerts and actions in high-availability deployments of {kib} will behave unexpectedly if the key isn't the same on all instances of {kib}. ++ +Although the key can be specified in clear text in `kibana.yml`, it's recommended to store this key securely in the <>. + +[float] +[[alert-settings]] +==== Action settings + +`xpack.actions.whitelistedHosts`:: +A list of hostnames that {kib} is allowed to connect to when built-in actions are triggered. It defaults to `[*]`, allowing any host, but keep in mind the potential for SSRF attacks when hosts are not explicitly whitelisted. An empty list `[]` can be used to block built-in actions from making any external connections. ++ +Note that hosts associated with built-in actions, such as Slack and PagerDuty, are not automatically whitelisted. If you are not using the default `[*]` setting, you must ensure that the corresponding endpoints are whitelisted as well. + +`xpack.actions.enabledActionTypes`:: +A list of action types that are enabled. It defaults to `[*]`, enabling all types. The names for built-in {kib} action types are prefixed with a `.` and include: `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, and `.webhook`. An empty list `[]` will disable all action types. ++ +Disabled action types will not appear as an option when creating new connectors, but existing connectors and actions of that type will remain in {kib} and will not function. + +[float] +[[action-settings]] +==== Alert settings + +You do not need to configure any additional settings to use alerting in {kib}. diff --git a/docs/settings/settings-xkb.asciidoc b/docs/settings/settings-xkb.asciidoc index f509900e058436..f9727db838d551 100644 --- a/docs/settings/settings-xkb.asciidoc +++ b/docs/settings/settings-xkb.asciidoc @@ -10,6 +10,7 @@ include::{asciidoc-dir}/../../shared/settings.asciidoc[] For more {kib} configuration settings, see <>. +include::alert-action-settings.asciidoc[] include::apm-settings.asciidoc[] include::dev-settings.asciidoc[] include::graph-settings.asciidoc[] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 3212cdfafd876a..4eddb1779a26ab 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -457,16 +457,7 @@ Rollup user interface. `i18n.locale`:: *Default: en* Set this value to change the Kibana interface language. Valid locales are: `en`, `zh-CN`, `ja-JP`. -`xpack.actions.enabledActionTypes:`:: *Default: +[ {asterisk} ]+* Set this value -to an array of action types that are enabled. An element of `*` indicates all -action types registered are enabled. The action types provided by Kibana are: -`.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. - -`xpack.actions.whitelistedHosts:`:: *Default: +[ {asterisk} ]+* Set this value -to an array of host names which actions such as email, slack, pagerduty, and -webhook can connect to. An element of `*` indicates any host can be connected -to. An empty array indicates no hosts can be connected to. - +include::{docdir}/settings/alert-action-settings.asciidoc[] include::{docdir}/settings/apm-settings.asciidoc[] include::{docdir}/settings/dev-settings.asciidoc[] include::{docdir}/settings/graph-settings.asciidoc[] From 1246a9861eea81e86ca48cfc322e6af338c5e94a Mon Sep 17 00:00:00 2001 From: Derek Ditch Date: Fri, 7 Feb 2020 17:00:41 -0600 Subject: [PATCH 05/19] [SIEM][detection engine] Limit network rules to filebeat source semantics (#57130) * limit network rules to filebeat source semantics * Bump version number for network rules to ensure updates in post 7.6.0 --- .../network_dns_directly_to_the_internet.json | 7 ++----- ...tp_file_transfer_protocol_activity_to_the_internet.json | 7 ++----- ...ernet_relay_chat_protocol_activity_to_the_internet.json | 7 ++----- .../network_nat_traversal_port_activity.json | 7 ++----- .../rules/prepackaged_rules/network_port_26_activity.json | 7 ++----- .../network_port_8000_activity_to_the_internet.json | 7 ++----- ...rk_pptp_point_to_point_tunneling_protocol_activity.json | 7 ++----- .../network_proxy_port_activity_to_the_internet.json | 7 ++----- ...work_rdp_remote_desktop_protocol_from_the_internet.json | 7 ++----- ...etwork_rdp_remote_desktop_protocol_to_the_internet.json | 7 ++----- ...etwork_rpc_remote_procedure_call_from_the_internet.json | 7 ++----- .../network_rpc_remote_procedure_call_to_the_internet.json | 7 ++----- ..._smb_windows_file_sharing_activity_to_the_internet.json | 7 ++----- .../prepackaged_rules/network_smtp_to_the_internet.json | 7 ++----- .../network_sql_server_port_activity_to_the_internet.json | 7 ++----- .../network_ssh_secure_shell_from_the_internet.json | 7 ++----- .../network_ssh_secure_shell_to_the_internet.json | 7 ++----- .../prepackaged_rules/network_telnet_port_activity.json | 7 ++----- .../network_tor_activity_to_the_internet.json | 7 ++----- ...rk_vnc_virtual_network_computing_from_the_internet.json | 7 ++----- ...work_vnc_virtual_network_computing_to_the_internet.json | 7 ++----- 21 files changed, 42 insertions(+), 105 deletions(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_dns_directly_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_dns_directly_to_the_internet.json index 72e2ca14904173..56c11c236eecb4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_dns_directly_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_dns_directly_to_the_internet.json @@ -4,10 +4,7 @@ "Exclude DNS servers from this rule as this is expected behavior. Endpoints usually query local DNS servers defined in their DHCP scopes, but this may be overridden if a user configures their endpoint to use a remote DNS server. This is uncommon in managed enterprise networks because it could break intranet name resolution when split horizon DNS is utilized. Some consumer VPN services and browser plug-ins may send DNS traffic to remote Internet destinations. In that case, such devices or networks can be excluded from this rule when this is expected behavior." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -42,5 +39,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ftp_file_transfer_protocol_activity_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ftp_file_transfer_protocol_activity_to_the_internet.json index 54daf8a2091a75..a3a692596090cb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ftp_file_transfer_protocol_activity_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ftp_file_transfer_protocol_activity_to_the_internet.json @@ -4,10 +4,7 @@ "FTP servers should be excluded from this rule as this is expected behavior. Some business workflows may use FTP for data exchange. These workflows often have expected characteristics such as users, sources, and destinations. FTP activity involving an unusual source or destination may be more suspicious. FTP activity involving a production server that has no known associated FTP workflow or business requirement is often suspicious." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -53,5 +50,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_irc_internet_relay_chat_protocol_activity_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_irc_internet_relay_chat_protocol_activity_to_the_internet.json index d01006a225886c..0b5259d3417f5d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_irc_internet_relay_chat_protocol_activity_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_irc_internet_relay_chat_protocol_activity_to_the_internet.json @@ -4,10 +4,7 @@ "IRC activity may be normal behavior for developers and engineers but is unusual for non-engineering end users. IRC activity involving an unusual source or destination may be more suspicious. IRC activity involving a production server is often suspicious. Because these ports are in the ephemeral range, this rule may false under certain conditions, such as when a NAT-ed web server replies to a client which has used a port in the range by coincidence. In this case, these servers can be excluded. Some legacy applications may use these ports, but this is very uncommon and usually only appears in local traffic using private IPs, which does not match this rule's conditions." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -53,5 +50,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_nat_traversal_port_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_nat_traversal_port_activity.json index c66a9e9d77fe4b..675fd588a1834c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_nat_traversal_port_activity.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_nat_traversal_port_activity.json @@ -4,10 +4,7 @@ "Some networks may utilize these protocols but usage that is unfamiliar to local network administrators can be unexpected and suspicious. Because this port is in the ephemeral range, this rule may false under certain conditions, such as when an application server with a public IP address replies to a client which has used a UDP port in the range by coincidence. This is uncommon but such servers can be excluded." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -38,5 +35,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_26_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_26_activity.json index 7b261418985329..bc00383f94528d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_26_activity.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_26_activity.json @@ -4,10 +4,7 @@ "Servers that process email traffic may cause false positives and should be excluded from this rule as this is expected behavior." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -57,5 +54,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_8000_activity_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_8000_activity_to_the_internet.json index 7551119cd5a844..f418648bebdb91 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_8000_activity_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_8000_activity_to_the_internet.json @@ -4,10 +4,7 @@ "Because this port is in the ephemeral range, this rule may false under certain conditions, such as when a NATed web server replies to a client which has used a port in the range by coincidence. In this case, such servers can be excluded. Some applications may use this port but this is very uncommon and usually appears in local traffic using private IPs, which this rule does not match. Some cloud environments, particularly development environments, may use this port when VPNs or direct connects are not in use and cloud instances are accessed across the Internet." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -38,5 +35,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_pptp_point_to_point_tunneling_protocol_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_pptp_point_to_point_tunneling_protocol_activity.json index eeb38f756c67a6..2321b813a15528 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_pptp_point_to_point_tunneling_protocol_activity.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_pptp_point_to_point_tunneling_protocol_activity.json @@ -4,10 +4,7 @@ "Some networks may utilize PPTP protocols but this is uncommon as more modern VPN technologies are available. Usage that is unfamiliar to local network administrators can be unexpected and suspicious. Torrenting applications may use this port. Because this port is in the ephemeral range, this rule may false under certain conditions, such as when an application server replies to a client that used this port by coincidence. This is uncommon but such servers can be excluded." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -38,5 +35,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_proxy_port_activity_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_proxy_port_activity_to_the_internet.json index 981a7bdffcfed0..58bba5b3fa7125 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_proxy_port_activity_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_proxy_port_activity_to_the_internet.json @@ -4,10 +4,7 @@ "Some proxied applications may use these ports but this usually occurs in local traffic using private IPs\n which this rule does not match. Proxies are widely used as a security technology but in enterprise environments\n this is usually local traffic which this rule does not match. Internet proxy services using these ports can be\n white-listed if desired. Some screen recording applications may use these ports. Proxy port activity involving\n an unusual source or destination may be more suspicious. Some cloud environments may use this port when VPNs or\n direct connects are not in use and cloud instances are accessed across the Internet. Because these ports are in\n the ephemeral range, this rule may false under certain conditions such as when a NATed web server replies to a\n client which has used a port in the range by coincidence. In this case, such servers can be excluded if desired." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -38,5 +35,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_from_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_from_the_internet.json index 504df93f2f8ed1..03e507753cd226 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_from_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_from_the_internet.json @@ -4,10 +4,7 @@ " Some network security policies allow RDP directly from the Internet but usage that is unfamiliar to\n server or network owners can be unexpected and suspicious. RDP services may be exposed directly to the\n Internet in some networks such as cloud environments. In such cases, only RDP gateways, bastions or jump\n servers may be expected expose RDP directly to the Internet and can be exempted from this rule. RDP may\n be required by some work-flows such as remote access and support for specialized software products and\n servers. Such work-flows are usually known and not unexpected." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -68,5 +65,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_to_the_internet.json index 2d9fa6ba06dfdc..af2279b2d9008a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_to_the_internet.json @@ -4,10 +4,7 @@ "RDP connections may be made directly to Internet destinations in order to access\n Windows cloud server instances but such connections are usually made only by engineers.\n In such cases, only RDP gateways, bastions or jump servers may be expected Internet\n destinations and can be exempted from this rule. RDP may be required by some work-flows\n such as remote access and support for specialized software products and servers. Such\n work-flows are usually known and not unexpected. Usage that is unfamiliar to server or\n network owners can be unexpected and suspicious." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -53,5 +50,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_from_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_from_the_internet.json index d50c79db81ba5c..4539d639a593ac 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_from_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_from_the_internet.json @@ -1,10 +1,7 @@ { "description": "This rule detects network events that may indicate the use of RPC traffic\nfrom the Internet. RPC is commonly used by system administrators to remotely\ncontrol a system for maintenance or to use shared resources. It should almost\nnever be directly exposed to the Internet, as it is frequently targeted and\nexploited by threat actors as an initial access or back-door vector.\n", "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -35,5 +32,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_to_the_internet.json index ade7b661a79091..dd1b57572bcb3f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_to_the_internet.json @@ -1,10 +1,7 @@ { "description": "This rule detects network events that may indicate the use of RPC traffic\nto the Internet. RPC is commonly used by system administrators to remotely\ncontrol a system for maintenance or to use shared resources. It should almost\nnever be directly exposed to the Internet, as it is frequently targeted and\nexploited by threat actors as an initial access or back-door vector.\n", "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -35,5 +32,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smb_windows_file_sharing_activity_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smb_windows_file_sharing_activity_to_the_internet.json index 62c2fafb7404ff..8b97df21829922 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smb_windows_file_sharing_activity_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smb_windows_file_sharing_activity_to_the_internet.json @@ -1,10 +1,7 @@ { "description": "This rule detects network events that may indicate the use of Windows\nfile sharing (also called SMB or CIFS) traffic to the Internet. SMB is commonly\nused within networks to share files, printers, and other system resources amongst\ntrusted systems. It should almost never be directly exposed to the Internet, as\nit is frequently targeted and exploited by threat actors as an initial access\nor back-door vector or for data exfiltration.\n", "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -50,5 +47,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smtp_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smtp_to_the_internet.json index 02fca5603910e9..c6aa5eef372f40 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smtp_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smtp_to_the_internet.json @@ -4,10 +4,7 @@ "NATed servers that process email traffic may false and should be excluded from this rule as this is expected behavior for them. Consumer and personal devices may send email traffic to remote Internet destinations. In this case, such devices or networks can be excluded from this rule if this is expected behavior." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -53,5 +50,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_sql_server_port_activity_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_sql_server_port_activity_to_the_internet.json index 67e6a08ddf7918..f11d9705bbda4e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_sql_server_port_activity_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_sql_server_port_activity_to_the_internet.json @@ -4,10 +4,7 @@ "Because these ports are in the ephemeral range, this rule may false under certain conditions\n such as when a NATed web server replies to a client which has used a port in the range by\n coincidence. In this case, such servers can be excluded if desired. Some cloud environments may\n use this port when VPNs or direct connects are not in use and database instances are accessed\n directly across the Internet." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -38,5 +35,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_from_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_from_the_internet.json index 052600a0db68a1..a95447fc088dfa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_from_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_from_the_internet.json @@ -4,10 +4,7 @@ "Some network security policies allow SSH directly from the Internet but usage that is\n unfamiliar to server or network owners can be unexpected and suspicious. SSH services may\n be exposed directly to the Internet in some networks such as cloud environments. In such\n cases, only SSH gateways, bastions or jump servers may be expected expose SSH directly to\n the Internet and can be exempted from this rule. SSH may be required by some work-flows\n such as remote access and support for specialized software products and servers. Such\n work-flows are usually known and not unexpected." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -68,5 +65,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_to_the_internet.json index e3c3135c9240d5..b17d35f96324de 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_to_the_internet.json @@ -4,10 +4,7 @@ "SSH connections may be made directly to Internet destinations in order to access Linux\n cloud server instances but such connections are usually made only by engineers. In such cases,\n only SSH gateways, bastions or jump servers may be expected Internet destinations and can be\n exempted from this rule. SSH may be required by some work-flows such as remote access and support\n for specialized software products and servers. Such work-flows are usually known and not unexpected.\n Usage that is unfamiliar to server or network owners can be unexpected and suspicious." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -38,5 +35,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_telnet_port_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_telnet_port_activity.json index c05791c8a107d8..99813595013cfd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_telnet_port_activity.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_telnet_port_activity.json @@ -4,10 +4,7 @@ "IoT (Internet of Things) devices and networks may use telnet and can be excluded if\n desired. Some business work-flows may use Telnet for administration of older devices. These\n often have a predictable behavior. Telnet activity involving an unusual source or destination\n may be more suspicious. Telnet activity involving a production server that has no known\n associated Telnet work-flow or business requirement is often suspicious." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -68,5 +65,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_tor_activity_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_tor_activity_to_the_internet.json index 64397716eded26..47960f879dfb6b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_tor_activity_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_tor_activity_to_the_internet.json @@ -4,10 +4,7 @@ "Tor client activity is uncommon in managed enterprise networks but may be common\n in unmanaged or public networks where few security policies apply. Because these ports\n are in the ephemeral range, this rule may false under certain conditions such as when a\n NATed web server replies to a client which has used one of these ports by coincidence.\n In this case, such servers can be excluded if desired." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -53,5 +50,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_from_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_from_the_internet.json index dc4fbb281f7622..d9195a2d2e98c7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_from_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_from_the_internet.json @@ -4,10 +4,7 @@ "VNC connections may be received directly to Linux cloud server instances but\n such connections are usually made only by engineers. VNC is less common than SSH\n or RDP but may be required by some work-flows such as remote access and support\n for specialized software products or servers. Such work-flows are usually known\n and not unexpected. Usage that is unfamiliar to server or network owners can be\n unexpected and suspicious." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -53,5 +50,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_to_the_internet.json index 7da5db39d9bfe3..57131e28ee9a71 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_to_the_internet.json @@ -4,10 +4,7 @@ "VNC connections may be made directly to Linux cloud server instances but such\n connections are usually made only by engineers. VNC is less common than SSH or RDP\n but may be required by some work flows such as remote access and support for\n specialized software products or servers. Such work-flows are usually known and not\n unexpected. Usage that is unfamiliar to server or network owners can be unexpected\n and suspicious." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -38,5 +35,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } From b90bd85990d0e7529a9e218e57d58e6d6b479a68 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Sun, 9 Feb 2020 15:38:28 +0200 Subject: [PATCH 06/19] top nav ts arg support (#56984) timefilter initial state Co-authored-by: Elastic Machine --- src/plugins/data/public/ui/index.ts | 2 +- .../public/ui/search_bar/create_search_bar.tsx | 4 ++++ .../data/public/ui/search_bar/index.tsx | 1 + .../public/ui/search_bar/lib/use_timefilter.ts | 18 +++++++++++++++--- .../data/public/ui/search_bar/search_bar.tsx | 10 +++++----- .../public/top_nav_menu/top_nav_menu.test.tsx | 14 +------------- .../public/top_nav_menu/top_nav_menu.tsx | 5 ++--- 7 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index 0755363c9b16b3..5a1ad9957d7d75 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -21,7 +21,7 @@ export { SuggestionsComponent } from './typeahead/suggestions_component'; export { IndexPatternSelect } from './index_pattern_select'; export { FilterBar } from './filter_bar'; export { QueryStringInput } from './query_string_input/query_string_input'; -export { SearchBar, SearchBarProps } from './search_bar'; +export { SearchBar, SearchBarProps, StatefulSearchBarProps } from './search_bar'; // @internal export { diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 71d76f4db49e21..c24c20bd08fb8e 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -132,6 +132,10 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) filterManager: data.query.filterManager, }); const { timeRange, refreshInterval } = useTimefilter({ + dateRangeFrom: props.dateRangeFrom, + dateRangeTo: props.dateRangeTo, + refreshInterval: props.refreshInterval, + isRefreshPaused: props.isRefreshPaused, timefilter: data.query.timefilter.timefilter, }); diff --git a/src/plugins/data/public/ui/search_bar/index.tsx b/src/plugins/data/public/ui/search_bar/index.tsx index 4aa7f5fe2b0400..fbc9f4a41ebbfe 100644 --- a/src/plugins/data/public/ui/search_bar/index.tsx +++ b/src/plugins/data/public/ui/search_bar/index.tsx @@ -18,3 +18,4 @@ */ export { SearchBar, SearchBarProps } from './search_bar'; +export { StatefulSearchBarProps } from './create_search_bar'; diff --git a/src/plugins/data/public/ui/search_bar/lib/use_timefilter.ts b/src/plugins/data/public/ui/search_bar/lib/use_timefilter.ts index 942902ebd72866..b56c717df4978b 100644 --- a/src/plugins/data/public/ui/search_bar/lib/use_timefilter.ts +++ b/src/plugins/data/public/ui/search_bar/lib/use_timefilter.ts @@ -19,15 +19,27 @@ import { useState, useEffect } from 'react'; import { Subscription } from 'rxjs'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { DataPublicPluginStart, TimeRange, RefreshInterval } from 'src/plugins/data/public'; interface UseTimefilterProps { + dateRangeFrom?: string; + dateRangeTo?: string; + refreshInterval?: number; + isRefreshPaused?: boolean; timefilter: DataPublicPluginStart['query']['timefilter']['timefilter']; } export const useTimefilter = (props: UseTimefilterProps) => { - const [timeRange, setTimerange] = useState(props.timefilter.getTime()); - const [refreshInterval, setRefreshInterval] = useState(props.timefilter.getRefreshInterval()); + const initialTimeRange: TimeRange = { + from: props.dateRangeFrom || props.timefilter.getTime().from, + to: props.dateRangeTo || props.timefilter.getTime().to, + }; + const initialRefreshInterval: RefreshInterval = { + value: props.refreshInterval || props.timefilter.getRefreshInterval().value, + pause: props.isRefreshPaused || props.timefilter.getRefreshInterval().pause, + }; + const [timeRange, setTimerange] = useState(initialTimeRange); + const [refreshInterval, setRefreshInterval] = useState(initialRefreshInterval); useEffect(() => { const subscriptions = new Subscription(); diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index 2f0cdb322912b9..8d2219bc5731fd 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -47,13 +47,8 @@ interface SearchBarInjectedDeps { timeHistory: TimeHistoryContract; // Filter bar onFiltersUpdated?: (filters: esFilters.Filter[]) => void; - // Date picker - dateRangeFrom?: string; - dateRangeTo?: string; // Autorefresh onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void; - isRefreshPaused?: boolean; - refreshInterval?: number; } export interface SearchBarOwnProps { @@ -69,6 +64,11 @@ export interface SearchBarOwnProps { showDatePicker?: boolean; showAutoRefreshOnly?: boolean; filters?: esFilters.Filter[]; + // Date picker + isRefreshPaused?: boolean; + refreshInterval?: number; + dateRangeFrom?: string; + dateRangeTo?: string; // Query bar - should be in SearchBarInjectedDeps query?: Query; // Show when user has privileges to save diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx index 4e2ea44bf76422..8e0e8b3031132b 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx @@ -22,13 +22,6 @@ import { TopNavMenu } from './top_nav_menu'; import { TopNavMenuData } from './top_nav_menu_data'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -const mockTimeHistory = { - add: () => {}, - get: () => { - return []; - }, -}; - const dataShim = { ui: { SearchBar: () =>
, @@ -76,12 +69,7 @@ describe('TopNavMenu', () => { it('Should render search bar', () => { const component = shallowWithIntl( - + ); expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0); diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index 849a4b033399eb..cf39c82eff3ce5 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -24,10 +24,9 @@ import { I18nProvider } from '@kbn/i18n/react'; import { TopNavMenuData } from './top_nav_menu_data'; import { TopNavMenuItem } from './top_nav_menu_item'; -import { SearchBarProps, DataPublicPluginStart } from '../../../data/public'; +import { StatefulSearchBarProps, DataPublicPluginStart } from '../../../data/public'; -export type TopNavMenuProps = Partial & { - appName: string; +export type TopNavMenuProps = StatefulSearchBarProps & { config?: TopNavMenuData[]; showSearchBar?: boolean; data?: DataPublicPluginStart; From a02232d62b63436daa18e145f4caa2f185ab2894 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 10 Feb 2020 12:11:20 +1300 Subject: [PATCH 07/19] adds ability to fetch Alert and Alert Instance state (#56625) Enables access to the Alert State, which allows us to see which current Alert Instances are active. This includes: 1. Addition of a `get` api on Task Manager 2. Typing and validation on Serialisation & Deserialisation of the State of an Alert's underlying Task 3. Addition of the `getAlertState` api on AlertsClient --- x-pack/legacy/plugins/alerting/README.md | 9 + .../alert_instance/alert_instance.test.ts | 18 +- .../server/alert_instance/alert_instance.ts | 41 ++-- .../create_alert_instance_factory.test.ts | 2 +- .../alerting/server/alert_instance/index.ts | 2 +- .../alerting/server/alerts_client.mock.ts | 1 + .../alerting/server/alerts_client.test.ts | 114 +++++++++ .../plugins/alerting/server/alerts_client.ts | 12 + .../plugins/alerting/server/lib/types.test.ts | 28 +++ .../plugins/alerting/server/lib/types.ts | 25 ++ .../legacy/plugins/alerting/server/plugin.ts | 2 + .../server/routes/get_alert_state.test.ts | 73 ++++++ .../alerting/server/routes/get_alert_state.ts | 35 +++ .../plugins/alerting/server/routes/index.ts | 1 + .../task_runner/alert_task_instance.test.ts | 229 ++++++++++++++++++ .../server/task_runner/alert_task_instance.ts | 66 +++++ .../server/task_runner/task_runner.ts | 44 ++-- .../legacy/plugins/alerting/server/types.ts | 2 +- .../server/alerts/license_expiration.test.ts | 1 + .../plugins/task_manager/server/legacy.ts | 1 + x-pack/plugins/task_manager/server/README.md | 3 + .../server/create_task_manager.test.ts | 31 +-- x-pack/plugins/task_manager/server/mocks.ts | 1 + x-pack/plugins/task_manager/server/plugin.ts | 3 +- .../task_manager/server/task_manager.mock.ts | 1 + .../task_manager/server/task_manager.ts | 11 + .../common/fixtures/plugins/alerts/index.ts | 26 +- .../tests/alerting/get_alert_state.ts | 126 ++++++++++ .../tests/alerting/index.ts | 1 + .../tests/alerting/get_alert_state.ts | 89 +++++++ .../spaces_only/tests/alerting/index.ts | 1 + .../plugins/task_manager/init_routes.js | 23 +- .../task_manager/task_manager_integration.js | 6 +- 33 files changed, 958 insertions(+), 70 deletions(-) create mode 100644 x-pack/legacy/plugins/alerting/server/lib/types.test.ts create mode 100644 x-pack/legacy/plugins/alerting/server/lib/types.ts create mode 100644 x-pack/legacy/plugins/alerting/server/routes/get_alert_state.test.ts create mode 100644 x-pack/legacy/plugins/alerting/server/routes/get_alert_state.ts create mode 100644 x-pack/legacy/plugins/alerting/server/task_runner/alert_task_instance.test.ts create mode 100644 x-pack/legacy/plugins/alerting/server/task_runner/alert_task_instance.ts create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts diff --git a/x-pack/legacy/plugins/alerting/README.md b/x-pack/legacy/plugins/alerting/README.md index 4de45fe96a400c..eb9df042f92542 100644 --- a/x-pack/legacy/plugins/alerting/README.md +++ b/x-pack/legacy/plugins/alerting/README.md @@ -23,6 +23,7 @@ Table of Contents - [`DELETE /api/alert/{id}`: Delete alert](#delete-apialertid-delete-alert) - [`GET /api/alert/_find`: Find alerts](#get-apialertfind-find-alerts) - [`GET /api/alert/{id}`: Get alert](#get-apialertid-get-alert) + - [`GET /api/alert/{id}/state`: Get alert state](#get-apialertidstate-get-alert-state) - [`GET /api/alert/types`: List alert types](#get-apialerttypes-list-alert-types) - [`PUT /api/alert/{id}`: Update alert](#put-apialertid-update-alert) - [`POST /api/alert/{id}/_enable`: Enable an alert](#post-apialertidenable-enable-an-alert) @@ -273,6 +274,14 @@ Params: |---|---|---| |id|The id of the alert you're trying to get.|string| +### `GET /api/alert/{id}/state`: Get alert state + +Params: + +|Property|Description|Type| +|---|---|---| +|id|The id of the alert whose state you're trying to get.|string| + ### `GET /api/alert/types`: List alert types No parameters. diff --git a/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.test.ts b/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.test.ts index 6a80f4d2de4cbb..c5f93edfb74e5f 100644 --- a/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.test.ts @@ -192,7 +192,7 @@ describe('updateLastScheduledActions()', () => { state: {}, meta: { lastScheduledActions: { - date: new Date(), + date: new Date().toISOString(), group: 'default', }, }, @@ -216,3 +216,19 @@ describe('toJSON', () => { ); }); }); + +describe('toRaw', () => { + test('returns unserialised underlying state and meta', () => { + const raw = { + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }; + const alertInstance = new AlertInstance(raw); + expect(alertInstance.toRaw()).toEqual(raw); + }); +}); diff --git a/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.ts b/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.ts index a56e2077cdfd83..df67f7d2a1d9ee 100644 --- a/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.ts +++ b/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.ts @@ -3,34 +3,41 @@ * 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 t from 'io-ts'; import { State, Context } from '../types'; +import { DateFromString } from '../lib/types'; import { parseDuration } from '../lib'; -interface Meta { - lastScheduledActions?: { - group: string; - date: Date; - }; -} - interface ScheduledExecutionOptions { actionGroup: string; context: Context; state: State; } -interface ConstructorOptions { - state?: State; - meta?: Meta; -} +const metaSchema = t.partial({ + lastScheduledActions: t.type({ + group: t.string, + date: DateFromString, + }), +}); +type AlertInstanceMeta = t.TypeOf; + +const stateSchema = t.record(t.string, t.unknown); +type AlertInstanceState = t.TypeOf; + +export const rawAlertInstance = t.partial({ + state: stateSchema, + meta: metaSchema, +}); +export type RawAlertInstance = t.TypeOf; export class AlertInstance { private scheduledExecutionOptions?: ScheduledExecutionOptions; - private meta: Meta; - private state: State; + private meta: AlertInstanceMeta; + private state: AlertInstanceState; - constructor({ state = {}, meta = {} }: ConstructorOptions = {}) { + constructor({ state = {}, meta = {} }: RawAlertInstance = {}) { this.state = state; this.meta = meta; } @@ -48,7 +55,7 @@ export class AlertInstance { if ( this.meta.lastScheduledActions && this.meta.lastScheduledActions.group === actionGroup && - new Date(this.meta.lastScheduledActions.date).getTime() + throttleMills > Date.now() + this.meta.lastScheduledActions.date.getTime() + throttleMills > Date.now() ) { return true; } @@ -89,6 +96,10 @@ export class AlertInstance { * Used to serialize alert instance state */ toJSON() { + return rawAlertInstance.encode(this.toRaw()); + } + + toRaw(): RawAlertInstance { return { state: this.state, meta: this.meta, diff --git a/x-pack/legacy/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts b/x-pack/legacy/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts index 914f726ebbd786..03bc8b7cc3b145 100644 --- a/x-pack/legacy/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts @@ -40,7 +40,7 @@ test('reuses existing instances', () => { Object { "meta": Object { "lastScheduledActions": Object { - "date": 1970-01-01T00:00:00.000Z, + "date": "1970-01-01T00:00:00.000Z", "group": "default", }, }, diff --git a/x-pack/legacy/plugins/alerting/server/alert_instance/index.ts b/x-pack/legacy/plugins/alerting/server/alert_instance/index.ts index 40ee0874e805cd..fc828096adf284 100644 --- a/x-pack/legacy/plugins/alerting/server/alert_instance/index.ts +++ b/x-pack/legacy/plugins/alerting/server/alert_instance/index.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { AlertInstance } from './alert_instance'; +export { AlertInstance, RawAlertInstance, rawAlertInstance } from './alert_instance'; export { createAlertInstanceFactory } from './create_alert_instance_factory'; diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.mock.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.mock.ts index c7d359491680f5..3189fa214d5f7b 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.mock.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.mock.ts @@ -12,6 +12,7 @@ const createAlertsClientMock = () => { const mocked: jest.Mocked = { create: jest.fn(), get: jest.fn(), + getAlertState: jest.fn(), find: jest.fn(), delete: jest.fn(), update: jest.fn(), diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index 56ccf08d6a44fb..f9d1d97a521fe7 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -1356,6 +1356,120 @@ describe('get()', () => { }); }); +describe('getAlertState()', () => { + test('calls saved objects client with given params', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + + taskManager.get.mockResolvedValueOnce({ + id: '1', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + + await alertsClient.getAlertState({ id: '1' }); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alert", + "1", + ] + `); + }); + + test('gets the underlying task from TaskManager', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + + const scheduledTaskId = 'task-123'; + + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + enabled: true, + scheduledTaskId, + mutedInstanceIds: [], + muteAll: true, + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + + taskManager.get.mockResolvedValueOnce({ + id: scheduledTaskId, + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: { + alertId: '1', + }, + ownerId: null, + }); + + await alertsClient.getAlertState({ id: '1' }); + expect(taskManager.get).toHaveBeenCalledTimes(1); + expect(taskManager.get).toHaveBeenCalledWith(scheduledTaskId); + }); +}); + describe('find()', () => { test('calls saved objects client with given params', async () => { const alertsClient = new AlertsClient(alertsClientParams); diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index 40125f3067ee3e..f6841ed5a0e46d 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -31,6 +31,7 @@ import { } from '../../../../plugins/security/server'; import { EncryptedSavedObjectsPluginStart } from '../../../../plugins/encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../../../plugins/task_manager/server'; +import { AlertTaskState, taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance'; type NormalizedAlertAction = Omit; export type CreateAPIKeyResult = @@ -204,6 +205,17 @@ export class AlertsClient { return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references); } + public async getAlertState({ id }: { id: string }): Promise { + const alert = await this.get({ id }); + if (alert.scheduledTaskId) { + const { state } = taskInstanceToAlertTaskInstance( + await this.taskManager.get(alert.scheduledTaskId), + alert + ); + return state; + } + } + public async find({ options = {} }: FindOptions = {}): Promise { const { page, diff --git a/x-pack/legacy/plugins/alerting/server/lib/types.test.ts b/x-pack/legacy/plugins/alerting/server/lib/types.test.ts new file mode 100644 index 00000000000000..517b66aa2faabe --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/lib/types.test.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 { DateFromString } from './types'; +import { right, isLeft } from 'fp-ts/lib/Either'; + +describe('DateFromString', () => { + test('validated and parses a string into a Date', () => { + const date = new Date(1973, 10, 30); + expect(DateFromString.decode(date.toISOString())).toEqual(right(date)); + }); + + test('validated and returns a failure for an actual Date', () => { + const date = new Date(1973, 10, 30); + expect(isLeft(DateFromString.decode(date))).toEqual(true); + }); + + test('validated and returns a failure for an invalid Date string', () => { + expect(isLeft(DateFromString.decode('1234-23-45'))).toEqual(true); + }); + + test('validated and returns a failure for a null value', () => { + expect(isLeft(DateFromString.decode(null))).toEqual(true); + }); +}); diff --git a/x-pack/legacy/plugins/alerting/server/lib/types.ts b/x-pack/legacy/plugins/alerting/server/lib/types.ts new file mode 100644 index 00000000000000..6df593ab17ce81 --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/lib/types.ts @@ -0,0 +1,25 @@ +/* + * 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 t from 'io-ts'; +import { either } from 'fp-ts/lib/Either'; +// represents a Date from an ISO string +export const DateFromString = new t.Type( + 'DateFromString', + // detect the type + (value): value is Date => value instanceof Date, + (valueToDecode, context) => + either.chain( + // validate this is a string + t.string.validate(valueToDecode, context), + // decode + value => { + const decoded = new Date(value); + return isNaN(decoded.getTime()) ? t.failure(valueToDecode, context) : t.success(decoded); + } + ), + valueToEncode => valueToEncode.toISOString() +); diff --git a/x-pack/legacy/plugins/alerting/server/plugin.ts b/x-pack/legacy/plugins/alerting/server/plugin.ts index a4de7af376fb04..e3f7656002d182 100644 --- a/x-pack/legacy/plugins/alerting/server/plugin.ts +++ b/x-pack/legacy/plugins/alerting/server/plugin.ts @@ -25,6 +25,7 @@ import { deleteAlertRoute, findAlertRoute, getAlertRoute, + getAlertStateRoute, listAlertTypesRoute, updateAlertRoute, enableAlertRoute, @@ -92,6 +93,7 @@ export class Plugin { core.http.route(extendRouteWithLicenseCheck(deleteAlertRoute, this.licenseState)); core.http.route(extendRouteWithLicenseCheck(findAlertRoute, this.licenseState)); core.http.route(extendRouteWithLicenseCheck(getAlertRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(getAlertStateRoute, this.licenseState)); core.http.route(extendRouteWithLicenseCheck(listAlertTypesRoute, this.licenseState)); core.http.route(extendRouteWithLicenseCheck(updateAlertRoute, this.licenseState)); core.http.route(extendRouteWithLicenseCheck(enableAlertRoute, this.licenseState)); diff --git a/x-pack/legacy/plugins/alerting/server/routes/get_alert_state.test.ts b/x-pack/legacy/plugins/alerting/server/routes/get_alert_state.test.ts new file mode 100644 index 00000000000000..9e3b3b6579eadc --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/routes/get_alert_state.test.ts @@ -0,0 +1,73 @@ +/* + * 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 { createMockServer } from './_mock_server'; +import { getAlertStateRoute } from './get_alert_state'; +import { SavedObjectsErrorHelpers } from 'src/core/server'; + +const { server, alertsClient } = createMockServer(); +server.route(getAlertStateRoute); + +const mockedAlertState = { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: { + state: {}, + meta: { + lastScheduledActions: { + group: 'first_group', + date: new Date(), + }, + }, + }, + second_instance: {}, + }, +}; + +beforeEach(() => jest.resetAllMocks()); + +test('gets alert state', async () => { + const request = { + method: 'GET', + url: '/api/alert/1/state', + }; + + alertsClient.getAlertState.mockResolvedValueOnce(mockedAlertState); + + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + expect(alertsClient.getAlertState).toHaveBeenCalledWith({ id: '1' }); +}); + +test('returns NO-CONTENT when alert exists but has no task state yet', async () => { + const request = { + method: 'GET', + url: '/api/alert/1/state', + }; + + alertsClient.getAlertState.mockResolvedValueOnce(undefined); + + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(204); + expect(alertsClient.getAlertState).toHaveBeenCalledWith({ id: '1' }); +}); + +test('returns NOT-FOUND when alert is not found', async () => { + const request = { + method: 'GET', + url: '/api/alert/1/state', + }; + + alertsClient.getAlertState.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError('alert', '1') + ); + + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(404); + expect(alertsClient.getAlertState).toHaveBeenCalledWith({ id: '1' }); +}); diff --git a/x-pack/legacy/plugins/alerting/server/routes/get_alert_state.ts b/x-pack/legacy/plugins/alerting/server/routes/get_alert_state.ts new file mode 100644 index 00000000000000..12136a975bb19c --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/routes/get_alert_state.ts @@ -0,0 +1,35 @@ +/* + * 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 Joi from 'joi'; +import Hapi from 'hapi'; + +interface GetAlertStateRequest extends Hapi.Request { + params: { + id: string; + }; +} + +export const getAlertStateRoute = { + method: 'GET', + path: '/api/alert/{id}/state', + options: { + tags: ['access:alerting-read'], + validate: { + params: Joi.object() + .keys({ + id: Joi.string().required(), + }) + .required(), + }, + }, + async handler(request: GetAlertStateRequest, h: Hapi.ResponseToolkit) { + const { id } = request.params; + const alertsClient = request.getAlertsClient!(); + const state = await alertsClient.getAlertState({ id }); + return state ? state : h.response().code(204); + }, +}; diff --git a/x-pack/legacy/plugins/alerting/server/routes/index.ts b/x-pack/legacy/plugins/alerting/server/routes/index.ts index 02cba8adc9db2d..7ec901ae685c47 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/index.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/index.ts @@ -8,6 +8,7 @@ export { createAlertRoute } from './create'; export { deleteAlertRoute } from './delete'; export { findAlertRoute } from './find'; export { getAlertRoute } from './get'; +export { getAlertStateRoute } from './get_alert_state'; export { listAlertTypesRoute } from './list_alert_types'; export { updateAlertRoute } from './update'; export { enableAlertRoute } from './enable'; diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/alert_task_instance.test.ts b/x-pack/legacy/plugins/alerting/server/task_runner/alert_task_instance.test.ts new file mode 100644 index 00000000000000..9cbe91a4dbcedd --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/task_runner/alert_task_instance.test.ts @@ -0,0 +1,229 @@ +/* + * 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 { ConcreteTaskInstance, TaskStatus } from '../../../../../plugins/task_manager/server'; +import { AlertTaskInstance, taskInstanceToAlertTaskInstance } from './alert_task_instance'; +import uuid from 'uuid'; +import { SanitizedAlert } from '../types'; + +const alert: SanitizedAlert = { + id: 'alert-123', + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [], + enabled: true, + name: '', + tags: [], + consumer: '', + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], +}; + +describe('Alert Task Instance', () => { + test(`validates that a TaskInstance has valid Alert Task State`, () => { + const lastScheduledActionsDate = new Date(); + const taskInstance: ConcreteTaskInstance = { + id: uuid.v4(), + attempts: 0, + status: TaskStatus.Running, + version: '123', + runAt: new Date(), + scheduledAt: new Date(), + startedAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: { + state: {}, + meta: { + lastScheduledActions: { + group: 'first_group', + date: lastScheduledActionsDate.toISOString(), + }, + }, + }, + second_instance: {}, + }, + }, + taskType: 'alerting:test', + params: { + alertId: '1', + }, + ownerId: null, + }; + + const alertTaskInsatnce: AlertTaskInstance = taskInstanceToAlertTaskInstance(taskInstance); + + expect(alertTaskInsatnce).toEqual({ + ...taskInstance, + state: { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: { + state: {}, + meta: { + lastScheduledActions: { + group: 'first_group', + date: lastScheduledActionsDate, + }, + }, + }, + second_instance: {}, + }, + }, + }); + }); + + test(`throws if state is invalid`, () => { + const taskInstance: ConcreteTaskInstance = { + id: '215ee69b-1df9-428e-ab1a-ccf274f8fa5b', + attempts: 0, + status: TaskStatus.Running, + version: '123', + runAt: new Date(), + scheduledAt: new Date(), + startedAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: 'invalid', + second_instance: {}, + }, + }, + taskType: 'alerting:test', + params: { + alertId: '1', + }, + ownerId: null, + }; + + expect(() => taskInstanceToAlertTaskInstance(taskInstance)).toThrowErrorMatchingInlineSnapshot( + `"Task \\"215ee69b-1df9-428e-ab1a-ccf274f8fa5b\\" has invalid state at .alertInstances.first_instance"` + ); + }); + + test(`throws with Alert id when alert is present and state is invalid`, () => { + const taskInstance: ConcreteTaskInstance = { + id: '215ee69b-1df9-428e-ab1a-ccf274f8fa5b', + attempts: 0, + status: TaskStatus.Running, + version: '123', + runAt: new Date(), + scheduledAt: new Date(), + startedAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: 'invalid', + second_instance: {}, + }, + }, + taskType: 'alerting:test', + params: { + alertId: '1', + }, + ownerId: null, + }; + + expect(() => + taskInstanceToAlertTaskInstance(taskInstance, alert) + ).toThrowErrorMatchingInlineSnapshot( + `"Task \\"215ee69b-1df9-428e-ab1a-ccf274f8fa5b\\" (underlying Alert \\"alert-123\\") has invalid state at .alertInstances.first_instance"` + ); + }); + + test(`allows an initial empty state`, () => { + const taskInstance: ConcreteTaskInstance = { + id: uuid.v4(), + attempts: 0, + status: TaskStatus.Running, + version: '123', + runAt: new Date(), + scheduledAt: new Date(), + startedAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: {}, + taskType: 'alerting:test', + params: { + alertId: '1', + }, + ownerId: null, + }; + + const alertTaskInsatnce: AlertTaskInstance = taskInstanceToAlertTaskInstance(taskInstance); + + expect(alertTaskInsatnce).toEqual(taskInstance); + }); + + test(`validates that a TaskInstance has valid Params`, () => { + const taskInstance: ConcreteTaskInstance = { + id: uuid.v4(), + attempts: 0, + status: TaskStatus.Running, + version: '123', + runAt: new Date(), + scheduledAt: new Date(), + startedAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: {}, + taskType: 'alerting:test', + params: { + alertId: '1', + }, + ownerId: null, + }; + + const alertTaskInsatnce: AlertTaskInstance = taskInstanceToAlertTaskInstance( + taskInstance, + alert + ); + + expect(alertTaskInsatnce).toEqual(taskInstance); + }); + + test(`throws if params are invalid`, () => { + const taskInstance: ConcreteTaskInstance = { + id: '215ee69b-1df9-428e-ab1a-ccf274f8fa5b', + attempts: 0, + status: TaskStatus.Running, + version: '123', + runAt: new Date(), + scheduledAt: new Date(), + startedAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: {}, + taskType: 'alerting:test', + params: {}, + ownerId: null, + }; + + expect(() => + taskInstanceToAlertTaskInstance(taskInstance, alert) + ).toThrowErrorMatchingInlineSnapshot( + `"Task \\"215ee69b-1df9-428e-ab1a-ccf274f8fa5b\\" (underlying Alert \\"alert-123\\") has an invalid param at .0.alertId"` + ); + }); +}); diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/alert_task_instance.ts b/x-pack/legacy/plugins/alerting/server/task_runner/alert_task_instance.ts new file mode 100644 index 00000000000000..33b416fe8e2da2 --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/task_runner/alert_task_instance.ts @@ -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 * as t from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { ConcreteTaskInstance } from '../../../../../plugins/task_manager/server'; +import { SanitizedAlert } from '../types'; +import { DateFromString } from '../lib/types'; +import { AlertInstance, rawAlertInstance } from '../alert_instance'; + +export interface AlertTaskInstance extends ConcreteTaskInstance { + state: AlertTaskState; +} + +export const alertStateSchema = t.partial({ + alertTypeState: t.record(t.string, t.unknown), + alertInstances: t.record(t.string, rawAlertInstance), + previousStartedAt: t.union([t.null, DateFromString]), +}); +export type AlertInstances = Record; +export type AlertTaskState = t.TypeOf; + +const alertParamsSchema = t.intersection([ + t.type({ + alertId: t.string, + }), + t.partial({ + spaceId: t.string, + }), +]); +export type AlertTaskParams = t.TypeOf; + +const enumerateErrorFields = (e: t.Errors) => + `${e.map(({ context }) => context.map(({ key }) => key).join('.'))}`; + +export function taskInstanceToAlertTaskInstance( + taskInstance: ConcreteTaskInstance, + alert?: SanitizedAlert +): AlertTaskInstance { + return { + ...taskInstance, + params: pipe( + alertParamsSchema.decode(taskInstance.params), + fold((e: t.Errors) => { + throw new Error( + `Task "${taskInstance.id}" ${ + alert ? `(underlying Alert "${alert.id}") ` : '' + }has an invalid param at ${enumerateErrorFields(e)}` + ); + }, t.identity) + ), + state: pipe( + alertStateSchema.decode(taskInstance.state), + fold((e: t.Errors) => { + throw new Error( + `Task "${taskInstance.id}" ${ + alert ? `(underlying Alert "${alert.id}") ` : '' + }has invalid state at ${enumerateErrorFields(e)}` + ); + }, t.identity) + ), + }; +} diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts index 0f643e3d3121cc..1466d3ccd274b5 100644 --- a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts @@ -10,25 +10,32 @@ import { SavedObject } from '../../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance } from '../../../../../plugins/task_manager/server'; import { createExecutionHandler } from './create_execution_handler'; -import { AlertInstance, createAlertInstanceFactory } from '../alert_instance'; +import { AlertInstance, createAlertInstanceFactory, RawAlertInstance } from '../alert_instance'; import { getNextRunAt } from './get_next_run_at'; import { validateAlertTypeParams } from '../lib'; -import { AlertType, RawAlert, IntervalSchedule, Services, State, AlertInfoParams } from '../types'; +import { AlertType, RawAlert, IntervalSchedule, Services, AlertInfoParams } from '../types'; import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type'; - -type AlertInstances = Record; +import { + AlertTaskState, + AlertInstances, + taskInstanceToAlertTaskInstance, +} from './alert_task_instance'; const FALLBACK_RETRY_INTERVAL: IntervalSchedule = { interval: '5m' }; interface AlertTaskRunResult { - state: State; + state: AlertTaskState; runAt: Date; } +interface AlertTaskInstance extends ConcreteTaskInstance { + state: AlertTaskState; +} + export class TaskRunner { private context: TaskRunnerContext; private logger: Logger; - private taskInstance: ConcreteTaskInstance; + private taskInstance: AlertTaskInstance; private alertType: AlertType; constructor( @@ -39,7 +46,7 @@ export class TaskRunner { this.context = context; this.logger = context.logger; this.alertType = alertType; - this.taskInstance = taskInstance; + this.taskInstance = taskInstanceToAlertTaskInstance(taskInstance); } async getApiKeyForAlertPermissions(alertId: string, spaceId: string) { @@ -128,7 +135,7 @@ export class TaskRunner { alertInfoParams: AlertInfoParams, executionHandler: ReturnType, spaceId: string - ): Promise { + ): Promise { const { params, throttle, @@ -145,9 +152,9 @@ export class TaskRunner { } = this.taskInstance; const namespace = this.context.spaceIdToNamespace(spaceId); - const alertInstances = mapValues( + const alertInstances = mapValues( alertRawInstances, - alert => new AlertInstance(alert) + rawAlertInstance => new AlertInstance(rawAlertInstance) ); const updatedAlertTypeState = await this.alertType.executor({ @@ -159,7 +166,7 @@ export class TaskRunner { params, state: alertTypeState, startedAt: this.taskInstance.startedAt!, - previousStartedAt: previousStartedAt && new Date(previousStartedAt), + previousStartedAt: previousStartedAt ? new Date(previousStartedAt) : null, spaceId, namespace, name, @@ -171,7 +178,7 @@ export class TaskRunner { // Cleanup alert instances that are no longer scheduling actions to avoid over populating the alertInstances object const instancesWithScheduledActions = pick( alertInstances, - alertInstance => alertInstance.hasScheduledActions() + (alertInstance: AlertInstance) => alertInstance.hasScheduledActions() ); if (!muteAll) { @@ -192,8 +199,11 @@ export class TaskRunner { } return { - alertTypeState: updatedAlertTypeState, - alertInstances: instancesWithScheduledActions, + alertTypeState: updatedAlertTypeState || undefined, + alertInstances: mapValues( + instancesWithScheduledActions, + alertInstance => alertInstance.toRaw() + ), }; } @@ -239,7 +249,7 @@ export class TaskRunner { ); return { - state: await promiseResult( + state: await promiseResult( this.validateAndExecuteAlert(services, apiKey, attributes, references) ), runAt: asOk( @@ -264,9 +274,9 @@ export class TaskRunner { const { state, runAt } = await errorAsAlertTaskRunResult(this.loadAlertAttributesAndRun()); return { - state: map( + state: map( state, - (stateUpdates: State) => { + (stateUpdates: AlertTaskState) => { return { ...stateUpdates, previousStartedAt, diff --git a/x-pack/legacy/plugins/alerting/server/types.ts b/x-pack/legacy/plugins/alerting/server/types.ts index 9c4a64ff02105c..5aef3b1337a884 100644 --- a/x-pack/legacy/plugins/alerting/server/types.ts +++ b/x-pack/legacy/plugins/alerting/server/types.ts @@ -31,7 +31,7 @@ export interface AlertServices extends Services { export interface AlertExecutorOptions { alertId: string; startedAt: Date; - previousStartedAt?: Date; + previousStartedAt: Date | null; services: AlertServices; params: Record; state: State; diff --git a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts index 2fec949f5692e8..ec00ece9e6ee26 100644 --- a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts +++ b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts @@ -63,6 +63,7 @@ const alertExecutorOptions: LicenseExpirationAlertExecutorOptions = { spaceId: '', name: '', tags: [], + previousStartedAt: null, createdBy: null, updatedBy: null, }; diff --git a/x-pack/legacy/plugins/task_manager/server/legacy.ts b/x-pack/legacy/plugins/task_manager/server/legacy.ts index f5e81bfd90169b..cd2047b757e61e 100644 --- a/x-pack/legacy/plugins/task_manager/server/legacy.ts +++ b/x-pack/legacy/plugins/task_manager/server/legacy.ts @@ -47,6 +47,7 @@ export function createLegacyApi(legacyTaskManager: Promise): Legacy legacyTaskManager.then((tm: TaskManager) => tm.registerTaskDefinitions(taskDefinitions)); }, fetch: (opts: SearchOpts) => legacyTaskManager.then((tm: TaskManager) => tm.fetch(opts)), + get: (id: string) => legacyTaskManager.then((tm: TaskManager) => tm.get(id)), remove: (id: string) => legacyTaskManager.then((tm: TaskManager) => tm.remove(id)), schedule: (taskInstance: TaskInstanceWithDeprecatedFields, options?: any) => legacyTaskManager.then((tm: TaskManager) => tm.schedule(taskInstance, options)), diff --git a/x-pack/plugins/task_manager/server/README.md b/x-pack/plugins/task_manager/server/README.md index a067358dc8841e..a4154f3ecf2124 100644 --- a/x-pack/plugins/task_manager/server/README.md +++ b/x-pack/plugins/task_manager/server/README.md @@ -261,6 +261,9 @@ The _Start_ Plugin api allow you to use Task Manager to facilitate your Plugin's remove: (id: string) => { // ... }, + get: (id: string) => { + // ... + }, schedule: (taskInstance: TaskInstanceWithDeprecatedFields, options?: any) => { // ... }, diff --git a/x-pack/plugins/task_manager/server/create_task_manager.test.ts b/x-pack/plugins/task_manager/server/create_task_manager.test.ts index 34258e15f45d1f..133cfcac4c046e 100644 --- a/x-pack/plugins/task_manager/server/create_task_manager.test.ts +++ b/x-pack/plugins/task_manager/server/create_task_manager.test.ts @@ -42,20 +42,21 @@ describe('createTaskManager', () => { const mockLegacyDeps = getMockLegacyDeps(); const setupResult = createTaskManager(mockCoreSetup, mockLegacyDeps); expect(setupResult).toMatchInlineSnapshot(` - TaskManager { - "addMiddleware": [MockFunction], - "assertUninitialized": [MockFunction], - "attemptToRun": [MockFunction], - "ensureScheduled": [MockFunction], - "fetch": [MockFunction], - "registerTaskDefinitions": [MockFunction], - "remove": [MockFunction], - "runNow": [MockFunction], - "schedule": [MockFunction], - "start": [MockFunction], - "stop": [MockFunction], - "waitUntilStarted": [MockFunction], - } - `); + TaskManager { + "addMiddleware": [MockFunction], + "assertUninitialized": [MockFunction], + "attemptToRun": [MockFunction], + "ensureScheduled": [MockFunction], + "fetch": [MockFunction], + "get": [MockFunction], + "registerTaskDefinitions": [MockFunction], + "remove": [MockFunction], + "runNow": [MockFunction], + "schedule": [MockFunction], + "start": [MockFunction], + "stop": [MockFunction], + "waitUntilStarted": [MockFunction], + } + `); }); }); diff --git a/x-pack/plugins/task_manager/server/mocks.ts b/x-pack/plugins/task_manager/server/mocks.ts index 00b27bd55e7ddb..8ec05dd1bd401d 100644 --- a/x-pack/plugins/task_manager/server/mocks.ts +++ b/x-pack/plugins/task_manager/server/mocks.ts @@ -18,6 +18,7 @@ const createSetupMock = () => { const createStartMock = () => { const mock: jest.Mocked = { fetch: jest.fn(), + get: jest.fn(), remove: jest.fn(), schedule: jest.fn(), runNow: jest.fn(), diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 5e59be65c729dc..fdfe0c068afcf1 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -21,7 +21,7 @@ export type TaskManagerSetupContract = { export type TaskManagerStartContract = Pick< TaskManager, - 'fetch' | 'remove' | 'schedule' | 'runNow' | 'ensureScheduled' + 'fetch' | 'get' | 'remove' | 'schedule' | 'runNow' | 'ensureScheduled' >; export class TaskManagerPlugin @@ -69,6 +69,7 @@ export class TaskManagerPlugin public start(): TaskManagerStartContract { return { fetch: (...args) => this.taskManager.then(tm => tm.fetch(...args)), + get: (...args) => this.taskManager.then(tm => tm.get(...args)), remove: (...args) => this.taskManager.then(tm => tm.remove(...args)), schedule: (...args) => this.taskManager.then(tm => tm.schedule(...args)), runNow: (...args) => this.taskManager.then(tm => tm.runNow(...args)), diff --git a/x-pack/plugins/task_manager/server/task_manager.mock.ts b/x-pack/plugins/task_manager/server/task_manager.mock.ts index 89d1210b006715..1be1a81cdeb688 100644 --- a/x-pack/plugins/task_manager/server/task_manager.mock.ts +++ b/x-pack/plugins/task_manager/server/task_manager.mock.ts @@ -21,6 +21,7 @@ export const taskManagerMock = { ensureScheduled: jest.fn(), schedule: jest.fn(), fetch: jest.fn(), + get: jest.fn(), runNow: jest.fn(), remove: jest.fn(), ...overrides, diff --git a/x-pack/plugins/task_manager/server/task_manager.ts b/x-pack/plugins/task_manager/server/task_manager.ts index da9640fa3e0718..641826de615b1b 100644 --- a/x-pack/plugins/task_manager/server/task_manager.ts +++ b/x-pack/plugins/task_manager/server/task_manager.ts @@ -333,6 +333,17 @@ export class TaskManager { return this.store.fetch(opts); } + /** + * Get the current state of a specified task. + * + * @param {string} id + * @returns {Promise} + */ + public async get(id: string): Promise { + await this.waitUntilStarted(); + return this.store.get(id); + } + /** * Removes the specified task from the index. * diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts index 6c2a22f2737fe9..f7f3d0fa91fff2 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { times } from 'lodash'; import { schema } from '@kbn/config-schema'; import { AlertExecutorOptions, AlertType } from '../../../../../../legacy/plugins/alerting'; import { ActionTypeExecutorOptions, ActionType } from '../../../../../../plugins/actions/server'; @@ -249,6 +249,29 @@ export default function(kibana: any) { }; }, }; + // Alert types + const cumulativeFiringAlertType: AlertType = { + id: 'test.cumulative-firing', + name: 'Test: Cumulative Firing', + actionGroups: ['default', 'other'], + async executor(alertExecutorOptions: AlertExecutorOptions) { + const { services, state } = alertExecutorOptions; + const group = 'default'; + + const runCount = (state.runCount || 0) + 1; + + times(runCount, index => { + services + .alertInstanceFactory(`instance-${index}`) + .replaceState({ instanceStateValue: true }) + .scheduleActions(group); + }); + + return { + runCount, + }; + }, + }; const neverFiringAlertType: AlertType = { id: 'test.never-firing', name: 'Test: Never firing', @@ -364,6 +387,7 @@ export default function(kibana: any) { async executor({ services, params, state }: AlertExecutorOptions) {}, }; server.plugins.alerting.setup.registerType(alwaysFiringAlertType); + server.plugins.alerting.setup.registerType(cumulativeFiringAlertType); server.plugins.alerting.setup.registerType(neverFiringAlertType); server.plugins.alerting.setup.registerType(failingAlertType); server.plugins.alerting.setup.registerType(validationAlertType); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts new file mode 100644 index 00000000000000..d95f9ea8ac0ea1 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts @@ -0,0 +1,126 @@ +/* + * 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 { getUrlPrefix, ObjectRemover, getTestAlertData } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function createGetAlertStateTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('getAlertState', () => { + const objectRemover = new ObjectRemover(supertest); + + afterEach(() => objectRemover.removeAll()); + + for (const scenario of UserAtSpaceScenarios) { + const { user, space } = scenario; + describe(scenario.id, () => { + it('should handle getAlertState alert request appropriately', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert'); + + const response = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}/state`) + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'global_read at space1': + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(200); + expect(response.body).to.key('alertInstances', 'previousStartedAt'); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it(`shouldn't getAlertState for an alert from another space`, async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert'); + + const response = await supertestWithoutAuth + .get(`${getUrlPrefix('other')}/api/alert/${createdAlert.id}/state`) + .auth(user.username, user.password); + + expect(response.statusCode).to.eql(404); + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'space_1_all at space1': + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'global_read at space1': + case 'superuser at space1': + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: `Saved object [alert/${createdAlert.id}] not found`, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it(`should handle getAlertState request appropriately when alert doesn't exist`, async () => { + const response = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alert/1/state`) + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'global_read at space1': + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Saved object [alert/1] not found', + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + }); + } + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts index 1aa084356cfa49..91b0ca0a37c92d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts @@ -15,6 +15,7 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./enable')); loadTestFile(require.resolve('./find')); loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./get_alert_state')); loadTestFile(require.resolve('./list_alert_types')); loadTestFile(require.resolve('./mute_all')); loadTestFile(require.resolve('./mute_instance')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts new file mode 100644 index 00000000000000..053df3b7199cc5 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts @@ -0,0 +1,89 @@ +/* + * 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 { Spaces } from '../../scenarios'; +import { getUrlPrefix, ObjectRemover, getTestAlertData } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createGetAlertStateTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const retry = getService('retry'); + + describe('getAlertState', () => { + const objectRemover = new ObjectRemover(supertest); + + afterEach(() => objectRemover.removeAll()); + + it('should handle getAlertState request appropriately', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); + + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}/state` + ); + + expect(response.statusCode).to.eql(200); + expect(response.body).to.key('alertInstances', 'previousStartedAt'); + }); + + it('should fetch updated state', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send({ + enabled: true, + name: 'abc', + tags: ['foo'], + alertTypeId: 'test.cumulative-firing', + consumer: 'bar', + schedule: { interval: '5s' }, + throttle: '5s', + actions: [], + params: {}, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); + + // wait for alert to actually execute + await retry.try(async () => { + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}/state` + ); + + expect(response.statusCode).to.eql(200); + expect(response.body).to.key('alertInstances', 'alertTypeState', 'previousStartedAt'); + expect(response.body.alertTypeState.runCount).to.greaterThan(1); + }); + + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}/state` + ); + + expect(response.body.alertTypeState.runCount).to.greaterThan(0); + + const alertInstances = Object.entries>(response.body.alertInstances); + expect(alertInstances.length).to.eql(response.body.alertTypeState.runCount); + alertInstances.forEach(([key, value], index) => { + expect(key).to.eql(`instance-${index}`); + expect(value.state).to.eql({ instanceStateValue: true }); + }); + }); + + it(`should handle getAlertState request appropriately when alert doesn't exist`, async () => { + await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/1/state`).expect(404, { + statusCode: 404, + error: 'Not Found', + message: 'Saved object [alert/1] not found', + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index 569c0d538d473f..0b7f51ac9a79b7 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -15,6 +15,7 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./enable')); loadTestFile(require.resolve('./find')); loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./get_alert_state')); loadTestFile(require.resolve('./list_alert_types')); loadTestFile(require.resolve('./mute_all')); loadTestFile(require.resolve('./mute_instance')); diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js index 9e818f050c9296..785fbed3414237 100644 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js +++ b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js @@ -24,6 +24,14 @@ const taskManagerQuery = { }; export function initRoutes(server, taskManager, legacyTaskManager, taskTestingEvents) { + const callCluster = server.plugins.elasticsearch.getCluster('admin').callWithInternalUser; + + async function ensureIndexIsRefreshed() { + return await callCluster('indices.refresh', { + index: '.kibana_task_manager', + }); + } + server.route({ path: '/api/sample_tasks/schedule', method: 'POST', @@ -198,19 +206,8 @@ export function initRoutes(server, taskManager, legacyTaskManager, taskTestingEv method: 'GET', async handler(request) { try { - return taskManager.fetch({ - query: { - bool: { - must: [ - { - ids: { - values: [`task:${request.params.taskId}`], - }, - }, - ], - }, - }, - }); + await ensureIndexIsRefreshed(); + return await taskManager.get(request.params.taskId); } catch (err) { return err; } diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js index 7ec0e9b5efa5b8..e8f976d5ae6e30 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js @@ -69,7 +69,7 @@ export default function({ getService }) { .get(`/api/sample_tasks/task/${task}`) .send({ task }) .expect(200) - .then(response => response.body.docs[0]); + .then(response => response.body); } function historyDocs(taskId) { @@ -434,9 +434,7 @@ export default function({ getService }) { expect(successfulRunNowResult).to.eql({ id: originalTask.id }); await retry.try(async () => { - const [task] = (await currentTasks()).docs.filter( - taskDoc => taskDoc.id === originalTask.id - ); + const task = await currentTask(originalTask.id); expect(task.state.count).to.eql(2); }); From 7f942e59308df78767f6057fb159f56fcd0696be Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Mon, 10 Feb 2020 11:54:56 +0300 Subject: [PATCH 08/19] Remove the feature catalogue registry (#56755) * Remove FeatureCatalogueRegistryProvider from x-pack: *infra *maps *reporting * Remove FeatureCatalogueRegistryProvider from x-pack: *canvas *grokdebugger *logstash * Remove feature_catalogue registry * Fix featureCatalogue register * Fix getting all of the registered features * Remove unused timelion feature register * Move feature registering into np * Rename translations Co-authored-by: Elastic Machine --- .../core_plugins/kibana/public/home/index.ts | 15 --- .../kibana/public/home/kibana_services.ts | 4 +- .../public/home/np_ready/application.tsx | 3 +- .../core_plugins/kibana/public/home/plugin.ts | 6 +- .../kibana/public/management/index.js | 21 ---- .../sections/index_patterns/index.js | 20 ---- .../management/sections/objects/index.js | 21 ---- .../management/sections/settings/index.js | 20 ---- .../core_plugins/management/public/legacy.ts | 2 +- .../management/public/np_ready/mocks.ts | 15 ++- .../management/public/np_ready/plugin.ts | 12 ++- .../index_pattern_management_service.ts | 23 +++- .../saved_objects_management_service.ts | 27 ++++- .../timelion/public/register_feature.ts | 36 ------- .../ui/public/registry/feature_catalogue.d.ts | 42 -------- .../ui/public/registry/feature_catalogue.js | 33 ------ .../public/registry/feature_catalogue.test.js | 101 ------------------ src/plugins/advanced_settings/kibana.json | 2 +- .../advanced_settings/public/plugin.ts | 18 +++- src/plugins/management/kibana.json | 2 +- src/plugins/management/public/plugin.ts | 21 +++- x-pack/legacy/plugins/canvas/i18n/index.ts | 7 -- x-pack/legacy/plugins/canvas/index.js | 2 +- .../canvas/public/feature_catalogue_entry.ts | 20 ++++ x-pack/legacy/plugins/canvas/public/legacy.ts | 4 +- .../public/legacy_register_feature.ts} | 9 +- .../legacy/plugins/canvas/public/plugin.tsx | 9 +- .../plugins/canvas/public/register_feature.js | 24 ----- .../grokdebugger/public/register_feature.js | 35 ------ .../grokdebugger/public/register_feature.ts | 34 ++++++ x-pack/legacy/plugins/infra/index.ts | 2 +- x-pack/legacy/plugins/infra/public/app.ts | 6 +- .../infra/public/feature_catalogue_entry.ts | 41 +++++++ .../infra/public/legacy_register_feature.ts | 15 +++ .../infra/public/new_platform_plugin.ts | 13 ++- .../plugins/infra/public/register_feature.ts | 43 -------- ...me_feature.js => register_home_feature.ts} | 28 ++--- .../common/{constants.js => constants.ts} | 2 +- .../{i18n_getters.js => i18n_getters.ts} | 4 +- x-pack/legacy/plugins/maps/index.js | 2 +- .../maps/public/feature_catalogue_entry.ts | 21 ++++ .../maps/public/legacy_register_feature.ts | 14 +++ x-pack/legacy/plugins/maps/public/plugin.ts | 25 +++-- .../plugins/maps/public/register_feature.js | 27 ----- .../monitoring/public/register_feature.js | 30 ------ .../monitoring/public/register_feature.ts | 30 ++++++ .../reporting/public/register_feature.js | 28 ----- .../reporting/public/register_feature.ts | 27 +++++ .../plugins/uptime/public/register_feature.ts | 14 +-- .../translations/translations/ja-JP.json | 13 ++- .../translations/translations/zh-CN.json | 13 ++- 51 files changed, 412 insertions(+), 574 deletions(-) delete mode 100644 src/legacy/core_plugins/timelion/public/register_feature.ts delete mode 100644 src/legacy/ui/public/registry/feature_catalogue.d.ts delete mode 100644 src/legacy/ui/public/registry/feature_catalogue.js delete mode 100644 src/legacy/ui/public/registry/feature_catalogue.test.js create mode 100644 x-pack/legacy/plugins/canvas/public/feature_catalogue_entry.ts rename x-pack/legacy/plugins/{logstash/public/lib/register_home_feature/index.js => canvas/public/legacy_register_feature.ts} (53%) mode change 100755 => 100644 delete mode 100644 x-pack/legacy/plugins/canvas/public/register_feature.js delete mode 100644 x-pack/legacy/plugins/grokdebugger/public/register_feature.js create mode 100644 x-pack/legacy/plugins/grokdebugger/public/register_feature.ts create mode 100644 x-pack/legacy/plugins/infra/public/feature_catalogue_entry.ts create mode 100644 x-pack/legacy/plugins/infra/public/legacy_register_feature.ts delete mode 100644 x-pack/legacy/plugins/infra/public/register_feature.ts rename x-pack/legacy/plugins/logstash/public/lib/{register_home_feature/register_home_feature.js => register_home_feature.ts} (63%) mode change 100755 => 100644 rename x-pack/legacy/plugins/maps/common/{constants.js => constants.ts} (98%) rename x-pack/legacy/plugins/maps/common/{i18n_getters.js => i18n_getters.ts} (90%) create mode 100644 x-pack/legacy/plugins/maps/public/feature_catalogue_entry.ts create mode 100644 x-pack/legacy/plugins/maps/public/legacy_register_feature.ts delete mode 100644 x-pack/legacy/plugins/maps/public/register_feature.js delete mode 100644 x-pack/legacy/plugins/monitoring/public/register_feature.js create mode 100644 x-pack/legacy/plugins/monitoring/public/register_feature.ts delete mode 100644 x-pack/legacy/plugins/reporting/public/register_feature.js create mode 100644 x-pack/legacy/plugins/reporting/public/register_feature.ts diff --git a/src/legacy/core_plugins/kibana/public/home/index.ts b/src/legacy/core_plugins/kibana/public/home/index.ts index f02ec234e0a83a..c4e58e1a5e1ae7 100644 --- a/src/legacy/core_plugins/kibana/public/home/index.ts +++ b/src/legacy/core_plugins/kibana/public/home/index.ts @@ -17,7 +17,6 @@ * under the License. */ -import { FeatureCatalogueRegistryProvider } from 'ui/registry/feature_catalogue'; import { npSetup, npStart } from 'ui/new_platform'; import chrome from 'ui/chrome'; import { HomePlugin, LegacyAngularInjectedDependencies } from './plugin'; @@ -44,26 +43,12 @@ async function getAngularDependencies(): Promise { const instance = new HomePlugin(); instance.setup(npSetup.core, { ...npSetup.plugins, __LEGACY: { metadata: npStart.core.injectedMetadata.getLegacyMetadata(), - getFeatureCatalogueEntries: async () => { - if (!copiedLegacyCatalogue) { - const injector = await chrome.dangerouslyGetActiveInjector(); - const Private = injector.get('Private'); - // Merge legacy registry with new registry - (Private(FeatureCatalogueRegistryProvider as any) as any).inTitleOrder.map( - npSetup.plugins.home.featureCatalogue.register - ); - copiedLegacyCatalogue = true; - } - return npStart.plugins.home.featureCatalogue.get(); - }, getAngularDependencies, }, }); diff --git a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts index 90fb32a88d43cb..66c4d995e2566a 100644 --- a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts @@ -31,14 +31,13 @@ import { import { UiStatsMetricType } from '@kbn/analytics'; import { Environment, - FeatureCatalogueEntry, HomePublicPluginSetup, + FeatureCatalogueEntry, } from '../../../../../plugins/home/public'; import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public'; export interface HomeKibanaServices { indexPatternService: any; - getFeatureCatalogueEntries: () => Promise; metadata: { app: unknown; bundleId: string; @@ -58,6 +57,7 @@ export interface HomeKibanaServices { uiSettings: IUiSettingsClient; config: KibanaLegacySetup['config']; homeConfig: HomePublicPluginSetup['config']; + directories: readonly FeatureCatalogueEntry[]; http: HttpStart; savedObjectsClient: SavedObjectsClientContract; toastNotifications: NotificationsSetup['toasts']; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/application.tsx b/src/legacy/core_plugins/kibana/public/home/np_ready/application.tsx index 8345491d99972c..2149885f3ee117 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/application.tsx +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/application.tsx @@ -26,8 +26,7 @@ import { getServices } from '../kibana_services'; export const renderApp = async (element: HTMLElement) => { const homeTitle = i18n.translate('kbn.home.breadcrumbs.homeTitle', { defaultMessage: 'Home' }); - const { getFeatureCatalogueEntries, chrome } = getServices(); - const directories = await getFeatureCatalogueEntries(); + const { directories, chrome } = getServices(); chrome.setBreadcrumbs([{ text: homeTitle }]); render(, element); diff --git a/src/legacy/core_plugins/kibana/public/home/plugin.ts b/src/legacy/core_plugins/kibana/public/home/plugin.ts index 5802f33627fb3c..e530906d5698e7 100644 --- a/src/legacy/core_plugins/kibana/public/home/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/home/plugin.ts @@ -25,9 +25,9 @@ import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; import { Environment, - FeatureCatalogueEntry, HomePublicPluginStart, HomePublicPluginSetup, + FeatureCatalogueEntry, } from '../../../../../plugins/home/public'; export interface LegacyAngularInjectedDependencies { @@ -55,7 +55,6 @@ export interface HomePluginSetupDependencies { devMode: boolean; uiSettings: { defaults: UiSettingsState; user?: UiSettingsState | undefined }; }; - getFeatureCatalogueEntries: () => Promise; getAngularDependencies: () => Promise; }; usageCollection: UsageCollectionSetup; @@ -67,6 +66,7 @@ export class HomePlugin implements Plugin { private dataStart: DataPublicPluginStart | null = null; private savedObjectsClient: any = null; private environment: Environment | null = null; + private directories: readonly FeatureCatalogueEntry[] | null = null; setup( core: CoreSetup, @@ -100,6 +100,7 @@ export class HomePlugin implements Plugin { environment: this.environment!, config: kibanaLegacy.config, homeConfig: home.config, + directories: this.directories!, ...angularDependencies, }); const { renderApp } = await import('./np_ready/application'); @@ -110,6 +111,7 @@ export class HomePlugin implements Plugin { start(core: CoreStart, { data, home }: HomePluginStartDependencies) { this.environment = home.environment.get(); + this.directories = home.featureCatalogue.get(); this.dataStart = data; this.savedObjectsClient = core.savedObjects.client; } diff --git a/src/legacy/core_plugins/kibana/public/management/index.js b/src/legacy/core_plugins/kibana/public/management/index.js index 1305310b6f6151..6e5269e11652ff 100644 --- a/src/legacy/core_plugins/kibana/public/management/index.js +++ b/src/legacy/core_plugins/kibana/public/management/index.js @@ -18,7 +18,6 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { render, unmountComponentAtNode } from 'react-dom'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -30,10 +29,6 @@ import appTemplate from './app.html'; import landingTemplate from './landing.html'; import { management, MANAGEMENT_BREADCRUMB } from 'ui/management'; import { ManagementSidebarNav } from '../../../../../plugins/management/public'; -import { - FeatureCatalogueRegistryProvider, - FeatureCatalogueCategory, -} from 'ui/registry/feature_catalogue'; import { timefilter } from 'ui/timefilter'; import { EuiPageContent, @@ -170,19 +165,3 @@ uiModules.get('apps/management').directive('kbnManagementLanding', function(kbnV }, }; }); - -FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'stack-management', - title: i18n.translate('kbn.stackManagement.managementLabel', { - defaultMessage: 'Stack Management', - }), - description: i18n.translate('kbn.stackManagement.managementDescription', { - defaultMessage: 'Your center console for managing the Elastic Stack.', - }), - icon: 'managementApp', - path: '/app/kibana#/management', - showOnHomePage: false, - category: FeatureCatalogueCategory.ADMIN, - }; -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js index 8ab26f8c0d1c83..310797a7f3a0cc 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js @@ -27,10 +27,6 @@ import indexTemplate from './index.html'; import indexPatternListTemplate from './list.html'; import { IndexPatternTable } from './index_pattern_table'; import { npStart } from 'ui/new_platform'; -import { - FeatureCatalogueRegistryProvider, - FeatureCatalogueCategory, -} from 'ui/registry/feature_catalogue'; import { i18n } from '@kbn/i18n'; import { I18nContext } from 'ui/i18n'; import { UICapabilitiesProvider } from 'ui/capabilities/react'; @@ -175,19 +171,3 @@ management.getSection('kibana').register('index_patterns', { order: 0, url: '#/management/kibana/index_patterns/', }); - -FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'index_patterns', - title: i18n.translate('kbn.management.indexPatternHeader', { - defaultMessage: 'Index Patterns', - }), - description: i18n.translate('kbn.management.indexPatternLabel', { - defaultMessage: 'Manage the index patterns that help retrieve your data from Elasticsearch.', - }), - icon: 'indexPatternApp', - path: '/app/kibana#/management/kibana/index_patterns', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - }; -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/index.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/index.js index 7bd57e87bc5c98..3965c42ac088dd 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/index.js @@ -23,10 +23,6 @@ import './_view'; import './_objects'; import 'ace'; import { uiModules } from 'ui/modules'; -import { - FeatureCatalogueRegistryProvider, - FeatureCatalogueCategory, -} from 'ui/registry/feature_catalogue'; // add the module deps to this module uiModules.get('apps/management'); @@ -38,20 +34,3 @@ management.getSection('kibana').register('objects', { order: 10, url: '#/management/kibana/objects', }); - -FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'saved_objects', - title: i18n.translate('kbn.management.objects.savedObjectsTitle', { - defaultMessage: 'Saved Objects', - }), - description: i18n.translate('kbn.management.objects.savedObjectsDescription', { - defaultMessage: - 'Import, export, and manage your saved searches, visualizations, and dashboards.', - }), - icon: 'savedObjectsApp', - path: '/app/kibana#/management/kibana/objects', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - }; -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/index.js b/src/legacy/core_plugins/kibana/public/management/sections/settings/index.js index 6d8987b1a928e5..16d70a9f4ed575 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/index.js @@ -23,10 +23,6 @@ import { uiModules } from 'ui/modules'; import { capabilities } from 'ui/capabilities'; import { I18nContext } from 'ui/i18n'; import indexTemplate from './index.html'; -import { - FeatureCatalogueRegistryProvider, - FeatureCatalogueCategory, -} from 'ui/registry/feature_catalogue'; import React from 'react'; import { AdvancedSettings } from './advanced_settings'; @@ -83,19 +79,3 @@ management.getSection('kibana').register('settings', { order: 20, url: '#/management/kibana/settings', }); - -FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'advanced_settings', - title: i18n.translate('kbn.management.settings.advancedSettingsLabel', { - defaultMessage: 'Advanced Settings', - }), - description: i18n.translate('kbn.management.settings.advancedSettingsDescription', { - defaultMessage: 'Directly edit settings that control behavior in Kibana.', - }), - icon: 'advancedSettingsApp', - path: '/app/kibana#/management/kibana/settings', - showOnHomePage: false, - category: FeatureCatalogueCategory.ADMIN, - }; -}); diff --git a/src/legacy/core_plugins/management/public/legacy.ts b/src/legacy/core_plugins/management/public/legacy.ts index 7c17f0c6bddc01..4481bad79c47d1 100644 --- a/src/legacy/core_plugins/management/public/legacy.ts +++ b/src/legacy/core_plugins/management/public/legacy.ts @@ -41,5 +41,5 @@ import { plugin } from '.'; const pluginInstance = plugin({} as PluginInitializerContext); -export const setup = pluginInstance.setup(npSetup.core, {}); +export const setup = pluginInstance.setup(npSetup.core, { home: npSetup.plugins.home }); export const start = pluginInstance.start(npStart.core, {}); diff --git a/src/legacy/core_plugins/management/public/np_ready/mocks.ts b/src/legacy/core_plugins/management/public/np_ready/mocks.ts index 13a0cf4c891a39..5ed7c045d1f645 100644 --- a/src/legacy/core_plugins/management/public/np_ready/mocks.ts +++ b/src/legacy/core_plugins/management/public/np_ready/mocks.ts @@ -19,7 +19,12 @@ import { PluginInitializerContext } from 'src/core/public'; import { coreMock } from '../../../../../core/public/mocks'; -import { ManagementSetup, ManagementStart, ManagementPlugin } from './plugin'; +import { + ManagementSetup, + ManagementStart, + ManagementPlugin, + ManagementPluginSetupDependencies, +} from './plugin'; const createSetupContract = (): ManagementSetup => ({ indexPattern: { @@ -49,7 +54,13 @@ const createStartContract = (): ManagementStart => ({}); const createInstance = async () => { const plugin = new ManagementPlugin({} as PluginInitializerContext); - const setup = plugin.setup(coreMock.createSetup(), {}); + const setup = plugin.setup(coreMock.createSetup(), ({ + home: { + featureCatalogue: { + register: jest.fn(), + }, + }, + } as unknown) as ManagementPluginSetupDependencies); const doStart = () => plugin.start(coreMock.createStart(), {}); return { diff --git a/src/legacy/core_plugins/management/public/np_ready/plugin.ts b/src/legacy/core_plugins/management/public/np_ready/plugin.ts index 032a46439ba55a..7dd2b23d40610f 100644 --- a/src/legacy/core_plugins/management/public/np_ready/plugin.ts +++ b/src/legacy/core_plugins/management/public/np_ready/plugin.ts @@ -17,14 +17,16 @@ * under the License. */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { HomePublicPluginSetup } from 'src/plugins/home/public'; import { IndexPatternManagementService, IndexPatternManagementSetup } from './services'; import { SavedObjectsManagementService, SavedObjectsManagementServiceSetup, } from './services/saved_objects_management'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface ManagementPluginSetupDependencies {} +export interface ManagementPluginSetupDependencies { + home: HomePublicPluginSetup; +} // eslint-disable-next-line @typescript-eslint/no-empty-interface interface ManagementPluginStartDependencies {} @@ -50,10 +52,10 @@ export class ManagementPlugin constructor(initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup, deps: ManagementPluginSetupDependencies) { + public setup(core: CoreSetup, { home }: ManagementPluginSetupDependencies) { return { - indexPattern: this.indexPattern.setup({ httpClient: core.http }), - savedObjects: this.savedObjects.setup(), + indexPattern: this.indexPattern.setup({ httpClient: core.http, home }), + savedObjects: this.savedObjects.setup({ home }), }; } diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/index_pattern_management_service.ts b/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/index_pattern_management_service.ts index b421024b60f4be..2b6f008dd928a1 100644 --- a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/index_pattern_management_service.ts +++ b/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/index_pattern_management_service.ts @@ -17,12 +17,18 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; +import { + FeatureCatalogueCategory, + HomePublicPluginSetup, +} from '../../../../../../../plugins/home/public'; import { HttpSetup } from '../../../../../../../core/public'; import { IndexPatternCreationManager, IndexPatternCreationConfig } from './creation'; import { IndexPatternListManager, IndexPatternListConfig } from './list'; interface SetupDependencies { httpClient: HttpSetup; + home: HomePublicPluginSetup; } /** @@ -31,13 +37,28 @@ interface SetupDependencies { * @internal */ export class IndexPatternManagementService { - public setup({ httpClient }: SetupDependencies) { + public setup({ httpClient, home }: SetupDependencies) { const creation = new IndexPatternCreationManager(httpClient); const list = new IndexPatternListManager(); creation.add(IndexPatternCreationConfig); list.add(IndexPatternListConfig); + home.featureCatalogue.register({ + id: 'index_patterns', + title: i18n.translate('management.indexPatternHeader', { + defaultMessage: 'Index Patterns', + }), + description: i18n.translate('management.indexPatternLabel', { + defaultMessage: + 'Manage the index patterns that help retrieve your data from Elasticsearch.', + }), + icon: 'indexPatternApp', + path: '/app/kibana#/management/kibana/index_patterns', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }); + return { creation, list, diff --git a/src/legacy/core_plugins/management/public/np_ready/services/saved_objects_management/saved_objects_management_service.ts b/src/legacy/core_plugins/management/public/np_ready/services/saved_objects_management/saved_objects_management_service.ts index d5e90d12cccc91..be102b2a4dce78 100644 --- a/src/legacy/core_plugins/management/public/np_ready/services/saved_objects_management/saved_objects_management_service.ts +++ b/src/legacy/core_plugins/management/public/np_ready/services/saved_objects_management/saved_objects_management_service.ts @@ -16,10 +16,35 @@ * specific language governing permissions and limitations * under the License. */ + +import { i18n } from '@kbn/i18n'; +import { + FeatureCatalogueCategory, + HomePublicPluginSetup, +} from '../../../../../../../plugins/home/public'; import { SavedObjectsManagementActionRegistry } from './saved_objects_management_action_registry'; +interface SetupDependencies { + home: HomePublicPluginSetup; +} + export class SavedObjectsManagementService { - public setup() { + public setup({ home }: SetupDependencies) { + home.featureCatalogue.register({ + id: 'saved_objects', + title: i18n.translate('management.objects.savedObjectsTitle', { + defaultMessage: 'Saved Objects', + }), + description: i18n.translate('management.objects.savedObjectsDescription', { + defaultMessage: + 'Import, export, and manage your saved searches, visualizations, and dashboards.', + }), + icon: 'savedObjectsApp', + path: '/app/kibana#/management/kibana/objects', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }); + return { registry: SavedObjectsManagementActionRegistry, }; diff --git a/src/legacy/core_plugins/timelion/public/register_feature.ts b/src/legacy/core_plugins/timelion/public/register_feature.ts deleted file mode 100644 index 7dd44b58bd1d7a..00000000000000 --- a/src/legacy/core_plugins/timelion/public/register_feature.ts +++ /dev/null @@ -1,36 +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 { FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; -import { i18n } from '@kbn/i18n'; - -export const registerFeature = () => { - return { - id: 'timelion', - title: 'Timelion', - description: i18n.translate('timelion.registerFeatureDescription', { - defaultMessage: - 'Use an expression language to analyze time series data and visualize the results.', - }), - icon: 'timelionApp', - path: '/app/timelion', - showOnHomePage: false, - category: FeatureCatalogueCategory.DATA, - }; -}; diff --git a/src/legacy/ui/public/registry/feature_catalogue.d.ts b/src/legacy/ui/public/registry/feature_catalogue.d.ts deleted file mode 100644 index 031c3efa6c5add..00000000000000 --- a/src/legacy/ui/public/registry/feature_catalogue.d.ts +++ /dev/null @@ -1,42 +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 { I18nServiceType } from '@kbn/i18n/angular'; - -export enum FeatureCatalogueCategory { - ADMIN = 'admin', - DATA = 'data', - OTHER = 'other', -} - -interface FeatureCatalogueObject { - id: string; - title: string; - description: string; - icon: string; - path: string; - showOnHomePage: boolean; - category: FeatureCatalogueCategory; -} - -type FeatureCatalogueRegistryFunction = (i18n: I18nServiceType) => FeatureCatalogueObject; - -export const FeatureCatalogueRegistryProvider: { - register: (fn: FeatureCatalogueRegistryFunction) => void; -}; diff --git a/src/legacy/ui/public/registry/feature_catalogue.js b/src/legacy/ui/public/registry/feature_catalogue.js deleted file mode 100644 index 23aaf2fb0a1d93..00000000000000 --- a/src/legacy/ui/public/registry/feature_catalogue.js +++ /dev/null @@ -1,33 +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 { uiRegistry } from './_registry'; -import { capabilities } from '../capabilities'; -export { FeatureCatalogueCategory } from '../../../../plugins/home/public'; - -export const FeatureCatalogueRegistryProvider = uiRegistry({ - name: 'featureCatalogue', - index: ['id'], - group: ['category'], - order: ['title'], - filter: featureCatalogItem => { - const isDisabledViaCapabilities = capabilities.get().catalogue[featureCatalogItem.id] === false; - return !isDisabledViaCapabilities && Object.keys(featureCatalogItem).length > 0; - }, -}); diff --git a/src/legacy/ui/public/registry/feature_catalogue.test.js b/src/legacy/ui/public/registry/feature_catalogue.test.js deleted file mode 100644 index 15aed781438825..00000000000000 --- a/src/legacy/ui/public/registry/feature_catalogue.test.js +++ /dev/null @@ -1,101 +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. - */ -jest.mock('ui/capabilities', () => ({ - capabilities: { - get: () => ({ - navLinks: {}, - management: {}, - catalogue: { - item1: true, - item2: false, - item3: true, - }, - }), - }, -})); -import { FeatureCatalogueCategory, FeatureCatalogueRegistryProvider } from './feature_catalogue'; - -describe('FeatureCatalogueRegistryProvider', () => { - beforeAll(() => { - FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'item1', - title: 'foo', - description: 'this is foo', - icon: 'savedObjectsApp', - path: '/app/kibana#/management/kibana/objects', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - }; - }); - - FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'item2', - title: 'bar', - description: 'this is bar', - icon: 'savedObjectsApp', - path: '/app/kibana#/management/kibana/objects', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - }; - }); - - // intentionally not listed in uiCapabilities.catalogue above - FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'item4', - title: 'secret', - description: 'this is a secret', - icon: 'savedObjectsApp', - path: '/app/kibana#/management/kibana/objects', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - }; - }); - }); - - it('should not return items hidden by uiCapabilities', () => { - const mockPrivate = entityFn => entityFn(); - const mockInjector = () => null; - - // eslint-disable-next-line new-cap - const foo = FeatureCatalogueRegistryProvider(mockPrivate, mockInjector).inTitleOrder; - expect(foo).toEqual([ - { - id: 'item1', - title: 'foo', - description: 'this is foo', - icon: 'savedObjectsApp', - path: '/app/kibana#/management/kibana/objects', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - }, - { - id: 'item4', - title: 'secret', - description: 'this is a secret', - icon: 'savedObjectsApp', - path: '/app/kibana#/management/kibana/objects', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - }, - ]); - }); -}); diff --git a/src/plugins/advanced_settings/kibana.json b/src/plugins/advanced_settings/kibana.json index 5fc1e916ae45f8..bafb2caba32be4 100644 --- a/src/plugins/advanced_settings/kibana.json +++ b/src/plugins/advanced_settings/kibana.json @@ -3,5 +3,5 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": [] + "requiredPlugins": ["home"] } diff --git a/src/plugins/advanced_settings/public/plugin.ts b/src/plugins/advanced_settings/public/plugin.ts index 692e515ca4e5ed..bffd5a51576151 100644 --- a/src/plugins/advanced_settings/public/plugin.ts +++ b/src/plugins/advanced_settings/public/plugin.ts @@ -17,15 +17,31 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { ComponentRegistry } from './component_registry'; import { AdvancedSettingsSetup, AdvancedSettingsStart } from './types'; +import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/public'; const component = new ComponentRegistry(); export class AdvancedSettingsPlugin implements Plugin { - public setup(core: CoreSetup) { + public setup(core: CoreSetup, { home }: { home: HomePublicPluginSetup }) { + home.featureCatalogue.register({ + id: 'advanced_settings', + title: i18n.translate('advancedSettings.advancedSettingsLabel', { + defaultMessage: 'Advanced Settings', + }), + description: i18n.translate('advancedSettings.advancedSettingsDescription', { + defaultMessage: 'Directly edit settings that control behavior in Kibana.', + }), + icon: 'advancedSettingsApp', + path: '/app/kibana#/management/kibana/settings', + showOnHomePage: false, + category: FeatureCatalogueCategory.ADMIN, + }); + return { component: component.setup, }; diff --git a/src/plugins/management/kibana.json b/src/plugins/management/kibana.json index 4bbf2039c8f38b..1789b7cd5ddba1 100644 --- a/src/plugins/management/kibana.json +++ b/src/plugins/management/kibana.json @@ -3,5 +3,5 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["kibanaLegacy"] + "requiredPlugins": ["kibanaLegacy", "home"] } diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index ce6959ec31345b..df2398412dac25 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -17,10 +17,12 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { ManagementSetup, ManagementStart } from './types'; import { ManagementService } from './management_service'; import { KibanaLegacySetup } from '../../kibana_legacy/public'; +import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/public'; // @ts-ignore import { LegacyManagementAdapter } from './legacy'; @@ -28,7 +30,24 @@ export class ManagementPlugin implements Plugin - i18n.translate('xpack.canvas.appDescription', { - defaultMessage: 'Showcase your data in a pixel-perfect way.', - }); diff --git a/x-pack/legacy/plugins/canvas/index.js b/x-pack/legacy/plugins/canvas/index.js index ebd4f35db8175c..b357ec9c0b61e6 100644 --- a/x-pack/legacy/plugins/canvas/index.js +++ b/x-pack/legacy/plugins/canvas/index.js @@ -36,7 +36,7 @@ export function canvas(kibana) { // window.onerror override 'plugins/canvas/lib/window_error_handler.js', ], - home: ['plugins/canvas/register_feature'], + home: ['plugins/canvas/legacy_register_feature'], mappings, migrations, savedObjectsManagement: { diff --git a/x-pack/legacy/plugins/canvas/public/feature_catalogue_entry.ts b/x-pack/legacy/plugins/canvas/public/feature_catalogue_entry.ts new file mode 100644 index 00000000000000..f610bd0299832a --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/feature_catalogue_entry.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; + +export const featureCatalogueEntry = { + id: 'canvas', + title: 'Canvas', + description: i18n.translate('xpack.canvas.appDescription', { + defaultMessage: 'Showcase your data in a pixel-perfect way.', + }), + icon: 'canvasApp', + path: '/app/canvas', + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, +}; diff --git a/x-pack/legacy/plugins/canvas/public/legacy.ts b/x-pack/legacy/plugins/canvas/public/legacy.ts index cbd2aa54627eed..c16bc124747c6e 100644 --- a/x-pack/legacy/plugins/canvas/public/legacy.ts +++ b/x-pack/legacy/plugins/canvas/public/legacy.ts @@ -22,7 +22,9 @@ const shimCoreSetup = { const shimCoreStart = { ...npStart.core, }; -const shimSetupPlugins = {}; +const shimSetupPlugins = { + home: npSetup.plugins.home, +}; const shimStartPlugins: CanvasStartDeps = { ...npStart.plugins, diff --git a/x-pack/legacy/plugins/logstash/public/lib/register_home_feature/index.js b/x-pack/legacy/plugins/canvas/public/legacy_register_feature.ts old mode 100755 new mode 100644 similarity index 53% rename from x-pack/legacy/plugins/logstash/public/lib/register_home_feature/index.js rename to x-pack/legacy/plugins/canvas/public/legacy_register_feature.ts index 72e3f201bd4ca5..00f788f267d4b1 --- a/x-pack/legacy/plugins/logstash/public/lib/register_home_feature/index.js +++ b/x-pack/legacy/plugins/canvas/public/legacy_register_feature.ts @@ -4,4 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import './register_home_feature'; +import { npSetup } from 'ui/new_platform'; +import { featureCatalogueEntry } from './feature_catalogue_entry'; + +const { + plugins: { home }, +} = npSetup; + +home.featureCatalogue.register(featureCatalogueEntry); diff --git a/x-pack/legacy/plugins/canvas/public/plugin.tsx b/x-pack/legacy/plugins/canvas/public/plugin.tsx index 7928d46067908b..a24fd758808bae 100644 --- a/x-pack/legacy/plugins/canvas/public/plugin.tsx +++ b/x-pack/legacy/plugins/canvas/public/plugin.tsx @@ -10,6 +10,7 @@ import { Chrome } from 'ui/chrome'; import { i18n } from '@kbn/i18n'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { CoreSetup, CoreStart, Plugin } from '../../../../../src/core/public'; +import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; // @ts-ignore: Untyped Local import { CapabilitiesStrings } from '../i18n'; const { ReadOnlyBadge: strings } = CapabilitiesStrings; @@ -27,6 +28,7 @@ import { getDocumentationLinks } from './lib/documentation_links'; // @ts-ignore: untyped local import { initClipboard } from './lib/clipboard'; +import { featureCatalogueEntry } from './feature_catalogue_entry'; export { CoreStart }; /** @@ -34,7 +36,9 @@ export { CoreStart }; * @internal */ // This interface will be built out as we require other plugins for setup -export interface CanvasSetupDeps {} // eslint-disable-line @typescript-eslint/no-empty-interface +export interface CanvasSetupDeps { + home: HomePublicPluginSetup; +} export interface CanvasStartDeps { __LEGACY: { absoluteToParsedUrl: (url: string, basePath: string) => any; @@ -79,6 +83,9 @@ export class CanvasPlugin return renderApp(coreStart, depsStart, params, canvasStore); }, }); + + plugins.home.featureCatalogue.register(featureCatalogueEntry); + return {}; } diff --git a/x-pack/legacy/plugins/canvas/public/register_feature.js b/x-pack/legacy/plugins/canvas/public/register_feature.js deleted file mode 100644 index 8d78498de34b2f..00000000000000 --- a/x-pack/legacy/plugins/canvas/public/register_feature.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - FeatureCatalogueRegistryProvider, - FeatureCatalogueCategory, -} from 'ui/registry/feature_catalogue'; - -import { getAppDescription } from '../i18n'; - -FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'canvas', - title: 'Canvas', - description: getAppDescription(), - icon: 'canvasApp', - path: '/app/canvas', - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA, - }; -}); diff --git a/x-pack/legacy/plugins/grokdebugger/public/register_feature.js b/x-pack/legacy/plugins/grokdebugger/public/register_feature.js deleted file mode 100644 index 18021ed0f752da..00000000000000 --- a/x-pack/legacy/plugins/grokdebugger/public/register_feature.js +++ /dev/null @@ -1,35 +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 { - FeatureCatalogueRegistryProvider, - FeatureCatalogueCategory, -} from 'ui/registry/feature_catalogue'; - -import { i18n } from '@kbn/i18n'; - -FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'grokdebugger', - title: i18n.translate('xpack.grokDebugger.registryProviderTitle', { - defaultMessage: '{grokLogParsingTool} Debugger', - values: { - grokLogParsingTool: 'Grok', - }, - }), - description: i18n.translate('xpack.grokDebugger.registryProviderDescription', { - defaultMessage: - 'Simulate and debug {grokLogParsingTool} patterns for data transformation on ingestion.', - values: { - grokLogParsingTool: 'grok', - }, - }), - icon: 'grokApp', - path: '/app/kibana#/dev_tools/grokdebugger', - showOnHomePage: false, - category: FeatureCatalogueCategory.ADMIN, - }; -}); diff --git a/x-pack/legacy/plugins/grokdebugger/public/register_feature.ts b/x-pack/legacy/plugins/grokdebugger/public/register_feature.ts new file mode 100644 index 00000000000000..97d2e53ce78362 --- /dev/null +++ b/x-pack/legacy/plugins/grokdebugger/public/register_feature.ts @@ -0,0 +1,34 @@ +/* + * 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 { npSetup } from 'ui/new_platform'; +import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; + +const { + plugins: { home }, +} = npSetup; + +home.featureCatalogue.register({ + id: 'grokdebugger', + title: i18n.translate('xpack.grokDebugger.registryProviderTitle', { + defaultMessage: '{grokLogParsingTool} Debugger', + values: { + grokLogParsingTool: 'Grok', + }, + }), + description: i18n.translate('xpack.grokDebugger.registryProviderDescription', { + defaultMessage: + 'Simulate and debug {grokLogParsingTool} patterns for data transformation on ingestion.', + values: { + grokLogParsingTool: 'grok', + }, + }), + icon: 'grokApp', + path: '/app/kibana#/dev_tools/grokdebugger', + showOnHomePage: false, + category: FeatureCatalogueCategory.ADMIN, +}); diff --git a/x-pack/legacy/plugins/infra/index.ts b/x-pack/legacy/plugins/infra/index.ts index d9abadcb5125c5..4ab2cde0824987 100644 --- a/x-pack/legacy/plugins/infra/index.ts +++ b/x-pack/legacy/plugins/infra/index.ts @@ -42,7 +42,7 @@ export function infra(kibana: any) { url: `/app/${APP_ID}#/infrastructure`, }, styleSheetPaths: resolve(__dirname, 'public/index.scss'), - home: ['plugins/infra/register_feature'], + home: ['plugins/infra/legacy_register_feature'], links: [ { description: i18n.translate('xpack.infra.linkInfrastructureDescription', { diff --git a/x-pack/legacy/plugins/infra/public/app.ts b/x-pack/legacy/plugins/infra/public/app.ts index 4b14e168eb7683..7a13d3a59cc0d9 100644 --- a/x-pack/legacy/plugins/infra/public/app.ts +++ b/x-pack/legacy/plugins/infra/public/app.ts @@ -9,7 +9,7 @@ // actually mount and run our application. Once in the NP this won't be an issue // as the NP will look for an export named "plugin" and run that from the index file. -import { npStart } from 'ui/new_platform'; +import { npStart, npSetup } from 'ui/new_platform'; import { PluginInitializerContext } from 'kibana/public'; import chrome from 'ui/chrome'; // @ts-ignore @@ -50,5 +50,7 @@ const checkForRoot = () => { }; checkForRoot().then(() => { - plugin({} as PluginInitializerContext).start(core, plugins, __LEGACY); + const pluginInstance = plugin({} as PluginInitializerContext); + pluginInstance.setup(npSetup.core, { home: npSetup.plugins.home }); + pluginInstance.start(core, plugins, __LEGACY); }); diff --git a/x-pack/legacy/plugins/infra/public/feature_catalogue_entry.ts b/x-pack/legacy/plugins/infra/public/feature_catalogue_entry.ts new file mode 100644 index 00000000000000..6442083234f2ce --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/feature_catalogue_entry.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; + +const APP_ID = 'infra'; + +export const featureCatalogueEntries = { + metrics: { + id: 'infraops', + title: i18n.translate('xpack.infra.registerFeatures.infraOpsTitle', { + defaultMessage: 'Metrics', + }), + description: i18n.translate('xpack.infra.registerFeatures.infraOpsDescription', { + defaultMessage: + 'Explore infrastructure metrics and logs for common servers, containers, and services.', + }), + icon: 'metricsApp', + path: `/app/${APP_ID}#infrastructure`, + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, + }, + logs: { + id: 'infralogging', + title: i18n.translate('xpack.infra.registerFeatures.logsTitle', { + defaultMessage: 'Logs', + }), + description: i18n.translate('xpack.infra.registerFeatures.logsDescription', { + defaultMessage: + 'Stream logs in real time or scroll through historical views in a console-like experience.', + }), + icon: 'logsApp', + path: `/app/${APP_ID}#logs`, + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, + }, +}; diff --git a/x-pack/legacy/plugins/infra/public/legacy_register_feature.ts b/x-pack/legacy/plugins/infra/public/legacy_register_feature.ts new file mode 100644 index 00000000000000..7b10a1e062f75c --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/legacy_register_feature.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { npSetup } from 'ui/new_platform'; +import { featureCatalogueEntries } from './feature_catalogue_entry'; + +const { + plugins: { home }, +} = npSetup; + +home.featureCatalogue.register(featureCatalogueEntries.metrics); +home.featureCatalogue.register(featureCatalogueEntries.logs); diff --git a/x-pack/legacy/plugins/infra/public/new_platform_plugin.ts b/x-pack/legacy/plugins/infra/public/new_platform_plugin.ts index 78594afcc8ada7..f438b657946534 100644 --- a/x-pack/legacy/plugins/infra/public/new_platform_plugin.ts +++ b/x-pack/legacy/plugins/infra/public/new_platform_plugin.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CoreStart, PluginInitializerContext } from 'kibana/public'; +import { CoreStart, CoreSetup, PluginInitializerContext } from 'kibana/public'; import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; import ApolloClient from 'apollo-client'; import { ApolloLink } from 'apollo-link'; @@ -13,12 +13,23 @@ import { startApp } from './apps/start_app'; import { InfraFrontendLibs } from './lib/lib'; import introspectionQueryResultData from './graphql/introspection.json'; import { InfraKibanaObservableApiAdapter } from './lib/adapters/observable_api/kibana_observable_api'; +import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; +import { featureCatalogueEntries } from './feature_catalogue_entry'; type ClientPlugins = any; type LegacyDeps = any; +interface InfraPluginSetupDependencies { + home: HomePublicPluginSetup; +} export class Plugin { constructor(context: PluginInitializerContext) {} + + setup(core: CoreSetup, { home }: InfraPluginSetupDependencies) { + home.featureCatalogue.register(featureCatalogueEntries.metrics); + home.featureCatalogue.register(featureCatalogueEntries.logs); + } + start(core: CoreStart, plugins: ClientPlugins, __LEGACY: LegacyDeps) { startApp(this.composeLibs(core, plugins, __LEGACY), core, plugins); } diff --git a/x-pack/legacy/plugins/infra/public/register_feature.ts b/x-pack/legacy/plugins/infra/public/register_feature.ts deleted file mode 100644 index bf56db77e360f0..00000000000000 --- a/x-pack/legacy/plugins/infra/public/register_feature.ts +++ /dev/null @@ -1,43 +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 { I18nServiceType } from '@kbn/i18n/angular'; -import { - FeatureCatalogueCategory, - FeatureCatalogueRegistryProvider, -} from 'ui/registry/feature_catalogue'; - -const APP_ID = 'infra'; - -FeatureCatalogueRegistryProvider.register((i18n: I18nServiceType) => ({ - id: 'infraops', - title: i18n('xpack.infra.registerFeatures.infraOpsTitle', { - defaultMessage: 'Metrics', - }), - description: i18n('xpack.infra.registerFeatures.infraOpsDescription', { - defaultMessage: - 'Explore infrastructure metrics and logs for common servers, containers, and services.', - }), - icon: 'metricsApp', - path: `/app/${APP_ID}#infrastructure`, - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA, -})); - -FeatureCatalogueRegistryProvider.register((i18n: I18nServiceType) => ({ - id: 'infralogging', - title: i18n('xpack.infra.registerFeatures.logsTitle', { - defaultMessage: 'Logs', - }), - description: i18n('xpack.infra.registerFeatures.logsDescription', { - defaultMessage: - 'Stream logs in real time or scroll through historical views in a console-like experience.', - }), - icon: 'logsApp', - path: `/app/${APP_ID}#logs`, - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA, -})); diff --git a/x-pack/legacy/plugins/logstash/public/lib/register_home_feature/register_home_feature.js b/x-pack/legacy/plugins/logstash/public/lib/register_home_feature.ts old mode 100755 new mode 100644 similarity index 63% rename from x-pack/legacy/plugins/logstash/public/lib/register_home_feature/register_home_feature.js rename to x-pack/legacy/plugins/logstash/public/lib/register_home_feature.ts index ee26cea54f977f..e943656120d5e7 --- a/x-pack/legacy/plugins/logstash/public/lib/register_home_feature/register_home_feature.js +++ b/x-pack/legacy/plugins/logstash/public/lib/register_home_feature.ts @@ -4,20 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - FeatureCatalogueRegistryProvider, - FeatureCatalogueCategory, -} from 'ui/registry/feature_catalogue'; - import { i18n } from '@kbn/i18n'; +import { npSetup } from 'ui/new_platform'; +// @ts-ignore +import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; +import { FeatureCatalogueCategory } from '../../../../../../src/plugins/home/public'; +// @ts-ignore +import { PLUGIN } from '../../common/constants'; + +const { + plugins: { home }, +} = npSetup; -FeatureCatalogueRegistryProvider.register($injector => { - const licenseService = $injector.get('logstashLicenseService'); - if (!licenseService.enableLinks) { - return; - } +const enableLinks = Boolean(xpackInfo.get(`features.${PLUGIN.ID}.enableLinks`)); - return { +if (enableLinks) { + home.featureCatalogue.register({ id: 'management_logstash', title: i18n.translate('xpack.logstash.homeFeature.logstashPipelinesTitle', { defaultMessage: 'Logstash Pipelines', @@ -29,5 +31,5 @@ FeatureCatalogueRegistryProvider.register($injector => { path: '/app/kibana#/management/logstash/pipelines', showOnHomePage: true, category: FeatureCatalogueCategory.ADMIN, - }; -}); + }); +} diff --git a/x-pack/legacy/plugins/maps/common/constants.js b/x-pack/legacy/plugins/maps/common/constants.ts similarity index 98% rename from x-pack/legacy/plugins/maps/common/constants.js rename to x-pack/legacy/plugins/maps/common/constants.ts index 2570341aa5756e..ab9a696fa3a177 100644 --- a/x-pack/legacy/plugins/maps/common/constants.js +++ b/x-pack/legacy/plugins/maps/common/constants.ts @@ -33,7 +33,7 @@ export const INDEX_SETTINGS_API_PATH = `${GIS_API_PATH}/indexSettings`; export const MAP_BASE_URL = `/${MAP_APP_PATH}#/${MAP_SAVED_OBJECT_TYPE}`; -export function createMapPath(id) { +export function createMapPath(id: string) { return `${MAP_BASE_URL}/${id}`; } diff --git a/x-pack/legacy/plugins/maps/common/i18n_getters.js b/x-pack/legacy/plugins/maps/common/i18n_getters.ts similarity index 90% rename from x-pack/legacy/plugins/maps/common/i18n_getters.js rename to x-pack/legacy/plugins/maps/common/i18n_getters.ts index 578d0cd4780e96..0008a119f1c7cc 100644 --- a/x-pack/legacy/plugins/maps/common/i18n_getters.js +++ b/x-pack/legacy/plugins/maps/common/i18n_getters.ts @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; +import { $Values } from '@kbn/utility-types'; import { ES_SPATIAL_RELATIONS } from './constants'; export function getAppTitle() { @@ -26,7 +27,7 @@ export function getUrlLabel() { }); } -export function getEsSpatialRelationLabel(spatialRelation) { +export function getEsSpatialRelationLabel(spatialRelation: $Values) { switch (spatialRelation) { case ES_SPATIAL_RELATIONS.INTERSECTS: return i18n.translate('xpack.maps.common.esSpatialRelation.intersectsLabel', { @@ -40,6 +41,7 @@ export function getEsSpatialRelationLabel(spatialRelation) { return i18n.translate('xpack.maps.common.esSpatialRelation.withinLabel', { defaultMessage: 'within', }); + // @ts-ignore case ES_SPATIAL_RELATIONS.CONTAINS: return i18n.translate('xpack.maps.common.esSpatialRelation.containsLabel', { defaultMessage: 'contains', diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index 4f679905fc352a..247dc8115c5c36 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -54,7 +54,7 @@ export function maps(kibana) { }, embeddableFactories: ['plugins/maps/embeddable/map_embeddable_factory'], inspectorViews: ['plugins/maps/inspector/views/register_views'], - home: ['plugins/maps/register_feature'], + home: ['plugins/maps/legacy_register_feature'], styleSheetPaths: `${__dirname}/public/index.scss`, savedObjectSchemas: { 'maps-telemetry': { diff --git a/x-pack/legacy/plugins/maps/public/feature_catalogue_entry.ts b/x-pack/legacy/plugins/maps/public/feature_catalogue_entry.ts new file mode 100644 index 00000000000000..fdda76b4e12127 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/feature_catalogue_entry.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import { APP_ID, APP_ICON } from '../common/constants'; +import { getAppTitle } from '../common/i18n_getters'; +import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; + +export const featureCatalogueEntry = { + id: APP_ID, + title: getAppTitle(), + description: i18n.translate('xpack.maps.feature.appDescription', { + defaultMessage: 'Explore geospatial data from Elasticsearch and the Elastic Maps Service', + }), + icon: APP_ICON, + path: '/app/maps', + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, +}; diff --git a/x-pack/legacy/plugins/maps/public/legacy_register_feature.ts b/x-pack/legacy/plugins/maps/public/legacy_register_feature.ts new file mode 100644 index 00000000000000..00f788f267d4b1 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/legacy_register_feature.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 { npSetup } from 'ui/new_platform'; +import { featureCatalogueEntry } from './feature_catalogue_entry'; + +const { + plugins: { home }, +} = npSetup; + +home.featureCatalogue.register(featureCatalogueEntry); diff --git a/x-pack/legacy/plugins/maps/public/plugin.ts b/x-pack/legacy/plugins/maps/public/plugin.ts index e5f765a11d2193..e2af53d59671fe 100644 --- a/x-pack/legacy/plugins/maps/public/plugin.ts +++ b/x-pack/legacy/plugins/maps/public/plugin.ts @@ -11,6 +11,9 @@ import { wrapInI18nContext } from 'ui/i18n'; import { MapListing } from './components/map_listing'; // @ts-ignore import { setLicenseId, setInspector } from './kibana_services'; +import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; +import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; +import { featureCatalogueEntry } from './feature_catalogue_entry'; /** * These are the interfaces with your public contracts. You should export these @@ -20,14 +23,20 @@ import { setLicenseId, setInspector } from './kibana_services'; export type MapsPluginSetup = ReturnType; export type MapsPluginStart = ReturnType; +interface MapsPluginSetupDependencies { + __LEGACY: any; + np: { + licensing?: LicensingPluginSetup; + home: HomePublicPluginSetup; + }; +} + /** @internal */ export class MapsPlugin implements Plugin { - public setup(core: any, plugins: any) { - const { - __LEGACY: { uiModules }, - np: { licensing }, - } = plugins; - + public setup( + core: any, + { __LEGACY: { uiModules }, np: { licensing, home } }: MapsPluginSetupDependencies + ) { uiModules .get('app/maps', ['ngRoute', 'react']) .directive('mapListing', function(reactDirective: any) { @@ -35,8 +44,10 @@ export class MapsPlugin implements Plugin { }); if (licensing) { - licensing.license$.subscribe(({ uid }: { uid: string }) => setLicenseId(uid)); + licensing.license$.subscribe(({ uid }) => setLicenseId(uid)); } + + home.featureCatalogue.register(featureCatalogueEntry); } public start(core: CoreStart, plugins: any) { diff --git a/x-pack/legacy/plugins/maps/public/register_feature.js b/x-pack/legacy/plugins/maps/public/register_feature.js deleted file mode 100644 index afd7fb061500d2..00000000000000 --- a/x-pack/legacy/plugins/maps/public/register_feature.js +++ /dev/null @@ -1,27 +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 { - FeatureCatalogueRegistryProvider, - FeatureCatalogueCategory, -} from 'ui/registry/feature_catalogue'; -import { i18n } from '@kbn/i18n'; -import { APP_ID, APP_ICON } from '../common/constants'; -import { getAppTitle } from '../common/i18n_getters'; - -FeatureCatalogueRegistryProvider.register(() => { - return { - id: APP_ID, - title: getAppTitle(), - description: i18n.translate('xpack.maps.feature.appDescription', { - defaultMessage: 'Explore geospatial data from Elasticsearch and the Elastic Maps Service', - }), - icon: APP_ICON, - path: '/app/maps', - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA, - }; -}); diff --git a/x-pack/legacy/plugins/monitoring/public/register_feature.js b/x-pack/legacy/plugins/monitoring/public/register_feature.js deleted file mode 100644 index f275662bfb077d..00000000000000 --- a/x-pack/legacy/plugins/monitoring/public/register_feature.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import chrome from 'ui/chrome'; -import { i18n } from '@kbn/i18n'; -import { - FeatureCatalogueRegistryProvider, - FeatureCatalogueCategory, -} from 'ui/registry/feature_catalogue'; - -if (chrome.getInjected('monitoringUiEnabled')) { - FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'monitoring', - title: i18n.translate('xpack.monitoring.monitoringTitle', { - defaultMessage: 'Monitoring', - }), - description: i18n.translate('xpack.monitoring.monitoringDescription', { - defaultMessage: 'Track the real-time health and performance of your Elastic Stack.', - }), - icon: 'monitoringApp', - path: '/app/monitoring', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - }; - }); -} diff --git a/x-pack/legacy/plugins/monitoring/public/register_feature.ts b/x-pack/legacy/plugins/monitoring/public/register_feature.ts new file mode 100644 index 00000000000000..9b72e01a193945 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/register_feature.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import chrome from 'ui/chrome'; +import { npSetup } from 'ui/new_platform'; +import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; + +const { + plugins: { home }, +} = npSetup; + +if (chrome.getInjected('monitoringUiEnabled')) { + home.featureCatalogue.register({ + id: 'monitoring', + title: i18n.translate('xpack.monitoring.monitoringTitle', { + defaultMessage: 'Monitoring', + }), + description: i18n.translate('xpack.monitoring.monitoringDescription', { + defaultMessage: 'Track the real-time health and performance of your Elastic Stack.', + }), + icon: 'monitoringApp', + path: '/app/monitoring', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }); +} diff --git a/x-pack/legacy/plugins/reporting/public/register_feature.js b/x-pack/legacy/plugins/reporting/public/register_feature.js deleted file mode 100644 index 98de06fa16e33b..00000000000000 --- a/x-pack/legacy/plugins/reporting/public/register_feature.js +++ /dev/null @@ -1,28 +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 { - FeatureCatalogueRegistryProvider, - FeatureCatalogueCategory, -} from 'ui/registry/feature_catalogue'; - -import { i18n } from '@kbn/i18n'; - -FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'reporting', - title: i18n.translate('xpack.reporting.registerFeature.reportingTitle', { - defaultMessage: 'Reporting', - }), - description: i18n.translate('xpack.reporting.registerFeature.reportingDescription', { - defaultMessage: 'Manage your reports generated from Discover, Visualize, and Dashboard.', - }), - icon: 'reportingApp', - path: '/app/kibana#/management/kibana/reporting', - showOnHomePage: false, - category: FeatureCatalogueCategory.ADMIN, - }; -}); diff --git a/x-pack/legacy/plugins/reporting/public/register_feature.ts b/x-pack/legacy/plugins/reporting/public/register_feature.ts new file mode 100644 index 00000000000000..4e8d32facfcec6 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/public/register_feature.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { npSetup } from 'ui/new_platform'; +import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; + +const { + plugins: { home }, +} = npSetup; + +home.featureCatalogue.register({ + id: 'reporting', + title: i18n.translate('xpack.reporting.registerFeature.reportingTitle', { + defaultMessage: 'Reporting', + }), + description: i18n.translate('xpack.reporting.registerFeature.reportingDescription', { + defaultMessage: 'Manage your reports generated from Discover, Visualize, and Dashboard.', + }), + icon: 'reportingApp', + path: '/app/kibana#/management/kibana/reporting', + showOnHomePage: false, + category: FeatureCatalogueCategory.ADMIN, +}); diff --git a/x-pack/legacy/plugins/uptime/public/register_feature.ts b/x-pack/legacy/plugins/uptime/public/register_feature.ts index 885d4f6e1310f0..2f83fa33ba4bc5 100644 --- a/x-pack/legacy/plugins/uptime/public/register_feature.ts +++ b/x-pack/legacy/plugins/uptime/public/register_feature.ts @@ -5,12 +5,14 @@ */ import { i18n } from '@kbn/i18n'; -import { - FeatureCatalogueCategory, - FeatureCatalogueRegistryProvider, -} from 'ui/registry/feature_catalogue'; +import { npSetup } from 'ui/new_platform'; +import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; -FeatureCatalogueRegistryProvider.register(() => ({ +const { + plugins: { home }, +} = npSetup; + +home.featureCatalogue.register({ id: 'uptime', title: i18n.translate('xpack.uptime.uptimeFeatureCatalogueTitle', { defaultMessage: 'Uptime' }), description: i18n.translate('xpack.uptime.featureCatalogueDescription', { @@ -20,4 +22,4 @@ FeatureCatalogueRegistryProvider.register(() => ({ path: `uptime#/`, showOnHomePage: true, category: FeatureCatalogueCategory.DATA, -})); +}); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1c2a0fc3d5ac84..5d45a275ede116 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1436,8 +1436,8 @@ "kbn.management.indexPattern.confirmOverwriteTitle": "{type} を上書きしますか?", "kbn.management.indexPattern.sectionsHeader": "インデックスパターン", "kbn.management.indexPattern.titleExistsLabel": "「{title}」というタイトルのインデックスパターンが既に存在します。", - "kbn.management.indexPatternHeader": "インデックスパターン", - "kbn.management.indexPatternLabel": "Elasticsearch からのデータの取得に役立つインデックスパターンを管理します。", + "management.indexPatternHeader": "インデックスパターン", + "management.indexPatternLabel": "Elasticsearch からのデータの取得に役立つインデックスパターンを管理します。", "kbn.management.indexPatternList.createButton.betaLabel": "ベータ", "kbn.management.indexPatternPrompt.exampleOne": "チャートを作成したりコンテンツを素早くクエリできるように log-west-001 という名前の単一のデータソースをインデックスします。", "kbn.management.indexPatternPrompt.exampleOneTitle": "単一のデータソース", @@ -1556,9 +1556,9 @@ "kbn.management.objects.objectsTable.table.typeFilterName": "タイプ", "kbn.management.objects.objectsTable.unableFindSavedObjectsNotificationMessage": "保存されたオブジェクトが見つかりません", "kbn.management.objects.parsingFieldErrorMessage": "{fieldName} をインデックスパターン {indexName} 用にパース中にエラーが発生しました: {errorMessage}", - "kbn.management.objects.savedObjectsDescription": "保存された検索、ビジュアライゼーション、ダッシュボードのインポート、エクスポート、管理を行います", + "management.objects.savedObjectsDescription": "保存された検索、ビジュアライゼーション、ダッシュボードのインポート、エクスポート、管理を行います", "kbn.management.objects.savedObjectsSectionLabel": "保存されたオブジェクト", - "kbn.management.objects.savedObjectsTitle": "保存されたオブジェクト", + "management.objects.savedObjectsTitle": "保存されたオブジェクト", "kbn.management.objects.view.cancelButtonAriaLabel": "キャンセル", "kbn.management.objects.view.cancelButtonLabel": "キャンセル", "kbn.management.objects.view.deleteItemButtonLabel": "{title} を削除", @@ -1576,8 +1576,8 @@ "kbn.management.objects.view.viewItemTitle": "{title} を表示", "kbn.management.savedObjects.editBreadcrumb": "{savedObjectType} を編集", "kbn.management.savedObjects.indexBreadcrumb": "保存されたオブジェクト", - "kbn.management.settings.advancedSettingsDescription": "Kibana の動作を管理する設定を直接変更します。", - "kbn.management.settings.advancedSettingsLabel": "高度な設定", + "advancedSettings.advancedSettingsDescription": "Kibana の動作を管理する設定を直接変更します。", + "advancedSettings.advancedSettingsLabel": "高度な設定", "kbn.management.settings.breadcrumb": "高度な設定", "kbn.management.settings.callOutCautionDescription": "これらの設定は非常に上級ユーザー向けなのでご注意ください。ここでの変更は Kibana の重要な部分に不具合を生じさせる可能性があります。これらの設定はドキュメントに記載されていなかったり、サポートされていなかったり、実験的であったりします。フィールドにデフォルトの値が設定されている場合、そのフィールドを未入力のままにするとデフォルトに戻り、他の構成により利用できない可能性があります。カスタム設定を削除すると、Kibana の構成から永久に削除されます。", "kbn.management.settings.callOutCautionTitle": "注意:不具合が起こる可能性があります", @@ -2792,7 +2792,6 @@ "timelion.noFunctionErrorMessage": "そのような関数はありません: {name}", "timelion.panels.noRenderFunctionErrorMessage": "パネルにはレンダリング関数が必要です", "timelion.panels.timechart.unknownIntervalErrorMessage": "不明な間隔", - "timelion.registerFeatureDescription": "時系列データを分析して結果を可視化するには、式言語を使用してください。", "timelion.requestHandlerErrorTitle": "Timelion リクエストエラー", "timelion.savedObjects.howToSaveAsNewDescription": "Kibana の以前のバージョンでは、{savedObjectName} の名前を変更すると新しい名前でコピーが作成されました。今後この操作を行うには、「新規 {savedObjectName} として保存」を使用します。", "timelion.savedObjects.saveAsNewLabel": "新規 {savedObjectName} として保存", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c24f2952ef2ac7..6bbb3e59b25e33 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1436,8 +1436,8 @@ "kbn.management.indexPattern.confirmOverwriteTitle": "覆盖“{type}”?", "kbn.management.indexPattern.sectionsHeader": "索引模式", "kbn.management.indexPattern.titleExistsLabel": "具有标题 “{title}” 的索引模式已存在。", - "kbn.management.indexPatternHeader": "索引模式", - "kbn.management.indexPatternLabel": "管理帮助从 Elasticsearch 检索数据的索引模式。", + "management.indexPatternHeader": "索引模式", + "management.indexPatternLabel": "管理帮助从 Elasticsearch 检索数据的索引模式。", "kbn.management.indexPatternList.createButton.betaLabel": "公测版", "kbn.management.indexPatternPrompt.exampleOne": "索引单个称作 log-west-001 的数据源,以便可以快速地构建图表或查询其内容。", "kbn.management.indexPatternPrompt.exampleOneTitle": "单数据源", @@ -1556,9 +1556,9 @@ "kbn.management.objects.objectsTable.table.typeFilterName": "类型", "kbn.management.objects.objectsTable.unableFindSavedObjectsNotificationMessage": "找不到已保存对象", "kbn.management.objects.parsingFieldErrorMessage": "为索引模式 “{indexName}” 解析 “{fieldName}” 时发生错误:{errorMessage}", - "kbn.management.objects.savedObjectsDescription": "导入、导出和管理您的已保存搜索、可视化和仪表板。", + "management.objects.savedObjectsDescription": "导入、导出和管理您的已保存搜索、可视化和仪表板。", "kbn.management.objects.savedObjectsSectionLabel": "已保存对象", - "kbn.management.objects.savedObjectsTitle": "已保存对象", + "management.objects.savedObjectsTitle": "已保存对象", "kbn.management.objects.view.cancelButtonAriaLabel": "取消", "kbn.management.objects.view.cancelButtonLabel": "取消", "kbn.management.objects.view.deleteItemButtonLabel": "删除“{title}”", @@ -1576,8 +1576,8 @@ "kbn.management.objects.view.viewItemTitle": "查看“{title}”", "kbn.management.savedObjects.editBreadcrumb": "编辑 {savedObjectType}", "kbn.management.savedObjects.indexBreadcrumb": "已保存对象", - "kbn.management.settings.advancedSettingsDescription": "直接编辑在 Kibana 中控制行为的设置。", - "kbn.management.settings.advancedSettingsLabel": "高级设置", + "advancedSettings.advancedSettingsDescription": "直接编辑在 Kibana 中控制行为的设置。", + "advancedSettings.advancedSettingsLabel": "高级设置", "kbn.management.settings.breadcrumb": "高级设置", "kbn.management.settings.callOutCautionDescription": "此处请谨慎操作,这些设置仅供高级用户使用。您在这里所做的更改可能使 Kibana 的大部分功能出现问题。这些设置有一部分可能未在文档中说明、不受支持或是实验性设置。如果字段有默认值,将字段留空会将其设置为默认值,其他配置指令可能不接受其默认值。删除定制设置会将其从 Kibana 的配置中永久删除。", "kbn.management.settings.callOutCautionTitle": "注意:在这里您可能会使问题出现", @@ -2792,7 +2792,6 @@ "timelion.noFunctionErrorMessage": "没有此类函数:{name}", "timelion.panels.noRenderFunctionErrorMessage": "面板必须具有渲染函数", "timelion.panels.timechart.unknownIntervalErrorMessage": "时间间隔未知", - "timelion.registerFeatureDescription": "使用表达式语言分析时间序列数据,并将结果可视化。", "timelion.requestHandlerErrorTitle": "Timelion 请求错误", "timelion.savedObjects.howToSaveAsNewDescription": "在 Kibana 的以前版本中,更改 {savedObjectName} 的名称将创建具有新名称的副本。使用“另存为新的 {savedObjectName}” 复选框可立即达到此目的。", "timelion.savedObjects.saveAsNewLabel": "另存为新的 {savedObjectName}", From 181a3a0cd7d8240b851ed6788b0f8be93bc27661 Mon Sep 17 00:00:00 2001 From: Maryia Lapata Date: Mon, 10 Feb 2020 12:26:05 +0300 Subject: [PATCH 09/19] Move dashboardConfig to kibana_legacy platform (#57081) * Create dashboard_config.ts * Replace dashboardConfig in reporting * Remove dashboardConfigProvider * Fix TS * Add mock Co-authored-by: Elastic Machine --- .../kibana/public/dashboard/legacy.ts | 24 ++------------ .../public/dashboard/np_ready/application.ts | 2 +- .../np_ready/dashboard_app_controller.tsx | 3 +- .../kibana/public/dashboard/plugin.ts | 33 +++++++++---------- .../new_platform/new_platform.karma_mock.js | 4 +++ .../kibana_legacy/public/dashboard_config.ts} | 22 +++++-------- src/plugins/kibana_legacy/public/mocks.ts | 4 +++ src/plugins/kibana_legacy/public/plugin.ts | 12 +++++-- .../dashboard_mode/public/dashboard_viewer.js | 4 +-- .../share_context_menu/register_reporting.tsx | 9 ++--- 10 files changed, 54 insertions(+), 63 deletions(-) rename src/{legacy/core_plugins/kibana/public/dashboard/dashboard_config.js => plugins/kibana_legacy/public/dashboard_config.ts} (69%) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts index ca2dc9d5fb4f5e..9c13337a71126e 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts @@ -18,34 +18,14 @@ */ import { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart, legacyChrome } from './legacy_imports'; -import { LegacyAngularInjectedDependencies } from './plugin'; +import { npSetup, npStart } from './legacy_imports'; import { start as data } from '../../../data/public/legacy'; import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy'; -import './dashboard_config'; import { plugin } from './index'; -/** - * Get dependencies relying on the global angular context. - * They also have to get resolved together with the legacy imports above - */ -async function getAngularDependencies(): Promise { - const injector = await legacyChrome.dangerouslyGetActiveInjector(); - - return { - dashboardConfig: injector.get('dashboardConfig'), - }; -} - (async () => { const instance = plugin({} as PluginInitializerContext); - instance.setup(npSetup.core, { - ...npSetup.plugins, - npData: npSetup.plugins.data, - __LEGACY: { - getAngularDependencies, - }, - }); + instance.setup(npSetup.core, npSetup.plugins); instance.start(npStart.core, { ...npStart.plugins, data, 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 b0e4785edcb0b2..3af0b29b80acb6 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 @@ -54,7 +54,7 @@ export interface RenderDeps { navigation: NavigationStart; savedObjectsClient: SavedObjectsClientContract; savedDashboards: SavedObjectLoader; - dashboardConfig: any; + dashboardConfig: KibanaLegacyStart['dashboardConfig']; dashboardCapabilities: any; uiSettings: IUiSettingsClient; chrome: ChromeStart; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index 9f6b01d5beb492..c44e36eab8c760 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -82,13 +82,14 @@ import { removeQueryParam, unhashUrl, } from '../../../../../../plugins/kibana_utils/public'; +import { KibanaLegacyStart } from '../../../../../../plugins/kibana_legacy/public'; export interface DashboardAppControllerDependencies extends RenderDeps { $scope: DashboardAppScope; $route: any; $routeParams: any; indexPatterns: IndexPatternsContract; - dashboardConfig: any; + dashboardConfig: KibanaLegacyStart['dashboardConfig']; confirmModal: ConfirmModalFn; history: History; kbnUrlStateStorage: IKbnUrlStateStorage; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index 7ae1c723a3914a..09ae49f2305fda 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -45,30 +45,25 @@ import { SharePluginStart } from '../../../../../plugins/share/public'; import { AngularRenderedAppUpdater, KibanaLegacySetup, + KibanaLegacyStart, } from '../../../../../plugins/kibana_legacy/public'; import { createSavedDashboardLoader } from './saved_dashboard/saved_dashboards'; import { createKbnUrlTracker } from '../../../../../plugins/kibana_utils/public'; import { getQueryStateContainer } from '../../../../../plugins/data/public'; -export interface LegacyAngularInjectedDependencies { - dashboardConfig: any; -} - export interface DashboardPluginStartDependencies { data: DataStart; npData: NpDataStart; embeddables: IEmbeddableStart; navigation: NavigationStart; share: SharePluginStart; + kibanaLegacy: KibanaLegacyStart; } export interface DashboardPluginSetupDependencies { - __LEGACY: { - getAngularDependencies: () => Promise; - }; home: HomePublicPluginSetup; kibanaLegacy: KibanaLegacySetup; - npData: NpDataSetup; + data: NpDataSetup; } export class DashboardPlugin implements Plugin { @@ -78,6 +73,7 @@ export class DashboardPlugin implements Plugin { embeddables: IEmbeddableStart; navigation: NavigationStart; share: SharePluginStart; + dashboardConfig: KibanaLegacyStart['dashboardConfig']; } | null = null; private appStateUpdater = new BehaviorSubject(() => ({})); @@ -85,12 +81,7 @@ export class DashboardPlugin implements Plugin { public setup( core: CoreSetup, - { - __LEGACY: { getAngularDependencies }, - home, - kibanaLegacy, - npData, - }: DashboardPluginSetupDependencies + { home, kibanaLegacy, data: npData }: DashboardPluginSetupDependencies ) { const { querySyncStateContainer, stop: stopQuerySyncStateContainer } = getQueryStateContainer( npData.query @@ -126,8 +117,8 @@ export class DashboardPlugin implements Plugin { navigation, share, npDataStart, + dashboardConfig, } = this.startDependencies; - const angularDependencies = await getAngularDependencies(); const savedDashboards = createSavedDashboardLoader({ savedObjectsClient, indexPatterns: npDataStart.indexPatterns, @@ -137,7 +128,7 @@ export class DashboardPlugin implements Plugin { const deps: RenderDeps = { core: contextCore as LegacyCoreStart, - ...angularDependencies, + dashboardConfig, navigation, share, npDataStart, @@ -186,7 +177,14 @@ export class DashboardPlugin implements Plugin { start( { savedObjects: { client: savedObjectsClient } }: CoreStart, - { data: dataStart, embeddables, navigation, npData, share }: DashboardPluginStartDependencies + { + data: dataStart, + embeddables, + navigation, + npData, + share, + kibanaLegacy: { dashboardConfig }, + }: DashboardPluginStartDependencies ) { this.startDependencies = { npDataStart: npData, @@ -194,6 +192,7 @@ export class DashboardPlugin implements Plugin { embeddables, navigation, share, + dashboardConfig, }; } diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 3cc33504d3daab..985dbc78e2f77a 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -212,6 +212,10 @@ export const npStart = { config: { defaultAppId: 'home', }, + dashboardConfig: { + turnHideWriteControlsOn: sinon.fake(), + getHideWriteControls: sinon.fake(), + }, }, data: { autocomplete: { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_config.js b/src/plugins/kibana_legacy/public/dashboard_config.ts similarity index 69% rename from src/legacy/core_plugins/kibana/public/dashboard/dashboard_config.js rename to src/plugins/kibana_legacy/public/dashboard_config.ts index aa8333a1bafca4..3c7670682ce259 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_config.js +++ b/src/plugins/kibana_legacy/public/dashboard_config.ts @@ -17,11 +17,13 @@ * under the License. */ -import { uiModules } from 'ui/modules'; -import { capabilities } from 'ui/capabilities'; +export interface DashboardConfig { + turnHideWriteControlsOn(): void; + getHideWriteControls(): boolean; +} -export function dashboardConfigProvider() { - let hideWriteControls = !capabilities.get().dashboard.showWriteControls; +export function getDashboardConfig(hideWriteControls: boolean): DashboardConfig { + let _hideWriteControls = hideWriteControls; return { /** @@ -29,16 +31,10 @@ export function dashboardConfigProvider() { * @type {boolean} */ turnHideWriteControlsOn() { - hideWriteControls = true; + _hideWriteControls = true; }, - $get() { - return { - getHideWriteControls() { - return hideWriteControls; - }, - }; + getHideWriteControls() { + return _hideWriteControls; }, }; } - -uiModules.get('kibana').provider('dashboardConfig', dashboardConfigProvider); diff --git a/src/plugins/kibana_legacy/public/mocks.ts b/src/plugins/kibana_legacy/public/mocks.ts index b6287dd9d9a556..aab3ab315f0c63 100644 --- a/src/plugins/kibana_legacy/public/mocks.ts +++ b/src/plugins/kibana_legacy/public/mocks.ts @@ -36,6 +36,10 @@ const createStartContract = (): Start => ({ config: { defaultAppId: 'home', }, + dashboardConfig: { + turnHideWriteControlsOn: jest.fn(), + getHideWriteControls: jest.fn(), + }, }); export const kibanaLegacyPluginMock = { diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts index 7c4b3428cbb6d1..86e56c44646c06 100644 --- a/src/plugins/kibana_legacy/public/plugin.ts +++ b/src/plugins/kibana_legacy/public/plugin.ts @@ -17,9 +17,16 @@ * under the License. */ -import { App, AppBase, PluginInitializerContext, AppUpdatableFields } from 'kibana/public'; +import { + App, + AppBase, + PluginInitializerContext, + AppUpdatableFields, + CoreStart, +} from 'kibana/public'; import { Observable } from 'rxjs'; import { ConfigSchema } from '../config'; +import { getDashboardConfig } from './dashboard_config'; interface ForwardDefinition { legacyAppId: string; @@ -104,7 +111,7 @@ export class KibanaLegacyPlugin { }; } - public start() { + public start({ application }: CoreStart) { return { /** * @deprecated @@ -117,6 +124,7 @@ export class KibanaLegacyPlugin { */ getForwards: () => this.forwards, config: this.initializerContext.config.get(), + dashboardConfig: getDashboardConfig(!application.capabilities.dashboard.showWriteControls), }; } } diff --git a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js index 44c4b81c8ad937..8ca023aa90cf12 100644 --- a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js @@ -40,9 +40,7 @@ import { localApplicationService } from 'plugins/kibana/local_application_servic import { showAppRedirectNotification } from 'ui/notify'; import { DashboardConstants, createDashboardEditUrl } from 'plugins/kibana/dashboard'; -uiModules - .get('kibana') - .config(dashboardConfigProvider => dashboardConfigProvider.turnHideWriteControlsOn()); +npStart.plugins.kibanaLegacy.dashboardConfig.turnHideWriteControlsOn(); localApplicationService.attachToAngular(routes); diff --git a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx b/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx index 8e0da6a69225e0..4153c7cdbdb0b7 100644 --- a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx +++ b/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx @@ -8,16 +8,14 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; // @ts-ignore: implicit any for JS file import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { npSetup } from 'ui/new_platform'; +import { npSetup, npStart } from 'ui/new_platform'; import React from 'react'; -import chrome from 'ui/chrome'; import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content'; import { ShareContext } from '../../../../../../src/plugins/share/public'; const { core } = npSetup; async function reportingProvider() { - const injector = await chrome.dangerouslyGetActiveInjector(); const getShareMenuItems = ({ objectType, objectId, @@ -31,7 +29,10 @@ async function reportingProvider() { } // Dashboard only mode does not currently support reporting // https://github.com/elastic/kibana/issues/18286 - if (objectType === 'dashboard' && injector.get('dashboardConfig').getHideWriteControls()) { + if ( + objectType === 'dashboard' && + npStart.plugins.kibanaLegacy.dashboardConfig.getHideWriteControls() + ) { return []; } From 8619423351c9c0a02b2845869c41610660dcafec Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 10 Feb 2020 11:27:16 +0100 Subject: [PATCH 10/19] fix default app id key (#56997) --- src/plugins/kibana_legacy/server/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/kibana_legacy/server/index.ts b/src/plugins/kibana_legacy/server/index.ts index ca8ad6410eec30..4d0fe8364a66c5 100644 --- a/src/plugins/kibana_legacy/server/index.ts +++ b/src/plugins/kibana_legacy/server/index.ts @@ -28,7 +28,7 @@ export const config: PluginConfigDescriptor = { schema: configSchema, deprecations: ({ renameFromRoot }) => [ // TODO: Remove deprecation once defaultAppId is deleted - renameFromRoot('kibana.defaultAppId', 'kibanaLegacy.defaultAppId', true), + renameFromRoot('kibana.defaultAppId', 'kibana_legacy.defaultAppId', true), ], }; From e0b7ffff69cac4f2431d248cb5b642670cad2070 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Mon, 10 Feb 2020 11:54:11 +0100 Subject: [PATCH 11/19] [SIEM] Overview Cypress test refactor (#56887) * extracts stub to a custom command * refactors overview tests Co-authored-by: Elastic Machine --- .../smoke_tests/overview/overview.spec.ts | 33 ++-- .../plugins/siem/cypress/screens/overview.ts | 144 ++++++++++++++++++ .../plugins/siem/cypress/support/commands.js | 10 ++ .../plugins/siem/cypress/support/index.d.ts | 11 ++ .../plugins/siem/cypress/tasks/overview.ts | 21 +++ .../plugins/siem/cypress/urls/navigation.ts | 1 + 6 files changed, 198 insertions(+), 22 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/cypress/screens/overview.ts create mode 100644 x-pack/legacy/plugins/siem/cypress/support/index.d.ts create mode 100644 x-pack/legacy/plugins/siem/cypress/tasks/overview.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts index be66fdc86be36d..64002aadc86d83 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts @@ -4,40 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { OVERVIEW_PAGE } from '../../lib/urls'; -import { clearFetch, stubApi } from '../../lib/fixtures/helpers'; -import { - HOST_STATS, - NETWORK_STATS, - OVERVIEW_HOST_STATS, - OVERVIEW_NETWORK_STATS, - STAT_AUDITD, -} from '../../lib/overview/selectors'; +import { OVERVIEW_PAGE } from '../../../urls/navigation'; +import { HOST_STATS, NETWORK_STATS } from '../../../screens/overview'; +import { expandHostStats, expandNetworkStats } from '../../../tasks/overview'; import { loginAndWaitForPage } from '../../lib/util/helpers'; describe('Overview Page', () => { - beforeEach(() => { - clearFetch(); - stubApi('overview'); + before(() => { + cy.stubSIEMapi('overview'); loginAndWaitForPage(OVERVIEW_PAGE); }); - it('Host and Network stats render with correct values', () => { - cy.get(OVERVIEW_HOST_STATS) - .find('button') - .invoke('click'); - - cy.get(OVERVIEW_NETWORK_STATS) - .find('button') - .invoke('click'); - - cy.get(STAT_AUDITD.domId); + it('Host stats render with correct values', () => { + expandHostStats(); HOST_STATS.forEach(stat => { cy.get(stat.domId) .invoke('text') .should('eq', stat.value); }); + }); + + it('Network stats render with correct values', () => { + expandNetworkStats(); NETWORK_STATS.forEach(stat => { cy.get(stat.domId) diff --git a/x-pack/legacy/plugins/siem/cypress/screens/overview.ts b/x-pack/legacy/plugins/siem/cypress/screens/overview.ts new file mode 100644 index 00000000000000..95facc89744009 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/screens/overview.ts @@ -0,0 +1,144 @@ +/* + * 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. + */ + +// Host Stats +export const STAT_AUDITD = { + value: '123', + domId: '[data-test-subj="host-stat-auditbeatAuditd"]', +}; +export const ENDGAME_DNS = { + value: '391', + domId: '[data-test-subj="host-stat-endgameDns"]', +}; +export const ENDGAME_FILE = { + value: '392', + domId: '[data-test-subj="host-stat-endgameFile"]', +}; +export const ENDGAME_IMAGE_LOAD = { + value: '393', + domId: '[data-test-subj="host-stat-endgameImageLoad"]', +}; +export const ENDGAME_NETWORK = { + value: '394', + domId: '[data-test-subj="host-stat-endgameNetwork"]', +}; +export const ENDGAME_PROCESS = { + value: '395', + domId: '[data-test-subj="host-stat-endgameProcess"]', +}; +export const ENDGAME_REGISTRY = { + value: '396', + domId: '[data-test-subj="host-stat-endgameRegistry"]', +}; +export const ENDGAME_SECURITY = { + value: '397', + domId: '[data-test-subj="host-stat-endgameSecurity"]', +}; +export const STAT_FILEBEAT = { + value: '890', + domId: '[data-test-subj="host-stat-filebeatSystemModule"]', +}; +export const STAT_FIM = { + value: '345', + domId: '[data-test-subj="host-stat-auditbeatFIM"]', +}; +export const STAT_LOGIN = { + value: '456', + domId: '[data-test-subj="host-stat-auditbeatLogin"]', +}; +export const STAT_PACKAGE = { + value: '567', + domId: '[data-test-subj="host-stat-auditbeatPackage"]', +}; +export const STAT_PROCESS = { + value: '678', + domId: '[data-test-subj="host-stat-auditbeatProcess"]', +}; +export const STAT_USER = { + value: '789', + domId: '[data-test-subj="host-stat-auditbeatUser"]', +}; +export const STAT_WINLOGBEAT_SECURITY = { + value: '70', + domId: '[data-test-subj="host-stat-winlogbeatSecurity"]', +}; +export const STAT_WINLOGBEAT_MWSYSMON_OPERATIONAL = { + value: '30', + domId: '[data-test-subj="host-stat-winlogbeatMWSysmonOperational"]', +}; + +export const HOST_STATS = [ + STAT_AUDITD, + ENDGAME_DNS, + ENDGAME_FILE, + ENDGAME_IMAGE_LOAD, + ENDGAME_NETWORK, + ENDGAME_PROCESS, + ENDGAME_REGISTRY, + ENDGAME_SECURITY, + STAT_FILEBEAT, + STAT_FIM, + STAT_LOGIN, + STAT_PACKAGE, + STAT_PROCESS, + STAT_USER, + STAT_WINLOGBEAT_SECURITY, + STAT_WINLOGBEAT_MWSYSMON_OPERATIONAL, +]; + +// Network Stats +export const STAT_SOCKET = { + value: '578,502', + domId: '[data-test-subj="network-stat-auditbeatSocket"]', +}; +export const STAT_CISCO = { + value: '999', + domId: '[data-test-subj="network-stat-filebeatCisco"]', +}; +export const STAT_NETFLOW = { + value: '2,544', + domId: '[data-test-subj="network-stat-filebeatNetflow"]', +}; +export const STAT_PANW = { + value: '678', + domId: '[data-test-subj="network-stat-filebeatPanw"]', +}; +export const STAT_SURICATA = { + value: '303,699', + domId: '[data-test-subj="network-stat-filebeatSuricata"]', +}; +export const STAT_ZEEK = { + value: '71,129', + domId: '[data-test-subj="network-stat-filebeatZeek"]', +}; +export const STAT_DNS = { + value: '1,090', + domId: '[data-test-subj="network-stat-packetbeatDNS"]', +}; +export const STAT_FLOW = { + value: '722,153', + domId: '[data-test-subj="network-stat-packetbeatFlow"]', +}; +export const STAT_TLS = { + value: '340', + domId: '[data-test-subj="network-stat-packetbeatTLS"]', +}; + +export const NETWORK_STATS = [ + STAT_SOCKET, + STAT_CISCO, + STAT_NETFLOW, + STAT_PANW, + STAT_SURICATA, + STAT_ZEEK, + STAT_DNS, + STAT_FLOW, + STAT_TLS, +]; + +export const OVERVIEW_HOST_STATS = '[data-test-subj="overview-hosts-stats"]'; + +export const OVERVIEW_NETWORK_STATS = '[data-test-subj="overview-network-stats"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/support/commands.js b/x-pack/legacy/plugins/siem/cypress/support/commands.js index 9a2e54b102c5e0..e697dbce0f2491 100644 --- a/x-pack/legacy/plugins/siem/cypress/support/commands.js +++ b/x-pack/legacy/plugins/siem/cypress/support/commands.js @@ -29,3 +29,13 @@ // // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) + +Cypress.Commands.add('stubSIEMapi', function(dataFileName) { + cy.on('window:before:load', win => { + // @ts-ignore no null, this is a temp hack see issue above + win.fetch = null; + }); + cy.server(); + cy.fixture(dataFileName).as(`${dataFileName}JSON`); + cy.route('POST', 'api/siem/graphql', `@${dataFileName}JSON`); +}); diff --git a/x-pack/legacy/plugins/siem/cypress/support/index.d.ts b/x-pack/legacy/plugins/siem/cypress/support/index.d.ts new file mode 100644 index 00000000000000..5d5173170a9f96 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/support/index.d.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +declare namespace Cypress { + interface Chainable { + stubSIEMapi(dataFileName: string): Chainable; + } +} diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/overview.ts b/x-pack/legacy/plugins/siem/cypress/tasks/overview.ts new file mode 100644 index 00000000000000..0ca4059a90097e --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/tasks/overview.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { OVERVIEW_HOST_STATS, OVERVIEW_NETWORK_STATS } from '../screens/overview'; + +export const expand = (statType: string) => { + cy.get(statType) + .find('button') + .invoke('click'); +}; + +export const expandHostStats = () => { + expand(OVERVIEW_HOST_STATS); +}; + +export const expandNetworkStats = () => { + expand(OVERVIEW_NETWORK_STATS); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts b/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts index 4675829df839a8..35db3003ac4365 100644 --- a/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts +++ b/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts @@ -5,3 +5,4 @@ */ export const TIMELINES_PAGE = '/app/siem#/timelines'; +export const OVERVIEW_PAGE = '/app/siem#/overview'; From bdba16d92c28c95a0c078f45548436e33104d59f Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 10 Feb 2020 12:14:55 +0100 Subject: [PATCH 12/19] Update CCR auto follower copy (#57135) * Update CCR auto follower copy. "pattern" -> "replication" * Update copy in test expectation Co-authored-by: Elastic Machine --- .../client_integration/auto_follow_pattern_list.test.js | 2 +- .../auto_follow_pattern_action_menu.tsx | 4 ++-- .../auto_follow_pattern_table/auto_follow_pattern_table.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js index 1003569733d911..88d8f98b973bd4 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js @@ -286,7 +286,7 @@ describe('', () => { actions.clickAutoFollowPatternAt(0); find('autoFollowPatternActionMenuButton').simulate('click'); expect(exists('autoFollowPatternDetail.closeFlyoutButton')).toBe(true); - expect(actions.getPatternsActionMenuItemText(0)).toEqual('Resume pattern'); + expect(actions.getPatternsActionMenuItemText(0)).toEqual('Resume replication'); expect(actions.getPatternsActionMenuItemText(1)).toEqual('Edit pattern'); expect(actions.getPatternsActionMenuItemText(2)).toEqual('Delete pattern'); }); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx index 7c129eac9cbd9f..12654e56bde97a 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx @@ -68,7 +68,7 @@ const AutoFollowPatternActionMenuUI: FunctionComponent = ({ ? patterns[0].active ? { name: i18n.translate('xpack.crossClusterReplication.pauseAutoFollowPatternsLabel', { - defaultMessage: 'Pause {total, plural, one {pattern} other {patterns}}', + defaultMessage: 'Pause {total, plural, one {replication} other {replications}}', values: { total: patterns.length }, }), icon: , @@ -79,7 +79,7 @@ const AutoFollowPatternActionMenuUI: FunctionComponent = ({ } : { name: i18n.translate('xpack.crossClusterReplication.resumeAutoFollowPatternsLabel', { - defaultMessage: 'Resume {total, plural, one {pattern} other {patterns}}', + defaultMessage: 'Resume {total, plural, one {replication} other {replications}}', values: { total: patterns.length }, }), icon: , diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js index 08b1770e399639..956a9f10d810bf 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js @@ -180,13 +180,13 @@ export class AutoFollowPatternTable extends PureComponent { ? i18n.translate( 'xpack.crossClusterReplication.autoFollowPatternList.table.actionPauseDescription', { - defaultMessage: 'Pause auto-follow pattern', + defaultMessage: 'Pause replication', } ) : i18n.translate( 'xpack.crossClusterReplication.autoFollowPatternList.table.actionResumeDescription', { - defaultMessage: 'Resume auto-follow pattern', + defaultMessage: 'Resume replication', } ); From 56571bac847b1579dbaca2d292614744a248d297 Mon Sep 17 00:00:00 2001 From: Maryia Lapata Date: Mon, 10 Feb 2020 14:32:28 +0300 Subject: [PATCH 13/19] Remove confirm modal directive and factory (#56846) * Graph: replace confirmModal * Remove confirmModal from visualize * Remove confirmModal from dashboard * Remove confirm_modal * Remove confirmModalPromises * Replace confirmModal * FIx TS * Add data-test-subj for graph confirm modal * Update public.api.md * Remove unused translation * Update mock test --- .../public/overlays/modal/modal_service.tsx | 3 +- src/core/public/public.api.md | 1 + .../kibana/public/dashboard/legacy_imports.ts | 4 - .../public/dashboard/np_ready/application.ts | 12 +- .../dashboard/np_ready/dashboard_app.tsx | 5 +- .../np_ready/dashboard_app_controller.tsx | 56 ++++--- .../kibana/public/dashboard/np_ready/types.ts | 12 -- .../create_index_pattern_wizard.js | 15 +- .../create_index_pattern_wizard/index.js | 2 +- .../edit_index_pattern/edit_index_pattern.js | 25 ++-- .../management/sections/objects/_objects.js | 3 +- .../management/sections/objects/_view.js | 23 ++- .../objects/lib/resolve_saved_objects.js | 33 +++-- .../kibana/public/visualize/legacy_imports.ts | 2 - .../public/visualize/np_ready/application.ts | 11 -- .../core_plugins/timelion/public/app.js | 20 ++- src/legacy/ui/public/autoload/modules.js | 1 - .../public/modals/__tests__/confirm_modal.js | 137 ------------------ .../modals/__tests__/confirm_modal_promise.js | 115 --------------- .../ui/public/modals/confirm_modal.html | 10 -- src/legacy/ui/public/modals/confirm_modal.js | 119 --------------- .../ui/public/modals/confirm_modal_promise.js | 49 ------- src/legacy/ui/public/modals/index.js | 24 --- .../ui/public/modals/modal_overlay.html | 1 - src/legacy/ui/public/modals/modal_overlay.js | 40 ----- .../new_platform/new_platform.karma_mock.js | 6 +- src/legacy/ui/public/react_components.js | 4 +- .../saved_objects/__tests__/saved_object.js | 20 +-- x-pack/legacy/plugins/graph/public/app.js | 27 ++-- .../plugins/graph/public/application.ts | 14 +- .../plugins/graph/public/legacy_imports.ts | 2 - x-pack/legacy/plugins/graph/public/plugin.ts | 1 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 34 files changed, 137 insertions(+), 662 deletions(-) delete mode 100644 src/legacy/ui/public/modals/__tests__/confirm_modal.js delete mode 100644 src/legacy/ui/public/modals/__tests__/confirm_modal_promise.js delete mode 100644 src/legacy/ui/public/modals/confirm_modal.html delete mode 100644 src/legacy/ui/public/modals/confirm_modal.js delete mode 100644 src/legacy/ui/public/modals/confirm_modal_promise.js delete mode 100644 src/legacy/ui/public/modals/index.js delete mode 100644 src/legacy/ui/public/modals/modal_overlay.html delete mode 100644 src/legacy/ui/public/modals/modal_overlay.js diff --git a/src/core/public/overlays/modal/modal_service.tsx b/src/core/public/overlays/modal/modal_service.tsx index ba7887b1afa5cf..3cf1fe745be8e7 100644 --- a/src/core/public/overlays/modal/modal_service.tsx +++ b/src/core/public/overlays/modal/modal_service.tsx @@ -20,7 +20,7 @@ /* eslint-disable max-classes-per-file */ import { i18n as t } from '@kbn/i18n'; -import { EuiModal, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiModal, EuiConfirmModal, EuiOverlayMask, EuiConfirmModalProps } from '@elastic/eui'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Subject } from 'rxjs'; @@ -68,6 +68,7 @@ export interface OverlayModalConfirmOptions { className?: string; closeButtonAriaLabel?: string; 'data-test-subj'?: string; + defaultFocusedButton?: EuiConfirmModalProps['defaultFocusedButton']; } /** diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index fb48524c20fb94..aa7ca4fee675ed 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -6,6 +6,7 @@ import { Breadcrumb } from '@elastic/eui'; import { EuiButtonEmptyProps } from '@elastic/eui'; +import { EuiConfirmModalProps } from '@elastic/eui'; import { EuiGlobalToastListToast } from '@elastic/eui'; import { ExclusiveUnion } from '@elastic/eui'; import { IconType } from '@elastic/eui'; 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 b729691831e9ad..57edf5e838170d 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -30,15 +30,11 @@ export const legacyChrome = chrome; export { SavedObjectSaveOpts } from 'ui/saved_objects/types'; export { npSetup, npStart } from 'ui/new_platform'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; -// @ts-ignore -export { ConfirmationButtonTypes } from 'ui/modals/confirm_modal'; export { KbnUrl } from 'ui/url/kbn_url'; // @ts-ignore export { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_top_nav'; // @ts-ignore export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url/index'; -// @ts-ignore -export { confirmModalFactory } from 'ui/modals/confirm_modal'; export { IInjector } from 'ui/chrome'; export { SavedObjectLoader } from 'ui/saved_objects'; export { VISUALIZE_EMBEDDABLE_TYPE } from '../../../visualizations/public/embeddable'; 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 3af0b29b80acb6..e608eb7b7f48c0 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 @@ -17,7 +17,7 @@ * under the License. */ -import { EuiConfirmModal, EuiIcon } from '@elastic/eui'; +import { EuiIcon } from '@elastic/eui'; import angular, { IModule } from 'angular'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { @@ -30,7 +30,6 @@ import { import { Storage } from '../../../../../../plugins/kibana_utils/public'; import { configureAppAngularModule, - confirmModalFactory, createTopNavDirective, createTopNavHelper, IPrivate, @@ -111,7 +110,6 @@ function createLocalAngularModule(core: AppMountContext['core'], navigation: Nav createLocalConfigModule(core); createLocalKbnUrlModule(); createLocalTopNavModule(navigation); - createLocalConfirmModalModule(); createLocalIconModule(); const dashboardAngularModule = angular.module(moduleName, [ @@ -122,7 +120,6 @@ function createLocalAngularModule(core: AppMountContext['core'], navigation: Nav 'app/dashboard/TopNav', 'app/dashboard/KbnUrl', 'app/dashboard/Promise', - 'app/dashboard/ConfirmModal', 'app/dashboard/icon', ]); return dashboardAngularModule; @@ -134,13 +131,6 @@ function createLocalIconModule() { .directive('icon', reactDirective => reactDirective(EuiIcon)); } -function createLocalConfirmModalModule() { - angular - .module('app/dashboard/ConfirmModal', ['react']) - .factory('confirmModal', confirmModalFactory) - .directive('confirmModal', reactDirective => reactDirective(EuiConfirmModal)); -} - function createLocalKbnUrlModule() { angular .module('app/dashboard/KbnUrl', ['app/dashboard/Private', 'ngRoute']) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx index 0537e3f8fc456d..ad69ef322a9099 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx @@ -25,7 +25,7 @@ import { IInjector } from '../legacy_imports'; import { ViewMode } from '../../../../embeddable_api/public/np_ready/public'; import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard'; -import { DashboardAppState, SavedDashboardPanel, ConfirmModalFn } from './types'; +import { DashboardAppState, SavedDashboardPanel } from './types'; import { IIndexPattern, TimeRange, @@ -87,8 +87,6 @@ export interface DashboardAppScope extends ng.IScope { export function initDashboardAppDirective(app: any, deps: RenderDeps) { app.directive('dashboardApp', function($injector: IInjector) { - const confirmModal = $injector.get('confirmModal'); - return { restrict: 'E', controllerAs: 'dashboardApp', @@ -105,7 +103,6 @@ export function initDashboardAppDirective(app: any, deps: RenderDeps) { $route, $scope, $routeParams, - confirmModal, indexPatterns: deps.npDataStart.indexPatterns, kbnUrlStateStorage, history, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index c44e36eab8c760..0b55adc1d52be7 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -19,6 +19,7 @@ import _, { uniq } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { EUI_MODAL_CANCEL_BUTTON } from '@elastic/eui'; import React from 'react'; import angular from 'angular'; @@ -27,12 +28,7 @@ import { map } from 'rxjs/operators'; import { History } from 'history'; import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen'; -import { - ConfirmationButtonTypes, - migrateLegacyQuery, - SavedObjectSaveOpts, - subscribeWithScope, -} from '../legacy_imports'; +import { migrateLegacyQuery, SavedObjectSaveOpts, subscribeWithScope } from '../legacy_imports'; import { COMPARE_ALL_OPTIONS, compareFilters, @@ -63,7 +59,7 @@ import { openAddPanelFlyout, ViewMode, } from '../../../../embeddable_api/public/np_ready/public'; -import { ConfirmModalFn, NavAction, SavedDashboardPanel } from './types'; +import { NavAction, SavedDashboardPanel } from './types'; import { showOptionsPopover } from './top_nav/show_options_popover'; import { DashboardSaveModal } from './top_nav/save_modal'; @@ -90,7 +86,6 @@ export interface DashboardAppControllerDependencies extends RenderDeps { $routeParams: any; indexPatterns: IndexPatternsContract; dashboardConfig: KibanaLegacyStart['dashboardConfig']; - confirmModal: ConfirmModalFn; history: History; kbnUrlStateStorage: IKbnUrlStateStorage; } @@ -108,7 +103,6 @@ export class DashboardAppController { dashboardConfig, localStorage, indexPatterns, - confirmModal, savedQueryService, embeddables, share, @@ -635,27 +629,31 @@ export class DashboardAppController { } } - confirmModal( - i18n.translate('kbn.dashboard.changeViewModeConfirmModal.discardChangesDescription', { - defaultMessage: `Once you discard your changes, there's no getting them back.`, - }), - { - onConfirm: revertChangesAndExitEditMode, - onCancel: _.noop, - confirmButtonText: i18n.translate( - 'kbn.dashboard.changeViewModeConfirmModal.confirmButtonLabel', - { defaultMessage: 'Discard changes' } - ), - cancelButtonText: i18n.translate( - 'kbn.dashboard.changeViewModeConfirmModal.cancelButtonLabel', - { defaultMessage: 'Continue editing' } - ), - defaultFocusedButton: ConfirmationButtonTypes.CANCEL, - title: i18n.translate('kbn.dashboard.changeViewModeConfirmModal.discardChangesTitle', { - defaultMessage: 'Discard changes to dashboard?', + overlays + .openConfirm( + i18n.translate('kbn.dashboard.changeViewModeConfirmModal.discardChangesDescription', { + defaultMessage: `Once you discard your changes, there's no getting them back.`, }), - } - ); + { + confirmButtonText: i18n.translate( + 'kbn.dashboard.changeViewModeConfirmModal.confirmButtonLabel', + { defaultMessage: 'Discard changes' } + ), + cancelButtonText: i18n.translate( + 'kbn.dashboard.changeViewModeConfirmModal.cancelButtonLabel', + { defaultMessage: 'Continue editing' } + ), + defaultFocusedButton: EUI_MODAL_CANCEL_BUTTON, + title: i18n.translate('kbn.dashboard.changeViewModeConfirmModal.discardChangesTitle', { + defaultMessage: 'Discard changes to dashboard?', + }), + } + ) + .then(isConfirmed => { + if (isConfirmed) { + revertChangesAndExitEditMode(); + } + }); }; /** diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/types.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/types.ts index 3151fbf821b9fe..146affda282008 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/types.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/types.ts @@ -137,15 +137,3 @@ export interface StagedFilter { operator: string; index: string; } - -export type ConfirmModalFn = ( - message: string, - confirmOptions: { - onConfirm: () => void; - onCancel: () => void; - confirmButtonText: string; - cancelButtonText: string; - defaultFocusedButton: string; - title: string; - } -) => void; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js index 77b43a651d548e..b5c6000eb2fe1c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js @@ -43,6 +43,7 @@ export class CreateIndexPatternWizard extends Component { indexPatternCreationType: PropTypes.object.isRequired, config: PropTypes.object.isRequired, changeUrl: PropTypes.func.isRequired, + openConfirm: PropTypes.func.isRequired, }).isRequired, }; @@ -142,12 +143,16 @@ export class CreateIndexPatternWizard extends Component { values: { title: this.title }, defaultMessage: "An index pattern with the title '{title}' already exists.", }); - try { - await services.confirmModalPromise(confirmMessage, { - confirmButtonText: 'Go to existing pattern', - }); + + const isConfirmed = await services.openConfirm(confirmMessage, { + confirmButtonText: i18n.translate('kbn.management.indexPattern.goToPatternButtonLabel', { + defaultMessage: 'Go to existing pattern', + }), + }); + + if (isConfirmed) { return services.changeUrl(`/management/kibana/index_patterns/${indexPatternId}`); - } catch (err) { + } else { return false; } } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js index d1087b4575e828..d06bc8784de51e 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js @@ -43,10 +43,10 @@ uiRoutes.when('/management/kibana/index_pattern', { $http: npStart.core.http, savedObjectsClient: npStart.core.savedObjects.client, indexPatternCreationType, - confirmModalPromise: $injector.get('confirmModalPromise'), changeUrl: url => { $scope.$evalAsync(() => kbnUrl.changePath(url)); }, + openConfirm: npStart.core.overlays.openConfirm, }; const initialQuery = $routeParams.id ? decodeURIComponent($routeParams.id) : undefined; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js index eb7358c66e2268..0cbac20a947bfc 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js @@ -198,8 +198,7 @@ uiModules $route, Promise, config, - Private, - confirmModal + Private ) { const { startSyncingState, @@ -290,15 +289,19 @@ uiModules confirmButtonText: i18n.translate('kbn.management.editIndexPattern.refreshButton', { defaultMessage: 'Refresh', }), - onConfirm: async () => { - await $scope.indexPattern.init(true); - $scope.fields = $scope.indexPattern.getNonScriptedFields(); - }, title: i18n.translate('kbn.management.editIndexPattern.refreshHeader', { defaultMessage: 'Refresh field list?', }), }; - confirmModal(confirmMessage, confirmModalOptions); + + npStart.core.overlays + .openConfirm(confirmMessage, confirmModalOptions) + .then(async isConfirmed => { + if (isConfirmed) { + await $scope.indexPattern.init(true); + $scope.fields = $scope.indexPattern.getNonScriptedFields(); + } + }); }; $scope.removePattern = function() { @@ -322,12 +325,16 @@ uiModules confirmButtonText: i18n.translate('kbn.management.editIndexPattern.deleteButton', { defaultMessage: 'Delete', }), - onConfirm: doRemove, title: i18n.translate('kbn.management.editIndexPattern.deleteHeader', { defaultMessage: 'Delete index pattern?', }), }; - confirmModal('', confirmModalOptions); + + npStart.core.overlays.openConfirm('', confirmModalOptions).then(isConfirmed => { + if (isConfirmed) { + doRemove(); + } + }); }; $scope.setDefaultPattern = function() { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js index c16e4cb00c2bdb..e3ab862cd84b7b 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js @@ -38,7 +38,6 @@ function updateObjectsTable($scope, $injector) { const $http = $injector.get('$http'); const kbnUrl = $injector.get('kbnUrl'); const config = $injector.get('config'); - const confirmModalPromise = $injector.get('confirmModalPromise'); const savedObjectsClient = npStart.core.savedObjects.client; const services = savedObjectManagementRegistry.all().map(obj => obj.service); @@ -54,7 +53,7 @@ function updateObjectsTable($scope, $injector) { fatalError(error, location)); } const confirmModalOptions = { - onConfirm: doDelete, confirmButtonText: i18n.translate( 'kbn.management.objects.confirmModalOptions.deleteButtonLabel', { @@ -244,12 +244,19 @@ uiModules defaultMessage: 'Delete saved Kibana object?', }), }; - confirmModal( - i18n.translate('kbn.management.objects.confirmModalOptions.modalDescription', { - defaultMessage: "You can't recover deleted objects", - }), - confirmModalOptions - ); + + overlays + .openConfirm( + i18n.translate('kbn.management.objects.confirmModalOptions.modalDescription', { + defaultMessage: "You can't recover deleted objects", + }), + confirmModalOptions + ) + .then(isConfirmed => { + if (isConfirmed) { + doDelete(); + } + }); }; $scope.submit = function() { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.js index e3cee4186e278e..e13e8c1efe8f75 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.js @@ -79,24 +79,25 @@ async function importIndexPattern(doc, indexPatterns, overwriteAll, confirmModal let newId = await emptyPattern.create(overwriteAll); if (!newId) { // We can override and we want to prompt for confirmation - try { - await confirmModalPromise( - i18n.translate('kbn.management.indexPattern.confirmOverwriteLabel', { - values: { title: this.title }, - defaultMessage: "Are you sure you want to overwrite '{title}'?", + const isConfirmed = await confirmModalPromise( + i18n.translate('kbn.management.indexPattern.confirmOverwriteLabel', { + values: { title: this.title }, + defaultMessage: "Are you sure you want to overwrite '{title}'?", + }), + { + title: i18n.translate('kbn.management.indexPattern.confirmOverwriteTitle', { + defaultMessage: 'Overwrite {type}?', + values: { type }, }), - { - title: i18n.translate('kbn.management.indexPattern.confirmOverwriteTitle', { - defaultMessage: 'Overwrite {type}?', - values: { type }, - }), - confirmButtonText: i18n.translate('kbn.management.indexPattern.confirmOverwriteButton', { - defaultMessage: 'Overwrite', - }), - } - ); + confirmButtonText: i18n.translate('kbn.management.indexPattern.confirmOverwriteButton', { + defaultMessage: 'Overwrite', + }), + } + ); + + if (isConfirmed) { newId = await emptyPattern.create(true); - } catch (err) { + } else { return; } } diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index d3a7f6ac1ff7df..ff7d167ccaacda 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -46,8 +46,6 @@ export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; export { EventsProvider } from 'ui/events'; // @ts-ignore export { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_top_nav'; -// @ts-ignore -export { confirmModalFactory } from 'ui/modals/confirm_modal'; export { registerTimefilterWithGlobalStateFactory } from 'ui/timefilter/setup_router'; // @ts-ignore export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts index 222b0357089767..44e7e9c2a74132 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts @@ -18,7 +18,6 @@ */ import angular, { IModule } from 'angular'; -import { EuiConfirmModal } from '@elastic/eui'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { AppMountContext, LegacyCoreStart } from 'kibana/public'; @@ -26,7 +25,6 @@ import { AppStateProvider, AppState, configureAppAngularModule, - confirmModalFactory, createTopNavDirective, createTopNavHelper, EventsProvider, @@ -93,7 +91,6 @@ function createLocalAngularModule(core: AppMountContext['core'], navigation: Nav createLocalStateModule(); createLocalPersistedStateModule(); createLocalTopNavModule(navigation); - createLocalConfirmModalModule(); const visualizeAngularModule: IModule = angular.module(moduleName, [ ...thirdPartyAngularDependencies, @@ -103,18 +100,10 @@ function createLocalAngularModule(core: AppMountContext['core'], navigation: Nav 'app/visualize/PersistedState', 'app/visualize/TopNav', 'app/visualize/State', - 'app/visualize/ConfirmModal', ]); return visualizeAngularModule; } -function createLocalConfirmModalModule() { - angular - .module('app/visualize/ConfirmModal', ['react']) - .factory('confirmModal', confirmModalFactory) - .directive('confirmModal', reactDirective => reactDirective(EuiConfirmModal)); -} - function createLocalStateModule() { angular .module('app/visualize/State', [ diff --git a/src/legacy/core_plugins/timelion/public/app.js b/src/legacy/core_plugins/timelion/public/app.js index a7fa9e0290a1c5..ff8f75c23435ef 100644 --- a/src/legacy/core_plugins/timelion/public/app.js +++ b/src/legacy/core_plugins/timelion/public/app.js @@ -114,7 +114,6 @@ app.controller('timelion', function( $timeout, AppState, config, - confirmModal, kbnUrl, Private ) { @@ -230,7 +229,6 @@ app.controller('timelion', function( } const confirmModalOptions = { - onConfirm: doDelete, confirmButtonText: i18n.translate('timelion.topNavMenu.delete.modal.confirmButtonLabel', { defaultMessage: 'Delete', }), @@ -241,12 +239,18 @@ app.controller('timelion', function( }; $scope.$evalAsync(() => { - confirmModal( - i18n.translate('timelion.topNavMenu.delete.modal.warningText', { - defaultMessage: `You can't recover deleted sheets.`, - }), - confirmModalOptions - ); + npStart.core.overlays + .openConfirm( + i18n.translate('timelion.topNavMenu.delete.modal.warningText', { + defaultMessage: `You can't recover deleted sheets.`, + }), + confirmModalOptions + ) + .then(isConfirmed => { + if (isConfirmed) { + doDelete(); + } + }); }); }, testId: 'timelionDeleteButton', diff --git a/src/legacy/ui/public/autoload/modules.js b/src/legacy/ui/public/autoload/modules.js index 938796ed279ea0..b40f051a5ec101 100644 --- a/src/legacy/ui/public/autoload/modules.js +++ b/src/legacy/ui/public/autoload/modules.js @@ -23,7 +23,6 @@ import '../config'; import '../notify'; import '../private'; import '../promises'; -import '../modals'; import '../state_management/app_state'; import '../state_management/global_state'; import '../style_compile'; diff --git a/src/legacy/ui/public/modals/__tests__/confirm_modal.js b/src/legacy/ui/public/modals/__tests__/confirm_modal.js deleted file mode 100644 index 6c05fb977c7015..00000000000000 --- a/src/legacy/ui/public/modals/__tests__/confirm_modal.js +++ /dev/null @@ -1,137 +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 angular from 'angular'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import sinon from 'sinon'; -import _ from 'lodash'; - -describe('ui/modals/confirm_modal', function() { - let confirmModal; - let $rootScope; - - beforeEach(function() { - ngMock.module('kibana'); - ngMock.inject(function($injector) { - confirmModal = $injector.get('confirmModal'); - $rootScope = $injector.get('$rootScope'); - }); - }); - - function findByDataTestSubj(dataTestSubj) { - return angular.element(document.body).find(`[data-test-subj=${dataTestSubj}]`); - } - - afterEach(function() { - const confirmButton = findByDataTestSubj('confirmModalConfirmButton'); - if (confirmButton) { - angular.element(confirmButton).click(); - } - }); - - describe('throws an exception', function() { - it('when no custom confirm button passed', function() { - expect(() => confirmModal('hi', { onConfirm: _.noop })).to.throwError(); - }); - - it('when no custom noConfirm function is passed', function() { - expect(() => confirmModal('hi', { confirmButtonText: 'bye' })).to.throwError(); - }); - - it('when showClose is on but title is not given', function() { - const options = { customConfirmButton: 'b', onConfirm: _.noop, showClose: true }; - expect(() => confirmModal('hi', options)).to.throwError(); - }); - }); - - it('shows the message', function() { - const myMessage = 'Hi, how are you?'; - confirmModal(myMessage, { confirmButtonText: 'GREAT!', onConfirm: _.noop }); - - $rootScope.$digest(); - const message = findByDataTestSubj('confirmModalBodyText')[0].innerText.trim(); - expect(message).to.equal(myMessage); - }); - - describe('shows custom text', function() { - const confirmModalOptions = { - confirmButtonText: 'Troodon', - cancelButtonText: 'Dilophosaurus', - title: 'Dinosaurs', - onConfirm: _.noop, - }; - - it('for confirm button', () => { - confirmModal("What's your favorite dinosaur?", confirmModalOptions); - $rootScope.$digest(); - const confirmButtonText = findByDataTestSubj('confirmModalConfirmButton')[0].innerText.trim(); - expect(confirmButtonText).to.equal('Troodon'); - }); - - it('for cancel button', () => { - confirmModal("What's your favorite dinosaur?", confirmModalOptions); - $rootScope.$digest(); - const cancelButtonText = findByDataTestSubj('confirmModalCancelButton')[0].innerText.trim(); - expect(cancelButtonText).to.equal('Dilophosaurus'); - }); - - it('for title text', () => { - confirmModal("What's your favorite dinosaur?", confirmModalOptions); - $rootScope.$digest(); - const titleText = findByDataTestSubj('confirmModalTitleText')[0].innerText.trim(); - expect(titleText).to.equal('Dinosaurs'); - }); - }); - - describe('callbacks are called:', function() { - const confirmCallback = sinon.spy(); - const cancelCallback = sinon.spy(); - - const confirmModalOptions = { - confirmButtonText: 'bye', - onConfirm: confirmCallback, - onCancel: cancelCallback, - title: 'hi', - }; - - beforeEach(() => { - confirmCallback.resetHistory(); - cancelCallback.resetHistory(); - }); - - it('onCancel', function() { - confirmModal('hi', confirmModalOptions); - $rootScope.$digest(); - findByDataTestSubj('confirmModalCancelButton').click(); - - expect(confirmCallback.called).to.be(false); - expect(cancelCallback.called).to.be(true); - }); - - it('onConfirm', function() { - confirmModal('hi', confirmModalOptions); - $rootScope.$digest(); - findByDataTestSubj('confirmModalConfirmButton').click(); - - expect(confirmCallback.called).to.be(true); - expect(cancelCallback.called).to.be(false); - }); - }); -}); diff --git a/src/legacy/ui/public/modals/__tests__/confirm_modal_promise.js b/src/legacy/ui/public/modals/__tests__/confirm_modal_promise.js deleted file mode 100644 index 0967b3caefbbb5..00000000000000 --- a/src/legacy/ui/public/modals/__tests__/confirm_modal_promise.js +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import testSubjSelector from '@kbn/test-subj-selector'; -import ngMock from 'ng_mock'; -import sinon from 'sinon'; -import $ from 'jquery'; - -describe('ui/modals/confirm_modal_promise', function() { - let $rootScope; - let message; - let confirmModalPromise; - let promise; - - beforeEach(function() { - ngMock.module('kibana'); - ngMock.inject(function($injector) { - confirmModalPromise = $injector.get('confirmModalPromise'); - $rootScope = $injector.get('$rootScope'); - }); - - message = 'woah'; - - promise = confirmModalPromise(message, { confirmButtonText: 'click me' }); - }); - - afterEach(function() { - $rootScope.$digest(); - $(testSubjSelector('confirmModalConfirmButton')).click(); - }); - - describe('before timeout completes', function() { - it('returned promise is not resolved', function() { - const callback = sinon.spy(); - promise.then(callback, callback); - $rootScope.$apply(); - expect(callback.called).to.be(false); - }); - }); - - describe('after timeout completes', function() { - it('confirmation dialogue is loaded to dom with message', function() { - $rootScope.$digest(); - const confirmModalElement = $(testSubjSelector('confirmModal')); - expect(confirmModalElement).to.not.be(undefined); - - const htmlString = confirmModalElement[0].innerHTML; - - expect(htmlString.indexOf(message)).to.be.greaterThan(0); - }); - - describe('when confirmed', function() { - it('promise is fulfilled with true', function() { - const confirmCallback = sinon.spy(); - const cancelCallback = sinon.spy(); - - promise.then(confirmCallback, cancelCallback); - $rootScope.$digest(); - const confirmButton = $(testSubjSelector('confirmModalConfirmButton')); - - confirmButton.click(); - expect(confirmCallback.called).to.be(true); - expect(cancelCallback.called).to.be(false); - }); - }); - - describe('when canceled', function() { - it('promise is rejected with false', function() { - const confirmCallback = sinon.spy(); - const cancelCallback = sinon.spy(); - promise.then(confirmCallback, cancelCallback); - - $rootScope.$digest(); - const noButton = $(testSubjSelector('confirmModalCancelButton')); - noButton.click(); - - expect(cancelCallback.called).to.be(true); - expect(confirmCallback.called).to.be(false); - }); - }); - - describe('error is thrown', function() { - it('when no confirm button text is used', function() { - const confirmCallback = sinon.spy(); - const cancelCallback = sinon.spy(); - confirmModalPromise(message).then(confirmCallback, cancelCallback); - - $rootScope.$digest(); - sinon.assert.notCalled(confirmCallback); - sinon.assert.calledOnce(cancelCallback); - sinon.assert.calledWithExactly( - cancelCallback, - sinon.match.has('message', sinon.match(/confirmation button text/)) - ); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/modals/confirm_modal.html b/src/legacy/ui/public/modals/confirm_modal.html deleted file mode 100644 index 3eabe81fe9bd34..00000000000000 --- a/src/legacy/ui/public/modals/confirm_modal.html +++ /dev/null @@ -1,10 +0,0 @@ - diff --git a/src/legacy/ui/public/modals/confirm_modal.js b/src/legacy/ui/public/modals/confirm_modal.js deleted file mode 100644 index c609beff2fb16e..00000000000000 --- a/src/legacy/ui/public/modals/confirm_modal.js +++ /dev/null @@ -1,119 +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 angular from 'angular'; -import { i18n } from '@kbn/i18n'; -import { noop } from 'lodash'; -import { uiModules } from '../modules'; -import template from './confirm_modal.html'; -import { ModalOverlay } from './modal_overlay'; - -const module = uiModules.get('kibana'); - -import { - EUI_MODAL_CONFIRM_BUTTON as CONFIRM_BUTTON, - EUI_MODAL_CANCEL_BUTTON as CANCEL_BUTTON, -} from '@elastic/eui'; - -export const ConfirmationButtonTypes = { - CONFIRM: CONFIRM_BUTTON, - CANCEL: CANCEL_BUTTON, -}; - -export function confirmModalFactory($rootScope, $compile) { - let modalPopover; - const confirmQueue = []; - - /** - * @param {String} message - the message to show in the body of the confirmation dialog. - * @param {ConfirmModalOptions} - Options to further customize the dialog. - */ - return function confirmModal(message, customOptions) { - const defaultOptions = { - onCancel: noop, - cancelButtonText: i18n.translate('common.ui.modals.cancelButtonLabel', { - defaultMessage: 'Cancel', - }), - defaultFocusedButton: ConfirmationButtonTypes.CONFIRM, - }; - - if (!customOptions.confirmButtonText || !customOptions.onConfirm) { - throw new Error('Please specify confirmation button text and onConfirm action'); - } - - const options = Object.assign(defaultOptions, customOptions); - - // Special handling for onClose - if no specific callback was supplied, default to the - // onCancel callback. - options.onClose = customOptions.onClose || options.onCancel; - - const confirmScope = $rootScope.$new(); - - confirmScope.message = message; - confirmScope.defaultFocusedButton = options.defaultFocusedButton; - confirmScope.confirmButtonText = options.confirmButtonText; - confirmScope.cancelButtonText = options.cancelButtonText; - confirmScope.title = options.title; - confirmScope.onConfirm = () => { - destroy(); - options.onConfirm(); - }; - confirmScope.onCancel = () => { - destroy(); - options.onCancel(); - }; - confirmScope.onClose = () => { - destroy(); - options.onClose(); - }; - - function showModal(confirmScope) { - const modalInstance = $compile(template)(confirmScope); - modalPopover = new ModalOverlay(modalInstance); - } - - if (modalPopover) { - confirmQueue.unshift(confirmScope); - } else { - showModal(confirmScope); - } - - function destroy() { - modalPopover.destroy(); - modalPopover = undefined; - angular.element(document.body).off('keydown'); - confirmScope.$destroy(); - - if (confirmQueue.length > 0) { - showModal(confirmQueue.pop()); - } - } - }; -} - -/** - * @typedef {Object} ConfirmModalOptions - * @property {String} confirmButtonText - * @property {String=} cancelButtonText - * @property {function} onConfirm - * @property {function=} onCancel - * @property {String=} title - If given, shows a title on the confirm modal. - */ - -module.factory('confirmModal', confirmModalFactory); diff --git a/src/legacy/ui/public/modals/confirm_modal_promise.js b/src/legacy/ui/public/modals/confirm_modal_promise.js deleted file mode 100644 index 54f568e80bff08..00000000000000 --- a/src/legacy/ui/public/modals/confirm_modal_promise.js +++ /dev/null @@ -1,49 +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 { uiModules } from '../modules'; -import './'; - -const module = uiModules.get('kibana'); - -/** - * @typedef {Object} PromisifiedConfirmOptions - * @property {String} confirmButtonText - * @property {String=} cancelButtonText - */ - -/** - * A "promisified" version of ConfirmModal that binds onCancel and onConfirm to - * Resolve and Reject methods. - */ -module.factory('confirmModalPromise', function(Promise, confirmModal) { - /** - * @param {String} message - * @param {PromisifiedConfirmOptions} customOptions - */ - return (message, customOptions) => - new Promise((resolve, reject) => { - const defaultOptions = { - onConfirm: resolve, - onCancel: reject, - }; - const confirmOptions = Object.assign(defaultOptions, customOptions); - confirmModal(message, confirmOptions); - }); -}); diff --git a/src/legacy/ui/public/modals/index.js b/src/legacy/ui/public/modals/index.js deleted file mode 100644 index d0612643459593..00000000000000 --- a/src/legacy/ui/public/modals/index.js +++ /dev/null @@ -1,24 +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 './confirm_modal'; -import './confirm_modal_promise'; - -export { ConfirmationButtonTypes } from './confirm_modal'; -export { ModalOverlay } from './modal_overlay'; diff --git a/src/legacy/ui/public/modals/modal_overlay.html b/src/legacy/ui/public/modals/modal_overlay.html deleted file mode 100644 index 2abc5768f46f16..00000000000000 --- a/src/legacy/ui/public/modals/modal_overlay.html +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/src/legacy/ui/public/modals/modal_overlay.js b/src/legacy/ui/public/modals/modal_overlay.js deleted file mode 100644 index 6ddecee9f2f711..00000000000000 --- a/src/legacy/ui/public/modals/modal_overlay.js +++ /dev/null @@ -1,40 +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 angular from 'angular'; -import modalOverlayTemplate from './modal_overlay.html'; - -/** - * Appends the modal to the dom on instantiation, and removes it when destroy is called. - */ -export class ModalOverlay { - constructor(modalElement) { - this.overlayElement = angular.element(modalOverlayTemplate); - this.overlayElement.append(modalElement); - - angular.element(document.body).append(this.overlayElement); - } - - /** - * Removes the overlay and modal from the dom. - */ - destroy() { - this.overlayElement.remove(); - } -} diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 985dbc78e2f77a..47ef690c4f83e7 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -176,7 +176,11 @@ let isAutoRefreshSelectorEnabled = true; export const npStart = { core: { - chrome: {}, + chrome: { + overlays: { + openModal: sinon.fake(), + }, + }, }, plugins: { management: { diff --git a/src/legacy/ui/public/react_components.js b/src/legacy/ui/public/react_components.js index fea25d2c71da3a..b771e37c9d538d 100644 --- a/src/legacy/ui/public/react_components.js +++ b/src/legacy/ui/public/react_components.js @@ -19,14 +19,12 @@ import 'ngreact'; -import { EuiConfirmModal, EuiIcon, EuiIconTip } from '@elastic/eui'; +import { EuiIcon, EuiIconTip } from '@elastic/eui'; import { uiModules } from './modules'; const app = uiModules.get('app/kibana', ['react']); -app.directive('confirmModal', reactDirective => reactDirective(EuiConfirmModal)); - app.directive('icon', reactDirective => reactDirective(EuiIcon)); app.directive('iconTip', reactDirective => diff --git a/src/legacy/ui/public/saved_objects/__tests__/saved_object.js b/src/legacy/ui/public/saved_objects/__tests__/saved_object.js index ace87a15f7b585..d027d8b6c99da4 100644 --- a/src/legacy/ui/public/saved_objects/__tests__/saved_object.js +++ b/src/legacy/ui/public/saved_objects/__tests__/saved_object.js @@ -26,7 +26,7 @@ import { createSavedObjectClass } from '../saved_object'; import StubIndexPattern from 'test_utils/stub_index_pattern'; import { npStart } from 'ui/new_platform'; import { InvalidJSONProperty } from '../../../../../plugins/kibana_utils/public'; -import { npSetup } from '../../new_platform/new_platform.karma_mock'; +import { npSetup, npStart as npStartMock } from '../../new_platform/new_platform.karma_mock'; const getConfig = cfg => cfg; @@ -89,18 +89,12 @@ describe('Saved Object', function() { obj[fName].restore && obj[fName].restore(); } - beforeEach( - ngMock.module( - 'kibana', - // Use the native window.confirm instead of our specialized version to make testing - // this easier. - function($provide) { - const overrideConfirm = message => - window.confirm(message) ? Promise.resolve() : Promise.reject(); - $provide.decorator('confirmModalPromise', () => overrideConfirm); - } - ) - ); + beforeEach(() => { + // Use the native window.confirm instead of our specialized version to make testing + // this easier. + npStartMock.core.overlays.openModal = message => + window.confirm(message) ? Promise.resolve() : Promise.reject(); + }); beforeEach( ngMock.inject(function($window) { diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index 38a601daa178e3..7010e1fa773ea8 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -49,6 +49,7 @@ export function initGraphApp(angularModule, deps) { storage, canEditDrillDownUrls, graphSavePolicy, + overlays, } = deps; const app = angularModule; @@ -162,7 +163,7 @@ export function initGraphApp(angularModule, deps) { }); //======== Controller for basic UI ================== - app.controller('graphuiPlugin', function($scope, $route, $location, confirmModal) { + app.controller('graphuiPlugin', function($scope, $route, $location) { function handleError(err) { const toastTitle = i18n.translate('xpack.graph.errorToastTitle', { defaultMessage: 'Graph Error', @@ -382,23 +383,29 @@ export function initGraphApp(angularModule, deps) { return; } const confirmModalOptions = { - onConfirm: callback, - onCancel: () => {}, confirmButtonText: i18n.translate('xpack.graph.leaveWorkspace.confirmButtonLabel', { defaultMessage: 'Leave anyway', }), title: i18n.translate('xpack.graph.leaveWorkspace.modalTitle', { defaultMessage: 'Unsaved changes', }), + 'data-test-subj': 'confirmModal', ...options, }; - confirmModal( - text || - i18n.translate('xpack.graph.leaveWorkspace.confirmText', { - defaultMessage: 'If you leave now, you will lose unsaved changes.', - }), - confirmModalOptions - ); + + overlays + .openConfirm( + text || + i18n.translate('xpack.graph.leaveWorkspace.confirmText', { + defaultMessage: 'If you leave now, you will lose unsaved changes.', + }), + confirmModalOptions + ) + .then(isConfirmed => { + if (isConfirmed) { + callback(); + } + }); } $scope.confirmWipeWorkspace = canWipeWorkspace; diff --git a/x-pack/legacy/plugins/graph/public/application.ts b/x-pack/legacy/plugins/graph/public/application.ts index 8f486ab6ad51a2..80a797b7f07243 100644 --- a/x-pack/legacy/plugins/graph/public/application.ts +++ b/x-pack/legacy/plugins/graph/public/application.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiConfirmModal } from '@elastic/eui'; - // inner angular imports // these are necessary to bootstrap the local angular. // They can stay even after NP cutover @@ -20,12 +18,12 @@ import { SavedObjectsClientContract, ToastsStart, IUiSettingsClient, + OverlayStart, } from 'kibana/public'; import { configureAppAngularModule, createTopNavDirective, createTopNavHelper, - confirmModalFactory, addAppRedirectMessageToUrl, } from './legacy_imports'; // @ts-ignore @@ -64,6 +62,7 @@ export interface GraphDependencies { storage: Storage; canEditDrillDownUrls: boolean; graphSavePolicy: string; + overlays: OverlayStart; } export const renderApp = ({ appBasePath, element, ...deps }: GraphDependencies) => { @@ -120,24 +119,15 @@ function mountGraphApp(appBasePath: string, element: HTMLElement) { function createLocalAngularModule(navigation: NavigationStart) { createLocalI18nModule(); createLocalTopNavModule(navigation); - createLocalConfirmModalModule(); const graphAngularModule = angular.module(moduleName, [ ...thirdPartyAngularDependencies, 'graphI18n', 'graphTopNav', - 'graphConfirmModal', ]); return graphAngularModule; } -function createLocalConfirmModalModule() { - angular - .module('graphConfirmModal', ['react']) - .factory('confirmModal', confirmModalFactory) - .directive('confirmModal', reactDirective => reactDirective(EuiConfirmModal)); -} - function createLocalTopNavModule(navigation: NavigationStart) { angular .module('graphTopNav', ['react']) diff --git a/x-pack/legacy/plugins/graph/public/legacy_imports.ts b/x-pack/legacy/plugins/graph/public/legacy_imports.ts index f1839d62a06675..73a96016054fcf 100644 --- a/x-pack/legacy/plugins/graph/public/legacy_imports.ts +++ b/x-pack/legacy/plugins/graph/public/legacy_imports.ts @@ -11,8 +11,6 @@ export { SavedObject, SavedObjectKibanaServices } from 'ui/saved_objects/types'; // @ts-ignore export { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_top_nav'; // @ts-ignore -export { confirmModalFactory } from 'ui/modals/confirm_modal'; -// @ts-ignore export { addAppRedirectMessageToUrl } from 'ui/notify'; export { createSavedObjectClass } from 'ui/saved_objects/saved_object'; export { configureAppAngularModule } from '../../../../../src/plugins/kibana_legacy/public'; diff --git a/x-pack/legacy/plugins/graph/public/plugin.ts b/x-pack/legacy/plugins/graph/public/plugin.ts index ab610d76be1016..48758ee1ec7703 100644 --- a/x-pack/legacy/plugins/graph/public/plugin.ts +++ b/x-pack/legacy/plugins/graph/public/plugin.ts @@ -50,6 +50,7 @@ export class GraphPlugin implements Plugin { config: contextCore.uiSettings, toastNotifications: contextCore.notifications.toasts, indexPatterns: this.npDataStart!.indexPatterns, + overlays: contextCore.overlays, }); }, }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5d45a275ede116..e9a5d9611c8063 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -352,7 +352,6 @@ "common.ui.flotCharts.thuLabel": "木", "common.ui.flotCharts.tueLabel": "火", "common.ui.flotCharts.wedLabel": "水", - "common.ui.modals.cancelButtonLabel": "キャンセル", "common.ui.paginateControls.pageSizeLabel": "ページサイズ", "common.ui.paginateControls.scrollTopButtonLabel": "最上部に移動", "common.ui.savedObjects.confirmModal.overwriteButtonLabel": "上書き", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6bbb3e59b25e33..201e3c35ee2829 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -352,7 +352,6 @@ "common.ui.flotCharts.thuLabel": "周四", "common.ui.flotCharts.tueLabel": "周二", "common.ui.flotCharts.wedLabel": "周三", - "common.ui.modals.cancelButtonLabel": "取消", "common.ui.paginateControls.pageSizeLabel": "页面大小", "common.ui.paginateControls.scrollTopButtonLabel": "滚动至顶部", "common.ui.savedObjects.confirmModal.overwriteButtonLabel": "覆盖", From 09f1cad573e7683ac0b6548966ba1c5bc3de49ec Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 10 Feb 2020 12:48:18 +0100 Subject: [PATCH 14/19] Check for legacy imports in vis types and fix problems (#56763) --- .eslintignore | 1 - .eslintrc.js | 2 + src/dev/precommit_hook/casing_check_config.js | 4 - .../public/discover/get_inner_angular.ts | 3 +- .../public/metric_vis_type.test.ts | 3 + .../public/get_inner_angular.ts | 4 +- .../paginated_table/paginated_table.test.ts | 10 +- .../vis_type_table/public/table_vis.mock.ts | 46 -- .../public/table_vis_controller.test.ts | 66 +-- .../public/components/tag_cloud_options.tsx | 2 +- .../components/tag_cloud_visualization.js | 8 +- .../public/legacy_imports.ts | 2 + .../lib/get_default_query_language.js | 4 +- .../public/components/lib/tick_formatter.js | 4 +- .../components/lib/tick_formatter.test.js | 6 +- .../components/panel_config/gauge.test.js | 4 + .../public/components/vis_editor.js | 14 +- .../public/components/vis_types/table/vis.js | 4 +- .../components/vis_types/timeseries/vis.js | 3 +- .../vis_type_timeseries/public/legacy.ts | 2 +- .../public/legacy_imports.ts} | 6 +- .../public/lib/fetch_fields.js | 9 +- .../vis_type_timeseries/public/metrics_fn.ts | 2 +- .../public/metrics_type.ts | 3 +- .../vis_type_timeseries/public/plugin.ts | 32 +- .../public/request_handler.js | 12 +- .../vis_type_timeseries/public/services.ts | 11 +- .../visualizations/views/timeseries/index.js | 6 +- .../public/__mocks__/services.ts | 55 +++ .../public/__tests__/vega_visualization.js | 3 + .../public/components/vega_vis_editor.tsx | 4 +- .../public/data_model/es_query_parser.js | 2 +- .../public/data_model/es_query_parser.test.js | 6 +- .../public/data_model/search_cache.test.js | 1 + .../public/data_model/time_cache.test.js | 1 + .../public/data_model/vega_parser.test.js | 1 + ...a_config_provider.js => legacy_imports.ts} | 10 +- .../vis_type_vega/public/plugin.ts | 15 +- .../vis_type_vega/public/services.ts | 13 + .../public/shim/legacy_dependencies_plugin.ts | 3 + .../vis_type_vega/public/vega_type.ts | 2 +- .../public/vega_view/vega_base_view.js | 19 +- .../public/vega_view/vega_map_layer.js | 2 +- .../public/vega_view/vega_map_view.js | 10 +- .../vislib/__tests__/response_handlers.js | 1 + .../angular-bootstrap/bindHtml/bindHtml.js | 10 - .../ui/public/angular-bootstrap/index.js | 47 -- .../angular-bootstrap/tooltip/position.js | 152 ------- .../angular-bootstrap/tooltip/tooltip.js | 374 ---------------- .../angular_bootstrap/bind_html/bind_html.js | 17 + .../public/angular_bootstrap/index.ts | 50 +++ .../angular_bootstrap/tooltip/position.js | 167 +++++++ .../angular_bootstrap/tooltip/tooltip.js | 423 ++++++++++++++++++ .../tooltip/tooltip_html_unsafe_popup.html} | 0 .../tooltip/tooltip_popup.html} | 0 src/plugins/kibana_legacy/public/index.ts | 2 + .../plugins/graph/public/legacy_imports.ts | 1 - x-pack/legacy/plugins/graph/public/plugin.ts | 2 + 58 files changed, 901 insertions(+), 765 deletions(-) delete mode 100644 src/legacy/core_plugins/vis_type_table/public/table_vis.mock.ts rename src/legacy/core_plugins/{vis_type_vega/public/helpers/index.js => vis_type_timeseries/public/legacy_imports.ts} (79%) create mode 100644 src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts rename src/legacy/core_plugins/vis_type_vega/public/{helpers/vega_config_provider.js => legacy_imports.ts} (77%) delete mode 100755 src/legacy/ui/public/angular-bootstrap/bindHtml/bindHtml.js delete mode 100644 src/legacy/ui/public/angular-bootstrap/index.js delete mode 100755 src/legacy/ui/public/angular-bootstrap/tooltip/position.js delete mode 100755 src/legacy/ui/public/angular-bootstrap/tooltip/tooltip.js create mode 100755 src/plugins/kibana_legacy/public/angular_bootstrap/bind_html/bind_html.js create mode 100644 src/plugins/kibana_legacy/public/angular_bootstrap/index.ts create mode 100755 src/plugins/kibana_legacy/public/angular_bootstrap/tooltip/position.js create mode 100755 src/plugins/kibana_legacy/public/angular_bootstrap/tooltip/tooltip.js rename src/{legacy/ui/public/angular-bootstrap/tooltip/tooltip-html-unsafe-popup.html => plugins/kibana_legacy/public/angular_bootstrap/tooltip/tooltip_html_unsafe_popup.html} (100%) rename src/{legacy/ui/public/angular-bootstrap/tooltip/tooltip-popup.html => plugins/kibana_legacy/public/angular_bootstrap/tooltip/tooltip_popup.html} (100%) diff --git a/.eslintignore b/.eslintignore index 86a01b68ecab1f..c3921bd22e1ab7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -11,7 +11,6 @@ bower_components /src/plugins/data/common/es_query/kuery/ast/_generated_/** /src/legacy/core_plugins/vis_type_timelion/public/_generated_/** src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data -/src/legacy/ui/public/angular-bootstrap /src/legacy/ui/public/flot-charts /test/fixtures/scenarios /src/legacy/core_plugins/console/public/webpackShims diff --git a/.eslintrc.js b/.eslintrc.js index 199f3743fd621e..abfe5e0a6cc270 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -302,6 +302,8 @@ module.exports = { 'test/plugin_functional/plugins/**/public/np_ready/**/*', 'test/plugin_functional/plugins/**/server/np_ready/**/*', 'src/legacy/core_plugins/**/public/np_ready/**/*', + 'src/legacy/core_plugins/vis_type_*/public/**/*', + '!src/legacy/core_plugins/vis_type_*/public/legacy*', 'src/legacy/core_plugins/**/server/np_ready/**/*', 'x-pack/legacy/plugins/**/public/np_ready/**/*', 'x-pack/legacy/plugins/**/server/np_ready/**/*', diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 78fc041345577e..ef114f51f31007 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -88,7 +88,6 @@ export const IGNORE_DIRECTORY_GLOBS = [ 'src/babel-*', 'packages/*', 'packages/kbn-ui-framework/generator-kui', - 'src/legacy/ui/public/angular-bootstrap', 'src/legacy/ui/public/flot-charts', 'src/legacy/ui/public/utils/lodash-mixins', 'test/functional/fixtures/es_archiver/visualize_source-filters', @@ -124,9 +123,6 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'src/legacy/core_plugins/timelion/server/series_functions/__tests__/fixtures/tlConfig.js', 'src/fixtures/config_upgrade_from_4.0.0_to_4.0.1-snapshot.json', 'src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_seriesMultiple.js', - 'src/legacy/ui/public/angular-bootstrap/bindHtml/bindHtml.js', - 'src/legacy/ui/public/angular-bootstrap/tooltip/tooltip-html-unsafe-popup.html', - 'src/legacy/ui/public/angular-bootstrap/tooltip/tooltip-popup.html', 'src/legacy/ui/public/assets/favicons/android-chrome-192x192.png', 'src/legacy/ui/public/assets/favicons/android-chrome-256x256.png', 'src/legacy/ui/public/assets/favicons/android-chrome-512x512.png', diff --git a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts index cc4dabd123ff44..eb6d7e6467f2f6 100644 --- a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts +++ b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts @@ -21,7 +21,6 @@ // these are necessary to bootstrap the local angular. // They can stay even after NP cutover import angular from 'angular'; -import 'ui/angular-bootstrap'; import { EuiIcon } from '@elastic/eui'; // @ts-ignore import { StateProvider } from 'ui/state_management/state'; @@ -64,6 +63,7 @@ import { createFieldChooserDirective } from './np_ready/components/field_chooser import { createDiscoverFieldDirective } from './np_ready/components/field_chooser/discover_field'; import { CollapsibleSidebarProvider } from './np_ready/angular/directives/collapsible_sidebar/collapsible_sidebar'; import { DiscoverStartPlugins } from './plugin'; +import { initAngularBootstrap } from '../../../../../plugins/kibana_legacy/public'; import { createCssTruncateDirective } from './np_ready/angular/directives/css_truncate'; // @ts-ignore import { FixedScrollProvider } from './np_ready/angular/directives/fixed_scroll'; @@ -85,6 +85,7 @@ import { * needs to render, so in the end the current 'kibana' angular module is no longer necessary */ export function getInnerAngularModule(name: string, core: CoreStart, deps: DiscoverStartPlugins) { + initAngularBootstrap(); const module = initializeInnerAngularModule(name, core, deps.navigation, deps.data); configureAppAngularModule(module, core as LegacyCoreStart, true); return module; diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts index 4f535c62f56a09..28565e0181b84d 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts @@ -19,6 +19,9 @@ import $ from 'jquery'; +// TODO This is an integration test and thus requires a running platform. When moving to the new platform, +// this test has to be migrated to the newly created integration test environment. +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { npStart } from 'ui/new_platform'; // @ts-ignore import getStubIndexPattern from 'fixtures/stubbed_logstash_index_pattern'; diff --git a/src/legacy/core_plugins/vis_type_table/public/get_inner_angular.ts b/src/legacy/core_plugins/vis_type_table/public/get_inner_angular.ts index 9f3a8327c9ad94..18d8e7bc9d8bb9 100644 --- a/src/legacy/core_plugins/vis_type_table/public/get_inner_angular.ts +++ b/src/legacy/core_plugins/vis_type_table/public/get_inner_angular.ts @@ -21,7 +21,6 @@ // these are necessary to bootstrap the local angular. // They can stay even after NP cutover import angular from 'angular'; -import 'ui/angular-bootstrap'; import 'angular-recursion'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { CoreStart, LegacyCoreStart, IUiSettingsClient } from 'kibana/public'; @@ -34,6 +33,9 @@ import { StateManagementConfigProvider, configureAppAngularModule, } from './legacy_imports'; +import { initAngularBootstrap } from '../../../../plugins/kibana_legacy/public'; + +initAngularBootstrap(); const thirdPartyAngularDependencies = ['ngSanitize', 'ui.bootstrap', 'RecursionHelper']; diff --git a/src/legacy/core_plugins/vis_type_table/public/paginated_table/paginated_table.test.ts b/src/legacy/core_plugins/vis_type_table/public/paginated_table/paginated_table.test.ts index 781782e42fbaf2..7352236f03febf 100644 --- a/src/legacy/core_plugins/vis_type_table/public/paginated_table/paginated_table.test.ts +++ b/src/legacy/core_plugins/vis_type_table/public/paginated_table/paginated_table.test.ts @@ -22,11 +22,15 @@ import angular, { IRootScopeService, IScope, ICompileService } from 'angular'; import $ from 'jquery'; import 'angular-sanitize'; import 'angular-mocks'; -import '../table_vis.mock'; import { getAngularModule } from '../get_inner_angular'; import { initTableVisLegacyModule } from '../table_vis_legacy_module'; -import { npStart } from '../legacy_imports'; +import { coreMock } from '../../../../../core/public/mocks'; + +jest.mock('ui/new_platform'); +jest.mock('../../../../../plugins/kibana_legacy/public/angular/angular_config', () => ({ + configureAppAngularModule: () => {}, +})); interface Sort { columnIndex: number; @@ -69,7 +73,7 @@ describe('Table Vis - Paginated table', () => { let paginatedTable: any; const initLocalAngular = () => { - const tableVisModule = getAngularModule('kibana/table_vis', npStart.core); + const tableVisModule = getAngularModule('kibana/table_vis', coreMock.createStart()); initTableVisLegacyModule(tableVisModule); }; diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis.mock.ts b/src/legacy/core_plugins/vis_type_table/public/table_vis.mock.ts deleted file mode 100644 index d04964cb7af038..00000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis.mock.ts +++ /dev/null @@ -1,46 +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 { createUiNewPlatformMock } from 'ui/new_platform/__mocks__/helpers'; -import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; -import { injectedMetadataServiceMock } from '../../../../core/public/mocks'; - -jest.doMock('ui/new_platform', () => { - const npMock = createUiNewPlatformMock(); - return { - npSetup: { - ...npMock.npSetup, - core: { - ...npMock.npSetup.core, - injectedMetadata: injectedMetadataServiceMock.createSetupContract(), - }, - }, - npStart: { - ...npMock.npStart, - core: { - ...npMock.npStart.core, - injectedMetadata: injectedMetadataServiceMock.createStartContract(), - }, - }, - }; -}); - -Object.assign(window, { - sessionStorage: new StubBrowserStorage(), -}); diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts b/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts index d8912975227bfb..0e1e48d00a1b27 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts +++ b/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts @@ -21,21 +21,26 @@ import angular, { IRootScopeService, IScope, ICompileService } from 'angular'; import 'angular-mocks'; import 'angular-sanitize'; import $ from 'jquery'; -import './table_vis.mock'; // @ts-ignore import StubIndexPattern from 'test_utils/stub_index_pattern'; import { getAngularModule } from './get_inner_angular'; import { initTableVisLegacyModule } from './table_vis_legacy_module'; -import { npStart, IAggConfig, tabifyAggResponse } from './legacy_imports'; import { tableVisTypeDefinition } from './table_vis_type'; import { Vis } from '../../visualizations/public'; -import { setup as visualizationsSetup } from '../../visualizations/public/np_ready/public/legacy'; // eslint-disable-next-line import { stubFields } from '../../../../plugins/data/public/stubs'; // eslint-disable-next-line -import { setFieldFormats } from '../../../../plugins/data/public/services'; import { tableVisResponseHandler } from './table_vis_response_handler'; +import { coreMock } from '../../../../core/public/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AggConfigs } from 'ui/agg_types'; +import { tabifyAggResponse, IAggConfig } from './legacy_imports'; + +jest.mock('ui/new_platform'); +jest.mock('../../../../plugins/kibana_legacy/public/angular/angular_config', () => ({ + configureAppAngularModule: () => {}, +})); interface TableVisScope extends IScope { [key: string]: any; @@ -79,14 +84,11 @@ describe('Table Vis - Controller', () => { let stubIndexPattern: any; const initLocalAngular = () => { - const tableVisModule = getAngularModule('kibana/table_vis', npStart.core); + const tableVisModule = getAngularModule('kibana/table_vis', coreMock.createStart()); initTableVisLegacyModule(tableVisModule); }; beforeEach(initLocalAngular); - beforeAll(() => { - visualizationsSetup.types.createBaseVisualization(tableVisTypeDefinition); - }); beforeEach(angular.mock.module('kibana/table_vis')); beforeEach( @@ -98,38 +100,38 @@ describe('Table Vis - Controller', () => { ); beforeEach(() => { - setFieldFormats(({ - getDefaultInstance: jest.fn(), - } as unknown) as any); stubIndexPattern = new StubIndexPattern( 'logstash-*', (cfg: any) => cfg, 'time', stubFields, - npStart.core + coreMock.createStart() ); }); function getRangeVis(params?: object) { - // @ts-ignore - return new Vis(stubIndexPattern, { - type: 'table', - params: params || {}, - aggs: [ - { type: 'count', schema: 'metric' }, - { - type: 'range', - schema: 'bucket', - params: { - field: 'bytes', - ranges: [ - { from: 0, to: 1000 }, - { from: 1000, to: 2000 }, - ], + return ({ + type: tableVisTypeDefinition, + params: Object.assign({}, tableVisTypeDefinition.visConfig.defaults, params), + aggs: new AggConfigs( + stubIndexPattern, + [ + { type: 'count', schema: 'metric' }, + { + type: 'range', + schema: 'bucket', + params: { + field: 'bytes', + ranges: [ + { from: 0, to: 1000 }, + { from: 1000, to: 2000 }, + ], + }, }, - }, - ], - }); + ], + tableVisTypeDefinition.editorConfig.schemas.all + ), + } as unknown) as Vis; } const dimensions = { @@ -241,13 +243,13 @@ describe('Table Vis - Controller', () => { const vis = getRangeVis({ showPartialRows: true }); initController(vis); - expect(vis.isHierarchical()).toEqual(true); + expect(vis.type.hierarchicalData(vis)).toEqual(true); }); test('passes partialRows:false to tabify based on the vis params', () => { const vis = getRangeVis({ showPartialRows: false }); initController(vis); - expect(vis.isHierarchical()).toEqual(false); + expect(vis.type.hierarchicalData(vis)).toEqual(false); }); }); diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx index eed5ffe8c35849..ab7c2cd980c424 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx @@ -21,10 +21,10 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ValidatedDualRange } from 'ui/validated_range'; import { VisOptionsProps } from '../../../vis_default_editor/public'; import { SelectOption, SwitchOption } from '../../../vis_type_vislib/public'; import { TagCloudVisParams } from '../types'; +import { ValidatedDualRange } from '../legacy_imports'; function TagCloudOptions({ stateParams, setValue, vis }: VisOptionsProps) { const handleFontSizeChange = ([minFontSize, maxFontSize]: [string | number, string | number]) => { diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js index f2163abbbc723d..5528278adf4eb3 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js @@ -21,9 +21,9 @@ import React from 'react'; import * as Rx from 'rxjs'; import { take } from 'rxjs/operators'; import { render, unmountComponentAtNode } from 'react-dom'; +import { I18nProvider } from '@kbn/i18n/react'; -import { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; -import { I18nContext } from 'ui/i18n'; +import { getFormat } from '../legacy_imports'; import { Label } from './label'; import { TagCloud } from './tag_cloud'; @@ -65,9 +65,9 @@ export function createTagCloudVisualization({ colors }) { this._containerNode.appendChild(this._feedbackNode); this._feedbackMessage = React.createRef(); render( - + - , + , this._feedbackNode ); diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts index ecc56ea0c34be4..d5b442bc5b3468 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts @@ -18,3 +18,5 @@ */ export { Schemas } from 'ui/agg_types'; +export { ValidatedDualRange } from 'ui/validated_range'; +export { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_default_query_language.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_default_query_language.js index 61662787c982de..26723da5ab5c99 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_default_query_language.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/get_default_query_language.js @@ -17,8 +17,8 @@ * under the License. */ -import chrome from 'ui/chrome'; +import { getUISettings } from '../../services'; export function getDefaultQueryLanguage() { - return chrome.getUiSettingsClient().get('search:queryLanguage'); + return getUISettings().get('search:queryLanguage'); } diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.js index 0705805312d2f4..3ab8e0f6b885e3 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.js @@ -19,11 +19,11 @@ import handlebars from 'handlebars/dist/handlebars'; import { isNumber } from 'lodash'; -import { npStart } from 'ui/new_platform'; import { inputFormats, outputFormats, isDuration } from '../lib/durations'; +import { getFieldFormats } from '../../services'; export const createTickFormatter = (format = '0,0.[00]', template, getConfig = null) => { - const fieldFormats = npStart.plugins.data.fieldFormats; + const fieldFormats = getFieldFormats(); if (!template) template = '{{value}}'; const render = handlebars.compile(template, { knownHelpersOnly: true }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.test.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.test.js index 76d3cff17343e1..e87cba126bb46a 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.test.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/lib/tick_formatter.test.js @@ -17,9 +17,9 @@ * under the License. */ -import { npStart } from 'ui/new_platform'; import { createTickFormatter } from './tick_formatter'; import { getFieldFormatsRegistry } from '../../../../../../test_utils/public/stub_field_formats'; +import { setFieldFormats } from '../../services'; const mockUiSettings = { get: item => { @@ -46,9 +46,7 @@ const mockCore = { }; describe('createTickFormatter(format, template)', () => { - npStart.plugins.data = { - fieldFormats: getFieldFormatsRegistry(mockCore), - }; + setFieldFormats(getFieldFormatsRegistry(mockCore)); test('returns a number with two decimal place by default', () => { const fn = createTickFormatter(); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.test.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.test.js index 9ec8184dbaebb6..d92dafadb68bc0 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.test.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.test.js @@ -26,6 +26,10 @@ jest.mock('plugins/data', () => { }; }); +jest.mock('../lib/get_default_query_language', () => ({ + getDefaultQueryLanguage: () => 'kuery', +})); + import { GaugePanelConfig } from './gauge'; describe('GaugePanelConfig', () => { diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js index 3dedb67bd1d99a..b2dd1813e6d208 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js @@ -30,13 +30,11 @@ import { createBrushHandler } from '../lib/create_brush_handler'; import { fetchFields } from '../lib/fetch_fields'; import { extractIndexPatterns } from '../../../../../plugins/vis_type_timeseries/common/extract_index_patterns'; import { esKuery } from '../../../../../plugins/data/public'; - -import { npStart } from 'ui/new_platform'; +import { getSavedObjectsClient, getUISettings, getDataStart, getCoreStart } from '../services'; import { CoreStartContextProvider } from '../contexts/query_input_bar_context'; import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; -import { timefilter } from 'ui/timefilter'; const VIS_STATE_DEBOUNCE_DELAY = 200; const APP_NAME = 'VisEditor'; @@ -52,7 +50,7 @@ export class VisEditor extends Component { visFields: props.visFields, extractedIndexPatterns: [''], }; - this.onBrush = createBrushHandler(timefilter); + this.onBrush = createBrushHandler(getDataStart().query.timefilter); this.visDataSubject = new Rx.BehaviorSubject(this.props.visData); this.visData$ = this.visDataSubject.asObservable().pipe(share()); @@ -60,8 +58,8 @@ export class VisEditor extends Component { // core dependencies required by React components downstream. this.coreContext = { appName: APP_NAME, - uiSettings: npStart.core.uiSettings, - savedObjectsClient: npStart.core.savedObjects.client, + uiSettings: getUISettings(), + savedObjectsClient: getSavedObjectsClient(), store: this.localStorage, }; } @@ -175,8 +173,8 @@ export class VisEditor extends Component { services={{ appName: APP_NAME, storage: this.localStorage, - data: npStart.plugins.data, - ...npStart.core, + data: getDataStart(), + ...getCoreStart(), }} >
diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/vis.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/vis.js index 94f4506cd01726..1fe9358cbfea9f 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/vis.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/vis.js @@ -20,7 +20,6 @@ import _, { isArray, last, get } from 'lodash'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { npStart } from 'ui/new_platform'; import { createTickFormatter } from '../../lib/tick_formatter'; import { calculateLabel } from '../../../../../../../plugins/vis_type_timeseries/common/calculate_label'; import { isSortable } from './is_sortable'; @@ -28,6 +27,7 @@ import { EuiToolTip, EuiIcon } from '@elastic/eui'; import { replaceVars } from '../../lib/replace_vars'; import { fieldFormats } from '../../../../../../../plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; +import { getFieldFormats } from '../../../services'; import { METRIC_TYPES } from '../../../../../../../plugins/vis_type_timeseries/common/metric_types'; @@ -49,7 +49,7 @@ export class TableVis extends Component { constructor(props) { super(props); - const fieldFormatsService = npStart.plugins.data.fieldFormats; + const fieldFormatsService = getFieldFormats(); const DateFormat = fieldFormatsService.getType(fieldFormats.FIELD_FORMAT_IDS.DATE); this.dateFormatter = new DateFormat({}, this.props.getConfig); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js index 5243f5f92a621a..954d3d174bb8c9 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js @@ -22,7 +22,6 @@ import React, { Component } from 'react'; import reactCSS from 'reactcss'; import { startsWith, get, cloneDeep, map } from 'lodash'; -import { toastNotifications } from 'ui/notify'; import { htmlIdGenerator } from '@elastic/eui'; import { ScaleType } from '@elastic/charts'; @@ -36,6 +35,7 @@ import { areFieldsDifferent } from '../../lib/charts'; import { createXaxisFormatter } from '../../lib/create_xaxis_formatter'; import { isBackgroundDark } from '../../../lib/set_is_reversed'; import { STACKED_OPTIONS } from '../../../visualizations/constants'; +import { getCoreStart } from '../../../services'; export class TimeseriesVisualization extends Component { static propTypes = { @@ -108,6 +108,7 @@ export class TimeseriesVisualization extends Component { createTickFormatter(get(model, 'formatter'), get(model, 'value_template'), getConfig); componentDidUpdate() { + const toastNotifications = getCoreStart().notifications.toasts; if ( this.showToastNotification && this.notificationReason !== this.showToastNotification.reason diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/legacy.ts b/src/legacy/core_plugins/vis_type_timeseries/public/legacy.ts index 93b35ee284f185..fb22bbd4146e20 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/legacy.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/public/legacy.ts @@ -32,4 +32,4 @@ const plugins: Readonly = { const pluginInstance = plugin({} as PluginInitializerContext); export const setup = pluginInstance.setup(npSetup.core, plugins); -export const start = pluginInstance.start(npStart.core); +export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/src/legacy/core_plugins/vis_type_vega/public/helpers/index.js b/src/legacy/core_plugins/vis_type_timeseries/public/legacy_imports.ts similarity index 79% rename from src/legacy/core_plugins/vis_type_vega/public/helpers/index.js rename to src/legacy/core_plugins/vis_type_timeseries/public/legacy_imports.ts index e9d6eb21fd3c7e..401acfc8df7663 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/helpers/index.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/legacy_imports.ts @@ -17,4 +17,8 @@ * under the License. */ -export * from './vega_config_provider'; +export { PersistedState } from 'ui/persisted_state'; +// @ts-ignore +export { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; +// @ts-ignore +export { timezoneProvider } from 'ui/vis/lib/timezone'; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/lib/fetch_fields.js b/src/legacy/core_plugins/vis_type_timeseries/public/lib/fetch_fields.js index 68e694f23fa7f9..9c64d0da2d88a5 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/lib/fetch_fields.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/lib/fetch_fields.js @@ -16,19 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import { kfetch } from 'ui/kfetch'; -import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; import { extractIndexPatterns } from '../../../../../plugins/vis_type_timeseries/common/extract_index_patterns'; +import { getCoreStart } from '../services'; export async function fetchFields(indexPatterns = ['*']) { const patterns = Array.isArray(indexPatterns) ? indexPatterns : [indexPatterns]; try { const indexFields = await Promise.all( patterns.map(pattern => { - return kfetch({ - method: 'GET', - pathname: '/api/metrics/fields', + return getCoreStart().http.get('/api/metrics/fields', { query: { index: pattern, }, @@ -43,7 +40,7 @@ export async function fetchFields(indexPatterns = ['*']) { }, {}); return fields; } catch (error) { - toastNotifications.addDanger({ + getCoreStart().notifications.toasts.addDanger({ title: i18n.translate('visTypeTimeseries.fetchFields.loadIndexPatternFieldsErrorMessage', { defaultMessage: 'Unable to load index_pattern fields', }), diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts index 225d81b71b8e08..5786399fc7830f 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts @@ -19,11 +19,11 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { PersistedState } from 'ui/persisted_state'; import { ExpressionFunction, KibanaContext, Render } from '../../../../plugins/expressions/public'; // @ts-ignore import { metricsRequestHandler } from './request_handler'; +import { PersistedState } from './legacy_imports'; const name = 'tsvb'; type Context = KibanaContext | null; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts index 22d2b3b10e566a..01750ee0c448d6 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts @@ -18,8 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; +import { defaultFeedbackMessage } from './legacy_imports'; // @ts-ignore import { metricsRequestHandler } from './request_handler'; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts b/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts index 4d1222d6f5a871..38a9c68487854e 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts @@ -16,29 +16,31 @@ * specific language governing permissions and limitations * under the License. */ -import { - PluginInitializerContext, - CoreSetup, - CoreStart, - Plugin, - SavedObjectsClientContract, - IUiSettingsClient, -} from '../../../../core/public'; +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; import { VisualizationsSetup } from '../../visualizations/public'; import { createMetricsFn } from './metrics_fn'; import { metricsVisDefinition } from './metrics_type'; -import { setSavedObjectsClient, setUISettings, setI18n } from './services'; +import { + setSavedObjectsClient, + setUISettings, + setI18n, + setFieldFormats, + setCoreStart, + setDataStart, +} from './services'; +import { DataPublicPluginStart } from '../../../../plugins/data/public'; /** @internal */ export interface MetricsPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; } -export interface MetricsVisualizationDependencies { - uiSettings: IUiSettingsClient; - savedObjectsClient: SavedObjectsClientContract; + +/** @internal */ +export interface MetricsPluginStartDependencies { + data: DataPublicPluginStart; } /** @internal */ @@ -58,9 +60,11 @@ export class MetricsPlugin implements Plugin, void> { visualizations.types.createReactVisualization(metricsVisDefinition); } - public start(core: CoreStart) { - // nothing to do here yet + public start(core: CoreStart, { data }: MetricsPluginStartDependencies) { setSavedObjectsClient(core.savedObjects); setI18n(core.i18n); + setFieldFormats(data.fieldFormats); + setDataStart(data); + setCoreStart(core); } } diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/request_handler.js b/src/legacy/core_plugins/vis_type_timeseries/public/request_handler.js index 84f62612aa9746..032ef335314d94 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/request_handler.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/request_handler.js @@ -18,10 +18,8 @@ */ import { validateInterval } from './lib/validate_interval'; -import { timezoneProvider } from 'ui/vis/lib/timezone'; -import { timefilter } from 'ui/timefilter'; -import { kfetch } from 'ui/kfetch'; -import { getUISettings } from './services'; +import { timezoneProvider } from './legacy_imports'; +import { getUISettings, getDataStart, getCoreStart } from './services'; export const metricsRequestHandler = async ({ uiState, @@ -34,7 +32,7 @@ export const metricsRequestHandler = async ({ const config = getUISettings(); const timezone = timezoneProvider(config)(); const uiStateObj = uiState.get(visParams.type, {}); - const parsedTimeRange = timefilter.calculateBounds(timeRange); + const parsedTimeRange = getDataStart().query.timefilter.timefilter.calculateBounds(timeRange); const scaledDataFormat = config.get('dateFormat:scaled'); const dateFormat = config.get('dateFormat'); @@ -44,9 +42,7 @@ export const metricsRequestHandler = async ({ validateInterval(parsedTimeRange, visParams, maxBuckets); - const resp = await kfetch({ - pathname: '/api/metrics/vis/data', - method: 'POST', + const resp = await getCoreStart().http.post('/api/metrics/vis/data', { body: JSON.stringify({ timerange: { timezone, diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/services.ts b/src/legacy/core_plugins/vis_type_timeseries/public/services.ts index af04578b8e27fc..c16ed47f548741 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/services.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/public/services.ts @@ -17,13 +17,22 @@ * under the License. */ -import { I18nStart, SavedObjectsStart, IUiSettingsClient } from 'src/core/public'; +import { I18nStart, SavedObjectsStart, IUiSettingsClient, CoreStart } from 'src/core/public'; import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; +import { DataPublicPluginStart, FieldFormatsStart } from '../../../../plugins/data/public'; export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); +export const [getFieldFormats, setFieldFormats] = createGetterSetter( + 'FieldFormats' +); + export const [getSavedObjectsClient, setSavedObjectsClient] = createGetterSetter( 'SavedObjectsClient' ); +export const [getCoreStart, setCoreStart] = createGetterSetter('CoreStart'); + +export const [getDataStart, setDataStart] = createGetterSetter('DataStart'); + export const [getI18n, setI18n] = createGetterSetter('I18n'); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js index bcd0b6314cef1d..986111b462b352 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js @@ -33,9 +33,9 @@ import { } from '@elastic/charts'; import { EuiIcon } from '@elastic/eui'; -import { timezoneProvider } from 'ui/vis/lib/timezone'; +import { timezoneProvider } from '../../../legacy_imports'; import { eventBus, ACTIVE_CURSOR } from '../../lib/active_cursor'; -import chrome from 'ui/chrome'; +import { getUISettings } from '../../../services'; import { GRID_LINE_CONFIG, ICON_TYPES_MAP, STACKED_OPTIONS } from '../../constants'; import { AreaSeriesDecorator } from './decorators/area_decorator'; import { BarSeriesDecorator } from './decorators/bar_decorator'; @@ -85,7 +85,7 @@ export const TimeSeries = ({ }, []); // eslint-disable-line const tooltipFormatter = decorateFormatter(xAxisFormatter); - const uiSettings = chrome.getUiSettingsClient(); + const uiSettings = getUISettings(); const timeZone = timezoneProvider(uiSettings)(); const hasBarChart = series.some(({ bars }) => bars.show); diff --git a/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts b/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts new file mode 100644 index 00000000000000..64a9aaaf3b7a68 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createGetterSetter } from '../../../../../plugins/kibana_utils/common'; +import { DataPublicPluginStart } from '../../../../../plugins/data/public'; +import { IUiSettingsClient, NotificationsStart, SavedObjectsStart } from 'kibana/public'; +import { dataPluginMock } from '../../../../../plugins/data/public/mocks'; +import { coreMock } from '../../../../../core/public/mocks'; + +export const [getData, setData] = createGetterSetter('Data'); +setData(dataPluginMock.createStartContract()); + +export const [getNotifications, setNotifications] = createGetterSetter( + 'Notifications' +); +setNotifications(coreMock.createStart().notifications); + +export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); +setUISettings(coreMock.createStart().uiSettings); + +export const [getSavedObjects, setSavedObjects] = createGetterSetter( + 'SavedObjects' +); +setSavedObjects(coreMock.createStart().savedObjects); + +export const [getInjectedVars, setInjectedVars] = createGetterSetter<{ + esShardTimeout: number; + enableExternalUrls: boolean; + emsTileLayerId: unknown; +}>('InjectedVars'); +setInjectedVars({ + emsTileLayerId: {}, + enableExternalUrls: true, + esShardTimeout: 10000, +}); + +export const getEsShardTimeout = () => getInjectedVars().esShardTimeout; +export const getEnableExternalUrls = () => getInjectedVars().enableExternalUrls; +export const getEmsTileLayerId = () => getInjectedVars().emsTileLayerId; diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js index 6c9eb86a9d2c02..b2ad45b5d7b6d9 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js +++ b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js @@ -43,6 +43,9 @@ import { SearchCache } from '../data_model/search_cache'; import { setup as visualizationsSetup } from '../../../visualizations/public/np_ready/public/legacy'; import { createVegaTypeDefinition } from '../vega_type'; +// TODO This is an integration test and thus requires a running platform. When moving to the new platform, +// this test has to be migrated to the newly created integration test environment. +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { npStart } from 'ui/new_platform'; const THRESHOLD = 0.1; diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx b/src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx index 18d48aea5d39a3..707a6830b5ba45 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx +++ b/src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx @@ -24,7 +24,7 @@ import compactStringify from 'json-stringify-pretty-compact'; import hjson from 'hjson'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { getNotifications } from '../services'; import { VisParams } from '../vega_fn'; import { VegaHelpMenu } from './vega_help_menu'; import { VegaActionsMenu } from './vega_actions_menu'; @@ -50,7 +50,7 @@ function format(value: string, stringify: typeof compactStringify, options?: any return stringify(spec, options); } catch (err) { // This is a common case - user tries to format an invalid HJSON text - toastNotifications.addError(err, { + getNotifications().toasts.addError(err, { title: i18n.translate('visTypeVega.editor.formatError', { defaultMessage: 'Error formatting spec', }), diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.js index 2f25f70610a81e..7c239800483f0c 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.js +++ b/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.js @@ -21,7 +21,7 @@ import _ from 'lodash'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; -import { getEsShardTimeout } from '../helpers'; +import { getEsShardTimeout } from '../services'; const TIMEFILTER = '%timefilter%'; const AUTOINTERVAL = '%autointerval%'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.test.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.test.js index 691e5e8944241b..c519da33ab1c94 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.test.js +++ b/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.test.js @@ -21,10 +21,6 @@ import { cloneDeep } from 'lodash'; import moment from 'moment'; import { EsQueryParser } from './es_query_parser'; -jest.mock('../helpers', () => ({ - getEsShardTimeout: jest.fn(() => '10000'), -})); - const second = 1000; const minute = 60 * second; const hour = 60 * minute; @@ -47,6 +43,8 @@ function create(min, max, dashboardCtx) { return inst; } +jest.mock('../services'); + describe(`EsQueryParser time`, () => { test(`roundInterval(4s)`, () => { expect(EsQueryParser._roundInterval(4 * second)).toBe(`1s`); diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.test.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.test.js index 0ec018f46c02b7..92f80545ce1b5d 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.test.js +++ b/src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.test.js @@ -18,6 +18,7 @@ */ import { SearchCache } from './search_cache'; +jest.mock('../services'); describe(`SearchCache`, () => { class FauxEs { diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.test.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.test.js index b76709ea2c9349..074744a0bda5e1 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.test.js +++ b/src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.test.js @@ -18,6 +18,7 @@ */ import { TimeCache } from './time_cache'; +jest.mock('../services'); describe(`TimeCache`, () => { class FauxTimefilter { diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js index 1bc8b1f90daabf..78d1cad8743112 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js +++ b/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js @@ -20,6 +20,7 @@ import { cloneDeep } from 'lodash'; import { VegaParser } from './vega_parser'; import { bypassExternalUrlCheck } from '../vega_view/vega_base_view'; +jest.mock('../services'); describe(`VegaParser._setDefaultValue`, () => { function check(spec, expected, ...params) { diff --git a/src/legacy/core_plugins/vis_type_vega/public/helpers/vega_config_provider.js b/src/legacy/core_plugins/vis_type_vega/public/legacy_imports.ts similarity index 77% rename from src/legacy/core_plugins/vis_type_vega/public/helpers/vega_config_provider.js rename to src/legacy/core_plugins/vis_type_vega/public/legacy_imports.ts index cc41b18479f93f..9e1067ed9099ab 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/helpers/vega_config_provider.js +++ b/src/legacy/core_plugins/vis_type_vega/public/legacy_imports.ts @@ -17,7 +17,9 @@ * under the License. */ -import chrome from 'ui/chrome'; - -export const getEsShardTimeout = () => chrome.getInjected('esShardTimeout'); -export const getEnableExternalUrls = () => chrome.getInjected('enableExternalUrls'); +// @ts-ignore +export { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; +// @ts-ignore +export { KibanaMapLayer } from 'ui/vis/map/kibana_map_layer'; +// @ts-ignore +export { KibanaMap } from 'ui/vis/map/kibana_map'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts index 75444a4a4f8e4e..9721de9848cfca 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts @@ -21,7 +21,13 @@ import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim' import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; import { Plugin as DataPublicPlugin } from '../../../../plugins/data/public'; import { VisualizationsSetup } from '../../visualizations/public'; -import { setNotifications, setData, setSavedObjects } from './services'; +import { + setNotifications, + setData, + setSavedObjects, + setInjectedVars, + setUISettings, +} from './services'; import { createVegaFn } from './vega_fn'; import { createVegaTypeDefinition } from './vega_type'; @@ -59,6 +65,13 @@ export class VegaPlugin implements Plugin, void> { core: CoreSetup, { data, expressions, visualizations, __LEGACY }: VegaPluginSetupDependencies ) { + setInjectedVars({ + esShardTimeout: core.injectedMetadata.getInjectedVar('esShardTimeout') as number, + enableExternalUrls: core.injectedMetadata.getInjectedVar('enableExternalUrls') as boolean, + emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true), + }); + setUISettings(core.uiSettings); + const visualizationDependencies: Readonly = { core, plugins: { diff --git a/src/legacy/core_plugins/vis_type_vega/public/services.ts b/src/legacy/core_plugins/vis_type_vega/public/services.ts index 94723f1a378d2a..88e0e0098bf8c3 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/services.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/services.ts @@ -21,6 +21,7 @@ import { SavedObjectsStart } from 'kibana/public'; import { NotificationsStart } from 'src/core/public'; import { DataPublicPluginStart } from '../../../../plugins/data/public'; import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; +import { IUiSettingsClient } from '../../../../core/public'; export const [getData, setData] = createGetterSetter('Data'); @@ -28,6 +29,18 @@ export const [getNotifications, setNotifications] = createGetterSetter('UISettings'); + export const [getSavedObjects, setSavedObjects] = createGetterSetter( 'SavedObjects' ); + +export const [getInjectedVars, setInjectedVars] = createGetterSetter<{ + esShardTimeout: number; + enableExternalUrls: boolean; + emsTileLayerId: unknown; +}>('InjectedVars'); + +export const getEsShardTimeout = () => getInjectedVars().esShardTimeout; +export const getEnableExternalUrls = () => getInjectedVars().enableExternalUrls; +export const getEmsTileLayerId = () => getInjectedVars().emsTileLayerId; diff --git a/src/legacy/core_plugins/vis_type_vega/public/shim/legacy_dependencies_plugin.ts b/src/legacy/core_plugins/vis_type_vega/public/shim/legacy_dependencies_plugin.ts index 5cf65d62a6aede..8925f76cffa43c 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/shim/legacy_dependencies_plugin.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/shim/legacy_dependencies_plugin.ts @@ -17,7 +17,10 @@ * under the License. */ +// TODO remove this file as soon as serviceSettings is exposed in the new platform +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import chrome from 'ui/chrome'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import 'ui/vis/map/service_settings'; import { CoreStart, Plugin } from 'kibana/public'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts b/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts index a7ca0dd3bb3499..1d4655b4d525ff 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; // @ts-ignore -import { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; +import { defaultFeedbackMessage } from './legacy_imports'; import { Status } from '../../visualizations/public'; import { DefaultEditorSize } from '../../vis_default_editor/public'; import { VegaVisualizationDependencies } from './plugin'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_base_view.js index 9d6adfd11aedd8..a6c17547d058e7 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -17,7 +17,6 @@ * under the License. */ -import chrome from 'ui/chrome'; import $ from 'jquery'; import moment from 'moment'; import dateMath from '@elastic/datemath'; @@ -29,7 +28,7 @@ import { i18n } from '@kbn/i18n'; import { TooltipHandler } from './vega_tooltip'; import { esFilters } from '../../../../../plugins/data/public'; -import { getEnableExternalUrls } from '../helpers/vega_config_provider'; +import { getEnableExternalUrls } from '../services'; vega.scheme('elastic', VISUALIZATION_COLORS); @@ -279,20 +278,14 @@ export class VegaBaseView { * @param {string} [index] as defined in Kibana, or default if missing */ async removeFilterHandler(query, index) { - const $injector = await chrome.dangerouslyGetActiveInjector(); const indexId = await this._findIndex(index); const filter = esFilters.buildQueryFilter(query, indexId); - // This is a workaround for the https://github.com/elastic/kibana/issues/18863 - // Once fixed, replace with a direct call (no await is needed because its not async) - // this._queryfilter.removeFilter(filter); - $injector.get('$rootScope').$evalAsync(() => { - try { - this._filterManager.removeFilter(filter); - } catch (err) { - this.onError(err); - } - }); + try { + this._filterManager.removeFilter(filter); + } catch (err) { + this.onError(err); + } } removeAllFiltersHandler() { diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js b/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js index 2794de6946ba09..38540e9f218fbd 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js @@ -19,7 +19,7 @@ import L from 'leaflet'; import 'leaflet-vega'; -import { KibanaMapLayer } from 'ui/vis/map/kibana_map_layer'; +import { KibanaMapLayer } from '../legacy_imports'; export class VegaMapLayer extends KibanaMapLayer { constructor(spec, options) { diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js b/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js index 82bcd6626789fa..487c90d01ada35 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js @@ -17,12 +17,12 @@ * under the License. */ -import { KibanaMap } from 'ui/vis/map/kibana_map'; import * as vega from 'vega-lib'; +import { i18n } from '@kbn/i18n'; import { VegaBaseView } from './vega_base_view'; import { VegaMapLayer } from './vega_map_layer'; -import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; +import { KibanaMap } from '../legacy_imports'; +import { getEmsTileLayerId, getUISettings } from '../services'; export class VegaMapView extends VegaBaseView { async _initViewCustomizations() { @@ -35,10 +35,10 @@ export class VegaMapView extends VegaBaseView { const tmsServices = await this._serviceSettings.getTMSServices(); // In some cases, Vega may be initialized twice, e.g. after awaiting... if (!this._$container) return; - const emsTileLayerId = chrome.getInjected('emsTileLayerId', true); + const emsTileLayerId = getEmsTileLayerId(); const mapStyle = mapConfig.mapStyle === 'default' ? emsTileLayerId.bright : mapConfig.mapStyle; - const isDarkMode = chrome.getUiSettingsClient().get('theme:darkMode'); + const isDarkMode = getUISettings().get('theme:darkMode'); baseMapOpts = tmsServices.find(s => s.id === mapStyle); baseMapOpts = { ...baseMapOpts, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/response_handlers.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/response_handlers.js index 642a032d8b9c20..3574fb232883dd 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/response_handlers.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/response_handlers.js @@ -21,6 +21,7 @@ import sinon from 'sinon'; import ngMock from 'ng_mock'; import expect from '@kbn/expect'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { aggResponseIndex } from 'ui/agg_response'; import { vislibSeriesResponseHandler } from '../response_handler'; diff --git a/src/legacy/ui/public/angular-bootstrap/bindHtml/bindHtml.js b/src/legacy/ui/public/angular-bootstrap/bindHtml/bindHtml.js deleted file mode 100755 index bafc7382686265..00000000000000 --- a/src/legacy/ui/public/angular-bootstrap/bindHtml/bindHtml.js +++ /dev/null @@ -1,10 +0,0 @@ -angular.module('ui.bootstrap.bindHtml', []) - - .directive('bindHtmlUnsafe', function () { - return function (scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe); - scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) { - element.html(value || ''); - }); - }; - }); diff --git a/src/legacy/ui/public/angular-bootstrap/index.js b/src/legacy/ui/public/angular-bootstrap/index.js deleted file mode 100644 index f574345af48abd..00000000000000 --- a/src/legacy/ui/public/angular-bootstrap/index.js +++ /dev/null @@ -1,47 +0,0 @@ - -/* eslint-disable */ - -/** - * TODO: Write custom components that address our needs to directly and deprecate these Bootstrap components. - */ - -import 'angular'; - -import { uiModules } from 'ui/modules'; - -uiModules.get('kibana', [ - 'ui.bootstrap', -]); - -/* - * angular-ui-bootstrap - * http://angular-ui.github.io/bootstrap/ - - * Version: 0.12.1 - 2015-02-20 - * License: MIT - */ -angular.module('ui.bootstrap', [ - 'ui.bootstrap.tpls', - 'ui.bootstrap.bindHtml', - 'ui.bootstrap.tooltip', -]); - -angular.module('ui.bootstrap.tpls', [ - 'template/tooltip/tooltip-html-unsafe-popup.html', - 'template/tooltip/tooltip-popup.html', -]); - -import './bindHtml/bindHtml'; -import './tooltip/tooltip'; - -import tooltipUnsafePopup from './tooltip/tooltip-html-unsafe-popup.html'; - -angular.module('template/tooltip/tooltip-html-unsafe-popup.html', []).run(['$templateCache', function($templateCache) { - $templateCache.put('template/tooltip/tooltip-html-unsafe-popup.html', tooltipUnsafePopup); -}]); - -import tooltipPopup from './tooltip/tooltip-popup.html'; - -angular.module('template/tooltip/tooltip-popup.html', []).run(['$templateCache', function($templateCache) { - $templateCache.put('template/tooltip/tooltip-popup.html', tooltipPopup); -}]); diff --git a/src/legacy/ui/public/angular-bootstrap/tooltip/position.js b/src/legacy/ui/public/angular-bootstrap/tooltip/position.js deleted file mode 100755 index 3444c33449152d..00000000000000 --- a/src/legacy/ui/public/angular-bootstrap/tooltip/position.js +++ /dev/null @@ -1,152 +0,0 @@ -angular.module('ui.bootstrap.position', []) - -/** - * A set of utility methods that can be use to retrieve position of DOM elements. - * It is meant to be used where we need to absolute-position DOM elements in - * relation to other, existing elements (this is the case for tooltips, popovers, - * typeahead suggestions etc.). - */ - .factory('$position', ['$document', '$window', function ($document, $window) { - - function getStyle(el, cssprop) { - if (el.currentStyle) { //IE - return el.currentStyle[cssprop]; - } else if ($window.getComputedStyle) { - return $window.getComputedStyle(el)[cssprop]; - } - // finally try and get inline style - return el.style[cssprop]; - } - - /** - * Checks if a given element is statically positioned - * @param element - raw DOM element - */ - function isStaticPositioned(element) { - return (getStyle(element, 'position') || 'static' ) === 'static'; - } - - /** - * returns the closest, non-statically positioned parentOffset of a given element - * @param element - */ - var parentOffsetEl = function (element) { - var docDomEl = $document[0]; - var offsetParent = element.offsetParent || docDomEl; - while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { - offsetParent = offsetParent.offsetParent; - } - return offsetParent || docDomEl; - }; - - return { - /** - * Provides read-only equivalent of jQuery's position function: - * http://api.jquery.com/position/ - */ - position: function (element) { - var elBCR = this.offset(element); - var offsetParentBCR = { top: 0, left: 0 }; - var offsetParentEl = parentOffsetEl(element[0]); - if (offsetParentEl != $document[0]) { - offsetParentBCR = this.offset(angular.element(offsetParentEl)); - offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; - offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; - } - - var boundingClientRect = element[0].getBoundingClientRect(); - return { - width: boundingClientRect.width || element.prop('offsetWidth'), - height: boundingClientRect.height || element.prop('offsetHeight'), - top: elBCR.top - offsetParentBCR.top, - left: elBCR.left - offsetParentBCR.left - }; - }, - - /** - * Provides read-only equivalent of jQuery's offset function: - * http://api.jquery.com/offset/ - */ - offset: function (element) { - var boundingClientRect = element[0].getBoundingClientRect(); - return { - width: boundingClientRect.width || element.prop('offsetWidth'), - height: boundingClientRect.height || element.prop('offsetHeight'), - top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), - left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) - }; - }, - - /** - * Provides coordinates for the targetEl in relation to hostEl - */ - positionElements: function (hostEl, targetEl, positionStr, appendToBody) { - - var positionStrParts = positionStr.split('-'); - var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center'; - - var hostElPos, - targetElWidth, - targetElHeight, - targetElPos; - - hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); - - targetElWidth = targetEl.prop('offsetWidth'); - targetElHeight = targetEl.prop('offsetHeight'); - - var shiftWidth = { - center: function () { - return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2; - }, - left: function () { - return hostElPos.left; - }, - right: function () { - return hostElPos.left + hostElPos.width; - } - }; - - var shiftHeight = { - center: function () { - return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2; - }, - top: function () { - return hostElPos.top; - }, - bottom: function () { - return hostElPos.top + hostElPos.height; - } - }; - - switch (pos0) { - case 'right': - targetElPos = { - top: shiftHeight[pos1](), - left: shiftWidth[pos0]() - }; - break; - case 'left': - targetElPos = { - top: shiftHeight[pos1](), - left: hostElPos.left - targetElWidth - }; - break; - case 'bottom': - targetElPos = { - top: shiftHeight[pos0](), - left: shiftWidth[pos1]() - }; - break; - default: - targetElPos = { - top: hostElPos.top - targetElHeight, - left: shiftWidth[pos1]() - }; - break; - } - - return targetElPos; - } - }; - }]); diff --git a/src/legacy/ui/public/angular-bootstrap/tooltip/tooltip.js b/src/legacy/ui/public/angular-bootstrap/tooltip/tooltip.js deleted file mode 100755 index b59b2922d8089a..00000000000000 --- a/src/legacy/ui/public/angular-bootstrap/tooltip/tooltip.js +++ /dev/null @@ -1,374 +0,0 @@ -import './position'; - -/** - * The following features are still outstanding: animation as a - * function, placement as a function, inside, support for more triggers than - * just mouse enter/leave, html tooltips, and selector delegation. - */ -angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) - -/** - * The $tooltip service creates tooltip- and popover-like directives as well as - * houses global options for them. - */ -.provider( '$tooltip', function () { - // The default options tooltip and popover. - var defaultOptions = { - placement: 'top', - animation: true, - popupDelay: 0 - }; - - // Default hide triggers for each show trigger - var triggerMap = { - 'mouseenter': 'mouseleave', - 'click': 'click', - 'focus': 'blur' - }; - - // The options specified to the provider globally. - var globalOptions = {}; - - /** - * `options({})` allows global configuration of all tooltips in the - * application. - * - * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { - * // place tooltips left instead of top by default - * $tooltipProvider.options( { placement: 'left' } ); - * }); - */ - this.options = function( value ) { - angular.extend( globalOptions, value ); - }; - - /** - * This allows you to extend the set of trigger mappings available. E.g.: - * - * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); - */ - this.setTriggers = function setTriggers ( triggers ) { - angular.extend( triggerMap, triggers ); - }; - - /** - * This is a helper function for translating camel-case to snake-case. - */ - function snake_case(name){ - var regexp = /[A-Z]/g; - var separator = '-'; - return name.replace(regexp, function(letter, pos) { - return (pos ? separator : '') + letter.toLowerCase(); - }); - } - - /** - * Returns the actual instance of the $tooltip service. - * TODO support multiple triggers - */ - this.$get = [ '$window', '$compile', '$timeout', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $document, $position, $interpolate ) { - return function $tooltip ( type, prefix, defaultTriggerShow ) { - var options = angular.extend( {}, defaultOptions, globalOptions ); - - /** - * Returns an object of show and hide triggers. - * - * If a trigger is supplied, - * it is used to show the tooltip; otherwise, it will use the `trigger` - * option passed to the `$tooltipProvider.options` method; else it will - * default to the trigger supplied to this directive factory. - * - * The hide trigger is based on the show trigger. If the `trigger` option - * was passed to the `$tooltipProvider.options` method, it will use the - * mapped trigger from `triggerMap` or the passed trigger if the map is - * undefined; otherwise, it uses the `triggerMap` value of the show - * trigger; else it will just use the show trigger. - */ - function getTriggers ( trigger ) { - var show = trigger || options.trigger || defaultTriggerShow; - var hide = triggerMap[show] || show; - return { - show: show, - hide: hide - }; - } - - var directiveName = snake_case( type ); - - var startSym = $interpolate.startSymbol(); - var endSym = $interpolate.endSymbol(); - var template = - '
'+ - '
'; - - return { - restrict: 'EA', - compile: function (tElem, tAttrs) { - var tooltipLinker = $compile( template ); - - return function link ( scope, element, attrs ) { - var tooltip; - var tooltipLinkedScope; - var transitionTimeout; - var popupTimeout; - var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; - var triggers = getTriggers( undefined ); - var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); - var ttScope = scope.$new(true); - - var positionTooltip = function () { - - var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody); - ttPosition.top += 'px'; - ttPosition.left += 'px'; - - // Now set the calculated positioning. - tooltip.css( ttPosition ); - }; - - // By default, the tooltip is not open. - // TODO add ability to start tooltip opened - ttScope.isOpen = false; - - function toggleTooltipBind () { - if ( ! ttScope.isOpen ) { - showTooltipBind(); - } else { - hideTooltipBind(); - } - } - - // Show the tooltip with delay if specified, otherwise show it immediately - function showTooltipBind() { - if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { - return; - } - - prepareTooltip(); - - if ( ttScope.popupDelay ) { - // Do nothing if the tooltip was already scheduled to pop-up. - // This happens if show is triggered multiple times before any hide is triggered. - if (!popupTimeout) { - popupTimeout = $timeout( show, ttScope.popupDelay, false ); - popupTimeout - .then(reposition => reposition()) - .catch((error) => { - // if the timeout is canceled then the string `canceled` is thrown. To prevent - // this from triggering an 'unhandled promise rejection' in angular 1.5+ the - // $timeout service explicitly tells $q that the promise it generated is "handled" - // but that does not include down chain promises like the one created by calling - // `popupTimeout.then()`. Because of this we need to ignore the "canceled" string - // and only propagate real errors - if (error !== 'canceled') { - throw error - } - }); - } - } else { - show()(); - } - } - - function hideTooltipBind () { - scope.$evalAsync(function () { - hide(); - }); - } - - // Show the tooltip popup element. - function show() { - - popupTimeout = null; - - // If there is a pending remove transition, we must cancel it, lest the - // tooltip be mysteriously removed. - if ( transitionTimeout ) { - $timeout.cancel( transitionTimeout ); - transitionTimeout = null; - } - - // Don't show empty tooltips. - if ( ! ttScope.content ) { - return angular.noop; - } - - createTooltip(); - - // Set the initial positioning. - tooltip.css({ top: 0, left: 0, display: 'block' }); - ttScope.$digest(); - - positionTooltip(); - - // And show the tooltip. - ttScope.isOpen = true; - ttScope.$digest(); // digest required as $apply is not called - - // Return positioning function as promise callback for correct - // positioning after draw. - return positionTooltip; - } - - // Hide the tooltip popup element. - function hide() { - // First things first: we don't show it anymore. - ttScope.isOpen = false; - - //if tooltip is going to be shown after delay, we must cancel this - $timeout.cancel( popupTimeout ); - popupTimeout = null; - - // And now we remove it from the DOM. However, if we have animation, we - // need to wait for it to expire beforehand. - // FIXME: this is a placeholder for a port of the transitions library. - if ( ttScope.animation ) { - if (!transitionTimeout) { - transitionTimeout = $timeout(removeTooltip, 500); - } - } else { - removeTooltip(); - } - } - - function createTooltip() { - // There can only be one tooltip element per directive shown at once. - if (tooltip) { - removeTooltip(); - } - tooltipLinkedScope = ttScope.$new(); - tooltip = tooltipLinker(tooltipLinkedScope, function (tooltip) { - if ( appendToBody ) { - $document.find( 'body' ).append( tooltip ); - } else { - element.after( tooltip ); - } - }); - } - - function removeTooltip() { - transitionTimeout = null; - if (tooltip) { - tooltip.remove(); - tooltip = null; - } - if (tooltipLinkedScope) { - tooltipLinkedScope.$destroy(); - tooltipLinkedScope = null; - } - } - - function prepareTooltip() { - prepPlacement(); - prepPopupDelay(); - } - - /** - * Observe the relevant attributes. - */ - attrs.$observe( type, function ( val ) { - ttScope.content = val; - - if (!val && ttScope.isOpen ) { - hide(); - } - }); - - attrs.$observe( prefix+'Title', function ( val ) { - ttScope.title = val; - }); - - function prepPlacement() { - var val = attrs[ prefix + 'Placement' ]; - ttScope.placement = angular.isDefined( val ) ? val : options.placement; - } - - function prepPopupDelay() { - var val = attrs[ prefix + 'PopupDelay' ]; - var delay = parseInt( val, 10 ); - ttScope.popupDelay = ! isNaN(delay) ? delay : options.popupDelay; - } - - var unregisterTriggers = function () { - element.unbind(triggers.show, showTooltipBind); - element.unbind(triggers.hide, hideTooltipBind); - }; - - function prepTriggers() { - var val = attrs[ prefix + 'Trigger' ]; - unregisterTriggers(); - - triggers = getTriggers( val ); - - if ( triggers.show === triggers.hide ) { - element.bind( triggers.show, toggleTooltipBind ); - } else { - element.bind( triggers.show, showTooltipBind ); - element.bind( triggers.hide, hideTooltipBind ); - } - } - prepTriggers(); - - var animation = scope.$eval(attrs[prefix + 'Animation']); - ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation; - - var appendToBodyVal = scope.$eval(attrs[prefix + 'AppendToBody']); - appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody; - - // if a tooltip is attached to we need to remove it on - // location change as its parent scope will probably not be destroyed - // by the change. - if ( appendToBody ) { - scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { - if ( ttScope.isOpen ) { - hide(); - } - }); - } - - // Make sure tooltip is destroyed and removed. - scope.$on('$destroy', function onDestroyTooltip() { - $timeout.cancel( transitionTimeout ); - $timeout.cancel( popupTimeout ); - unregisterTriggers(); - removeTooltip(); - ttScope = null; - }); - }; - } - }; - }; - }]; -}) - -.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); -}]) - -.directive( 'tooltipPopup', function () { - return { - restrict: 'EA', - replace: true, - scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-popup.html' - }; -}) - -.directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); -}]) - -.directive( 'tooltipHtmlUnsafePopup', function () { - return { - restrict: 'EA', - replace: true, - scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' - }; -}); \ No newline at end of file diff --git a/src/plugins/kibana_legacy/public/angular_bootstrap/bind_html/bind_html.js b/src/plugins/kibana_legacy/public/angular_bootstrap/bind_html/bind_html.js new file mode 100755 index 00000000000000..77844a3dd1363b --- /dev/null +++ b/src/plugins/kibana_legacy/public/angular_bootstrap/bind_html/bind_html.js @@ -0,0 +1,17 @@ +/* eslint-disable */ + +import angular from 'angular'; + +export function initBindHtml() { + angular + .module('ui.bootstrap.bindHtml', []) + + .directive('bindHtmlUnsafe', function() { + return function(scope, element, attr) { + element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe); + scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) { + element.html(value || ''); + }); + }; + }); +} diff --git a/src/plugins/kibana_legacy/public/angular_bootstrap/index.ts b/src/plugins/kibana_legacy/public/angular_bootstrap/index.ts new file mode 100644 index 00000000000000..1f15107a027623 --- /dev/null +++ b/src/plugins/kibana_legacy/public/angular_bootstrap/index.ts @@ -0,0 +1,50 @@ +/* eslint-disable */ + +import { once } from 'lodash'; +import angular from 'angular'; + +// @ts-ignore +import { initBindHtml } from './bind_html/bind_html'; +// @ts-ignore +import { initBootstrapTooltip } from './tooltip/tooltip'; + +import tooltipPopup from './tooltip/tooltip_popup.html'; + +import tooltipUnsafePopup from './tooltip/tooltip_html_unsafe_popup.html'; + +export const initAngularBootstrap = once(() => { + /* + * angular-ui-bootstrap + * http://angular-ui.github.io/bootstrap/ + + * Version: 0.12.1 - 2015-02-20 + * License: MIT + */ + angular.module('ui.bootstrap', [ + 'ui.bootstrap.tpls', + 'ui.bootstrap.bindHtml', + 'ui.bootstrap.tooltip', + ]); + + angular.module('ui.bootstrap.tpls', [ + 'template/tooltip/tooltip-html-unsafe-popup.html', + 'template/tooltip/tooltip-popup.html', + ]); + + initBindHtml(); + initBootstrapTooltip(); + + angular.module('template/tooltip/tooltip-html-unsafe-popup.html', []).run([ + '$templateCache', + function($templateCache: any) { + $templateCache.put('template/tooltip/tooltip-html-unsafe-popup.html', tooltipUnsafePopup); + }, + ]); + + angular.module('template/tooltip/tooltip-popup.html', []).run([ + '$templateCache', + function($templateCache: any) { + $templateCache.put('template/tooltip/tooltip-popup.html', tooltipPopup); + }, + ]); +}); diff --git a/src/plugins/kibana_legacy/public/angular_bootstrap/tooltip/position.js b/src/plugins/kibana_legacy/public/angular_bootstrap/tooltip/position.js new file mode 100755 index 00000000000000..24c8a8c5979cd5 --- /dev/null +++ b/src/plugins/kibana_legacy/public/angular_bootstrap/tooltip/position.js @@ -0,0 +1,167 @@ +/* eslint-disable */ + +import angular from 'angular'; + +export function initBootstrapPosition() { + angular + .module('ui.bootstrap.position', []) + + /** + * A set of utility methods that can be use to retrieve position of DOM elements. + * It is meant to be used where we need to absolute-position DOM elements in + * relation to other, existing elements (this is the case for tooltips, popovers, + * typeahead suggestions etc.). + */ + .factory('$position', [ + '$document', + '$window', + function($document, $window) { + function getStyle(el, cssprop) { + if (el.currentStyle) { + //IE + return el.currentStyle[cssprop]; + } else if ($window.getComputedStyle) { + return $window.getComputedStyle(el)[cssprop]; + } + // finally try and get inline style + return el.style[cssprop]; + } + + /** + * Checks if a given element is statically positioned + * @param element - raw DOM element + */ + function isStaticPositioned(element) { + return (getStyle(element, 'position') || 'static') === 'static'; + } + + /** + * returns the closest, non-statically positioned parentOffset of a given element + * @param element + */ + const parentOffsetEl = function(element) { + const docDomEl = $document[0]; + let offsetParent = element.offsetParent || docDomEl; + while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent)) { + offsetParent = offsetParent.offsetParent; + } + return offsetParent || docDomEl; + }; + + return { + /** + * Provides read-only equivalent of jQuery's position function: + * http://api.jquery.com/position/ + */ + position: function(element) { + const elBCR = this.offset(element); + let offsetParentBCR = { top: 0, left: 0 }; + const offsetParentEl = parentOffsetEl(element[0]); + if (offsetParentEl != $document[0]) { + offsetParentBCR = this.offset(angular.element(offsetParentEl)); + offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; + offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; + } + + const boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: elBCR.top - offsetParentBCR.top, + left: elBCR.left - offsetParentBCR.left, + }; + }, + + /** + * Provides read-only equivalent of jQuery's offset function: + * http://api.jquery.com/offset/ + */ + offset: function(element) { + const boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: + boundingClientRect.top + + ($window.pageYOffset || $document[0].documentElement.scrollTop), + left: + boundingClientRect.left + + ($window.pageXOffset || $document[0].documentElement.scrollLeft), + }; + }, + + /** + * Provides coordinates for the targetEl in relation to hostEl + */ + positionElements: function(hostEl, targetEl, positionStr, appendToBody) { + const positionStrParts = positionStr.split('-'); + const pos0 = positionStrParts[0]; + const pos1 = positionStrParts[1] || 'center'; + + let hostElPos; + let targetElWidth; + let targetElHeight; + let targetElPos; + + hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); + + targetElWidth = targetEl.prop('offsetWidth'); + targetElHeight = targetEl.prop('offsetHeight'); + + const shiftWidth = { + center: function() { + return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2; + }, + left: function() { + return hostElPos.left; + }, + right: function() { + return hostElPos.left + hostElPos.width; + }, + }; + + const shiftHeight = { + center: function() { + return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2; + }, + top: function() { + return hostElPos.top; + }, + bottom: function() { + return hostElPos.top + hostElPos.height; + }, + }; + + switch (pos0) { + case 'right': + targetElPos = { + top: shiftHeight[pos1](), + left: shiftWidth[pos0](), + }; + break; + case 'left': + targetElPos = { + top: shiftHeight[pos1](), + left: hostElPos.left - targetElWidth, + }; + break; + case 'bottom': + targetElPos = { + top: shiftHeight[pos0](), + left: shiftWidth[pos1](), + }; + break; + default: + targetElPos = { + top: hostElPos.top - targetElHeight, + left: shiftWidth[pos1](), + }; + break; + } + + return targetElPos; + }, + }; + }, + ]); +} diff --git a/src/plugins/kibana_legacy/public/angular_bootstrap/tooltip/tooltip.js b/src/plugins/kibana_legacy/public/angular_bootstrap/tooltip/tooltip.js new file mode 100755 index 00000000000000..05235fde9419be --- /dev/null +++ b/src/plugins/kibana_legacy/public/angular_bootstrap/tooltip/tooltip.js @@ -0,0 +1,423 @@ +/* eslint-disable */ + +import angular from 'angular'; + +import { initBootstrapPosition } from './position'; + +export function initBootstrapTooltip() { + initBootstrapPosition(); + /** + * The following features are still outstanding: animation as a + * function, placement as a function, inside, support for more triggers than + * just mouse enter/leave, html tooltips, and selector delegation. + */ + angular + .module('ui.bootstrap.tooltip', ['ui.bootstrap.position']) + + /** + * The $tooltip service creates tooltip- and popover-like directives as well as + * houses global options for them. + */ + .provider('$tooltip', function() { + // The default options tooltip and popover. + const defaultOptions = { + placement: 'top', + animation: true, + popupDelay: 0, + }; + + // Default hide triggers for each show trigger + const triggerMap = { + mouseenter: 'mouseleave', + click: 'click', + focus: 'blur', + }; + + // The options specified to the provider globally. + const globalOptions = {}; + + /** + * `options({})` allows global configuration of all tooltips in the + * application. + * + * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { + * // place tooltips left instead of top by default + * $tooltipProvider.options( { placement: 'left' } ); + * }); + */ + this.options = function(value) { + angular.extend(globalOptions, value); + }; + + /** + * This allows you to extend the set of trigger mappings available. E.g.: + * + * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); + */ + this.setTriggers = function setTriggers(triggers) { + angular.extend(triggerMap, triggers); + }; + + /** + * This is a helper function for translating camel-case to snake-case. + */ + function snake_case(name) { + const regexp = /[A-Z]/g; + const separator = '-'; + return name.replace(regexp, function(letter, pos) { + return (pos ? separator : '') + letter.toLowerCase(); + }); + } + + /** + * Returns the actual instance of the $tooltip service. + * TODO support multiple triggers + */ + this.$get = [ + '$window', + '$compile', + '$timeout', + '$document', + '$position', + '$interpolate', + function($window, $compile, $timeout, $document, $position, $interpolate) { + return function $tooltip(type, prefix, defaultTriggerShow) { + const options = angular.extend({}, defaultOptions, globalOptions); + + /** + * Returns an object of show and hide triggers. + * + * If a trigger is supplied, + * it is used to show the tooltip; otherwise, it will use the `trigger` + * option passed to the `$tooltipProvider.options` method; else it will + * default to the trigger supplied to this directive factory. + * + * The hide trigger is based on the show trigger. If the `trigger` option + * was passed to the `$tooltipProvider.options` method, it will use the + * mapped trigger from `triggerMap` or the passed trigger if the map is + * undefined; otherwise, it uses the `triggerMap` value of the show + * trigger; else it will just use the show trigger. + */ + function getTriggers(trigger) { + const show = trigger || options.trigger || defaultTriggerShow; + const hide = triggerMap[show] || show; + return { + show: show, + hide: hide, + }; + } + + const directiveName = snake_case(type); + + const startSym = $interpolate.startSymbol(); + const endSym = $interpolate.endSymbol(); + const template = + '
' + + '
'; + + return { + restrict: 'EA', + compile: function(tElem, tAttrs) { + const tooltipLinker = $compile(template); + + return function link(scope, element, attrs) { + let tooltip; + let tooltipLinkedScope; + let transitionTimeout; + let popupTimeout; + let appendToBody = angular.isDefined(options.appendToBody) + ? options.appendToBody + : false; + let triggers = getTriggers(undefined); + const hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']); + let ttScope = scope.$new(true); + + const positionTooltip = function() { + const ttPosition = $position.positionElements( + element, + tooltip, + ttScope.placement, + appendToBody + ); + ttPosition.top += 'px'; + ttPosition.left += 'px'; + + // Now set the calculated positioning. + tooltip.css(ttPosition); + }; + + // By default, the tooltip is not open. + // TODO add ability to start tooltip opened + ttScope.isOpen = false; + + function toggleTooltipBind() { + if (!ttScope.isOpen) { + showTooltipBind(); + } else { + hideTooltipBind(); + } + } + + // Show the tooltip with delay if specified, otherwise show it immediately + function showTooltipBind() { + if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) { + return; + } + + prepareTooltip(); + + if (ttScope.popupDelay) { + // Do nothing if the tooltip was already scheduled to pop-up. + // This happens if show is triggered multiple times before any hide is triggered. + if (!popupTimeout) { + popupTimeout = $timeout(show, ttScope.popupDelay, false); + popupTimeout + .then(reposition => reposition()) + .catch(error => { + // if the timeout is canceled then the string `canceled` is thrown. To prevent + // this from triggering an 'unhandled promise rejection' in angular 1.5+ the + // $timeout service explicitly tells $q that the promise it generated is "handled" + // but that does not include down chain promises like the one created by calling + // `popupTimeout.then()`. Because of this we need to ignore the "canceled" string + // and only propagate real errors + if (error !== 'canceled') { + throw error; + } + }); + } + } else { + show()(); + } + } + + function hideTooltipBind() { + scope.$evalAsync(function() { + hide(); + }); + } + + // Show the tooltip popup element. + function show() { + popupTimeout = null; + + // If there is a pending remove transition, we must cancel it, lest the + // tooltip be mysteriously removed. + if (transitionTimeout) { + $timeout.cancel(transitionTimeout); + transitionTimeout = null; + } + + // Don't show empty tooltips. + if (!ttScope.content) { + return angular.noop; + } + + createTooltip(); + + // Set the initial positioning. + tooltip.css({ top: 0, left: 0, display: 'block' }); + ttScope.$digest(); + + positionTooltip(); + + // And show the tooltip. + ttScope.isOpen = true; + ttScope.$digest(); // digest required as $apply is not called + + // Return positioning function as promise callback for correct + // positioning after draw. + return positionTooltip; + } + + // Hide the tooltip popup element. + function hide() { + // First things first: we don't show it anymore. + ttScope.isOpen = false; + + //if tooltip is going to be shown after delay, we must cancel this + $timeout.cancel(popupTimeout); + popupTimeout = null; + + // And now we remove it from the DOM. However, if we have animation, we + // need to wait for it to expire beforehand. + // FIXME: this is a placeholder for a port of the transitions library. + if (ttScope.animation) { + if (!transitionTimeout) { + transitionTimeout = $timeout(removeTooltip, 500); + } + } else { + removeTooltip(); + } + } + + function createTooltip() { + // There can only be one tooltip element per directive shown at once. + if (tooltip) { + removeTooltip(); + } + tooltipLinkedScope = ttScope.$new(); + tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) { + if (appendToBody) { + $document.find('body').append(tooltip); + } else { + element.after(tooltip); + } + }); + } + + function removeTooltip() { + transitionTimeout = null; + if (tooltip) { + tooltip.remove(); + tooltip = null; + } + if (tooltipLinkedScope) { + tooltipLinkedScope.$destroy(); + tooltipLinkedScope = null; + } + } + + function prepareTooltip() { + prepPlacement(); + prepPopupDelay(); + } + + /** + * Observe the relevant attributes. + */ + attrs.$observe(type, function(val) { + ttScope.content = val; + + if (!val && ttScope.isOpen) { + hide(); + } + }); + + attrs.$observe(prefix + 'Title', function(val) { + ttScope.title = val; + }); + + function prepPlacement() { + const val = attrs[prefix + 'Placement']; + ttScope.placement = angular.isDefined(val) ? val : options.placement; + } + + function prepPopupDelay() { + const val = attrs[prefix + 'PopupDelay']; + const delay = parseInt(val, 10); + ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay; + } + + const unregisterTriggers = function() { + element.unbind(triggers.show, showTooltipBind); + element.unbind(triggers.hide, hideTooltipBind); + }; + + function prepTriggers() { + const val = attrs[prefix + 'Trigger']; + unregisterTriggers(); + + triggers = getTriggers(val); + + if (triggers.show === triggers.hide) { + element.bind(triggers.show, toggleTooltipBind); + } else { + element.bind(triggers.show, showTooltipBind); + element.bind(triggers.hide, hideTooltipBind); + } + } + + prepTriggers(); + + const animation = scope.$eval(attrs[prefix + 'Animation']); + ttScope.animation = angular.isDefined(animation) + ? !!animation + : options.animation; + + const appendToBodyVal = scope.$eval(attrs[prefix + 'AppendToBody']); + appendToBody = angular.isDefined(appendToBodyVal) + ? appendToBodyVal + : appendToBody; + + // if a tooltip is attached to we need to remove it on + // location change as its parent scope will probably not be destroyed + // by the change. + if (appendToBody) { + scope.$on( + '$locationChangeSuccess', + function closeTooltipOnLocationChangeSuccess() { + if (ttScope.isOpen) { + hide(); + } + } + ); + } + + // Make sure tooltip is destroyed and removed. + scope.$on('$destroy', function onDestroyTooltip() { + $timeout.cancel(transitionTimeout); + $timeout.cancel(popupTimeout); + unregisterTriggers(); + removeTooltip(); + ttScope = null; + }); + }; + }, + }; + }; + }, + ]; + }) + + .directive('tooltip', [ + '$tooltip', + function($tooltip) { + return $tooltip('tooltip', 'tooltip', 'mouseenter'); + }, + ]) + + .directive('tooltipPopup', function() { + return { + restrict: 'EA', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-popup.html', + }; + }) + + .directive('tooltipHtmlUnsafe', [ + '$tooltip', + function($tooltip) { + return $tooltip('tooltipHtmlUnsafe', 'tooltip', 'mouseenter'); + }, + ]) + + .directive('tooltipHtmlUnsafePopup', function() { + return { + restrict: 'EA', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html', + }; + }); +} diff --git a/src/legacy/ui/public/angular-bootstrap/tooltip/tooltip-html-unsafe-popup.html b/src/plugins/kibana_legacy/public/angular_bootstrap/tooltip/tooltip_html_unsafe_popup.html similarity index 100% rename from src/legacy/ui/public/angular-bootstrap/tooltip/tooltip-html-unsafe-popup.html rename to src/plugins/kibana_legacy/public/angular_bootstrap/tooltip/tooltip_html_unsafe_popup.html diff --git a/src/legacy/ui/public/angular-bootstrap/tooltip/tooltip-popup.html b/src/plugins/kibana_legacy/public/angular_bootstrap/tooltip/tooltip_popup.html similarity index 100% rename from src/legacy/ui/public/angular-bootstrap/tooltip/tooltip-popup.html rename to src/plugins/kibana_legacy/public/angular_bootstrap/tooltip/tooltip_popup.html diff --git a/src/plugins/kibana_legacy/public/index.ts b/src/plugins/kibana_legacy/public/index.ts index 6e7a3cf87b87c0..19833d638fe4c4 100644 --- a/src/plugins/kibana_legacy/public/index.ts +++ b/src/plugins/kibana_legacy/public/index.ts @@ -24,6 +24,8 @@ export const plugin = (initializerContext: PluginInitializerContext) => new KibanaLegacyPlugin(initializerContext); export * from './plugin'; + +export { initAngularBootstrap } from './angular_bootstrap'; export * from './angular'; export * from './notify'; export * from './utils'; diff --git a/x-pack/legacy/plugins/graph/public/legacy_imports.ts b/x-pack/legacy/plugins/graph/public/legacy_imports.ts index 73a96016054fcf..27184f5701235b 100644 --- a/x-pack/legacy/plugins/graph/public/legacy_imports.ts +++ b/x-pack/legacy/plugins/graph/public/legacy_imports.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import 'ui/angular-bootstrap'; import 'ace'; export { SavedObject, SavedObjectKibanaServices } from 'ui/saved_objects/types'; diff --git a/x-pack/legacy/plugins/graph/public/plugin.ts b/x-pack/legacy/plugins/graph/public/plugin.ts index 48758ee1ec7703..b4ca4bf4231814 100644 --- a/x-pack/legacy/plugins/graph/public/plugin.ts +++ b/x-pack/legacy/plugins/graph/public/plugin.ts @@ -10,6 +10,7 @@ import { Plugin as DataPlugin } from 'src/plugins/data/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../src/plugins/navigation/public'; +import { initAngularBootstrap } from '../../../../../src/plugins/kibana_legacy/public'; export interface GraphPluginStartDependencies { npData: ReturnType; @@ -26,6 +27,7 @@ export class GraphPlugin implements Plugin { private savedObjectsClient: SavedObjectsClientContract | null = null; setup(core: CoreSetup, { licensing }: GraphPluginSetupDependencies) { + initAngularBootstrap(); core.application.register({ id: 'graph', title: 'Graph', From f293143cc7f07148d784f085d0fedadb2233ee12 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 10 Feb 2020 07:00:09 -0500 Subject: [PATCH 15/19] =?UTF-8?q?Logout=20should=20redirect=20to=20the=20l?= =?UTF-8?q?ogin=20screen=20at=20the=20server=20base=E2=80=A6=20(#56786)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * logout should redirect to the login screen at the server base path * Revert "logout should redirect to the login screen at the server base path" This reverts commit c80716be6e1c192f6c33c016e16522a24cdc2519. * fix logout url in nav control service Co-authored-by: Elastic Machine --- .../nav_control/nav_control_service.tsx | 8 +++- .../security/{security.js => security.ts} | 39 ++++++++++++++++++- x-pack/test/functional/page_objects/index.ts | 3 +- ...elector_page.js => space_selector_page.ts} | 11 +++--- 4 files changed, 50 insertions(+), 11 deletions(-) rename x-pack/test/functional/apps/security/{security.js => security.ts} (54%) rename x-pack/test/functional/page_objects/{space_selector_page.js => space_selector_page.ts} (86%) diff --git a/x-pack/plugins/security/public/nav_control/nav_control_service.tsx b/x-pack/plugins/security/public/nav_control/nav_control_service.tsx index 153e7112dc95b3..035549ccaa2cbb 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_service.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_service.tsx @@ -57,7 +57,7 @@ export class SecurityNavControlService { } private registerSecurityNavControl( - core: Pick + core: Pick ) { const currentUserPromise = this.authc.getCurrentUser(); core.chrome.navControls.registerRight({ @@ -65,10 +65,14 @@ export class SecurityNavControlService { mount: (el: HTMLElement) => { const I18nContext = core.i18n.Context; + const serverBasePath = core.injectedMetadata.getInjectedVar('serverBasePath') as string; + + const logoutUrl = `${serverBasePath}/logout`; + const props = { user: currentUserPromise, editProfileUrl: core.http.basePath.prepend('/app/kibana#/account'), - logoutUrl: core.http.basePath.prepend(`/logout`), + logoutUrl, }; ReactDOM.render( diff --git a/x-pack/test/functional/apps/security/security.js b/x-pack/test/functional/apps/security/security.ts similarity index 54% rename from x-pack/test/functional/apps/security/security.js rename to x-pack/test/functional/apps/security/security.ts index 37b01ff61f5afa..2096a7755e01de 100644 --- a/x-pack/test/functional/apps/security/security.js +++ b/x-pack/test/functional/apps/security/security.ts @@ -5,11 +5,15 @@ */ import expect from '@kbn/expect'; +import { parse } from 'url'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function({ getService, getPageObjects }) { +export default function({ getService, getPageObjects }: FtrProviderContext) { + const browser = getService('browser'); const esArchiver = getService('esArchiver'); - const PageObjects = getPageObjects(['security']); + const PageObjects = getPageObjects(['security', 'spaceSelector']); const testSubjects = getService('testSubjects'); + const spaces = getService('spaces'); describe('Security', function() { this.tags('smoke'); @@ -46,6 +50,37 @@ export default function({ getService, getPageObjects }) { const logoutMessage = await testSubjects.getVisibleText('loginInfoMessage'); expect(logoutMessage).to.eql('You have logged out of Kibana.'); }); + + describe('within a non-default space', async () => { + before(async () => { + await PageObjects.security.forceLogout(); + + await spaces.create({ + id: 'some-space', + name: 'Some non-default space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spaces.delete('some-space'); + }); + + it('logging out of a non-default space redirects to the login page at the server root', async () => { + await PageObjects.security.login(null, null, { + expectSpaceSelector: true, + }); + + await PageObjects.spaceSelector.clickSpaceCard('some-space'); + await PageObjects.spaceSelector.expectHomePage('some-space'); + + await PageObjects.security.logout(); + + const currentUrl = await browser.getCurrentUrl(); + const url = parse(currentUrl); + expect(url.pathname).to.eql('/login'); + }); + }); }); }); } diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 19a626536f1bdc..9479f880852220 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -22,8 +22,6 @@ import { WatcherPageProvider } from './watcher_page'; // @ts-ignore not ts yet import { ReportingPageProvider } from './reporting_page'; // @ts-ignore not ts yet -import { SpaceSelectorPageProvider } from './space_selector_page'; -// @ts-ignore not ts yet import { AccountSettingProvider } from './accountsetting_page'; import { InfraHomePageProvider } from './infra_home_page'; import { InfraLogsPageProvider } from './infra_logs_page'; @@ -46,6 +44,7 @@ import { CopySavedObjectsToSpacePageProvider } from './copy_saved_objects_to_spa import { LensPageProvider } from './lens_page'; import { InfraMetricExplorerProvider } from './infra_metric_explorer'; import { RoleMappingsPageProvider } from './role_mappings_page'; +import { SpaceSelectorPageProvider } from './space_selector_page'; import { EndpointPageProvider } from './endpoint_page'; // just like services, PageObjects are defined as a map of diff --git a/x-pack/test/functional/page_objects/space_selector_page.js b/x-pack/test/functional/page_objects/space_selector_page.ts similarity index 86% rename from x-pack/test/functional/page_objects/space_selector_page.js rename to x-pack/test/functional/page_objects/space_selector_page.ts index ad0f48bdd50bff..74f53a1cf551fb 100644 --- a/x-pack/test/functional/page_objects/space_selector_page.js +++ b/x-pack/test/functional/page_objects/space_selector_page.ts @@ -5,8 +5,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; -export function SpaceSelectorPageProvider({ getService, getPageObjects }) { +export function SpaceSelectorPageProvider({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const log = getService('log'); const testSubjects = getService('testSubjects'); @@ -19,7 +20,7 @@ export function SpaceSelectorPageProvider({ getService, getPageObjects }) { log.debug('SpaceSelectorPage:initTests'); } - async clickSpaceCard(spaceId) { + async clickSpaceCard(spaceId: string) { return await retry.try(async () => { log.info(`SpaceSelectorPage:clickSpaceCard(${spaceId})`); await testSubjects.click(`space-card-${spaceId}`); @@ -27,11 +28,11 @@ export function SpaceSelectorPageProvider({ getService, getPageObjects }) { }); } - async expectHomePage(spaceId) { + async expectHomePage(spaceId: string) { return await this.expectRoute(spaceId, `/app/kibana#/home`); } - async expectRoute(spaceId, route) { + async expectRoute(spaceId: string, route: string) { return await retry.try(async () => { log.debug(`expectRoute(${spaceId}, ${route})`); await find.byCssSelector('[data-test-subj="kibanaChrome"] nav:not(.ng-hide) ', 20000); @@ -49,7 +50,7 @@ export function SpaceSelectorPageProvider({ getService, getPageObjects }) { return await testSubjects.click('spacesNavSelector'); } - async clickSpaceAvatar(spaceId) { + async clickSpaceAvatar(spaceId: string) { return await retry.try(async () => { log.info(`SpaceSelectorPage:clickSpaceAvatar(${spaceId})`); await testSubjects.click(`space-avatar-${spaceId}`); From b13acff23df018c0049886a8a888f3873349cb1c Mon Sep 17 00:00:00 2001 From: Aris Papadopoulos Date: Mon, 10 Feb 2020 12:32:55 +0000 Subject: [PATCH 16/19] Kibana Kerberos documentation (#51883) * kerberos b Please enter the commit message for your changes. Lines starting * Apply suggestions from code review Co-Authored-By: Brandon Kobel Co-Authored-By: Lisa Cawley Co-authored-by: Brandon Kobel Co-authored-by: Lisa Cawley Co-authored-by: Elastic Machine --- .../security/authentication/index.asciidoc | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index a36b7b9c6d5f51..3906f15167bd08 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -12,6 +12,7 @@ - <> - <> - <> +- <> [[basic-authentication]] ==== Basic authentication @@ -214,3 +215,26 @@ leaked, it can't be re-used after logout. This is known as "local" logout. {kib} can also initiate a "global" logout or _Single Logout_ if it's supported by the external authentication provider and not explicitly disabled by {es}. In this case, the user is redirected to the external authentication provider for log out of all applications associated with the active provider session. + +[[kerberos]] +==== Kerberos single sign-on + +As with the previous SSOs, make sure that you have configured {es} first accordingly. See {ref}/kerberos-realm.html[Kerberos authentication]. + +Next, to enable Kerberos in {kib}, you will need to enable the Kerberos authentication provider in the `kibana.yml` configuration file, as follows: + +[source,yaml] +----------------------------------------------- +xpack.security.authc.providers: [kerberos] +----------------------------------------------- + +You may want to be able to authenticate with the basic authentication provider as a secondary mechanism or while you are setting up Kerberos for the stack: + +[source,yaml] +----------------------------------------------- +xpack.security.authc.providers: [kerberos, basic] +----------------------------------------------- + +As a reminder, the order is important as it determines the order in which each authentication provider is attempted. + +Kibana uses SPNEGO, which wraps the Kerberos protocol for use with HTTP, extending it to web applications. At the end of the Kerberos handshake, Kibana will forward the service ticket to Elasticsearch. Elasticsearch will unpack it and it will respond with an access and refresh token which are then used for subsequent authentication. From bcbb16d1f386f24f53ef8edddae7fb6ee0c9cc21 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 10 Feb 2020 09:09:55 -0500 Subject: [PATCH 17/19] [ML] Add functional tests for analytics UI: classification jobs (#56912) * add classification functional tests * remove unneeded testing tag * update jobId and description per suggestion Co-authored-by: Elastic Machine --- .../evaluate_panel.tsx | 6 +- .../results_table.tsx | 6 +- .../classification_creation.ts | 174 ++ .../data_frame_analytics/index.ts | 1 + .../regression_creation.ts | 2 +- .../ml/bm_classification/data.json.gz | Bin 0 -> 169485 bytes .../ml/bm_classification/mappings.json | 1548 +++++++++++++++++ .../machine_learning/data_frame_analytics.ts | 9 + 8 files changed, 1743 insertions(+), 3 deletions(-) create mode 100644 x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts create mode 100644 x-pack/test/functional/es_archives/ml/bm_classification/data.json.gz create mode 100644 x-pack/test/functional/es_archives/ml/bm_classification/mappings.json diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx index 2cb58f9c9d81cb..1e24bfec6de5e9 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -218,7 +218,10 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) } return ( - + @@ -337,6 +340,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) = React.memo( : searchError; return ( - + diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts new file mode 100644 index 00000000000000..1b8c8299a8ac61 --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('classification creation', function() { + this.tags(['smoke']); + before(async () => { + await esArchiver.load('ml/bm_classification'); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + await esArchiver.unload('ml/bm_classification'); + }); + + const testDataList = [ + { + suiteTitle: 'bank marketing', + jobType: 'classification', + jobId: `bm_1_${Date.now()}`, + jobDescription: + "Classification job based on 'bank-marketing' dataset with dependentVariable 'y' and trainingPercent '20'", + source: 'bank-marketing*', + get destinationIndex(): string { + return `dest_${this.jobId}`; + }, + dependentVariable: 'y', + trainingPercent: '20', + modelMemory: '105mb', + createIndexPattern: true, + expected: { + row: { + type: 'classification', + status: 'stopped', + progress: '100', + }, + }, + }, + ]; + for (const testData of testDataList) { + describe(`${testData.suiteTitle}`, function() { + after(async () => { + await ml.api.deleteIndices(testData.destinationIndex); + }); + + it('loads the data frame analytics page', async () => { + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToDataFrameAnalytics(); + }); + + it('loads the job creation flyout', async () => { + await ml.dataFrameAnalytics.startAnalyticsCreation(); + }); + + it('selects the job type', async () => { + await ml.dataFrameAnalyticsCreation.assertJobTypeSelectExists(); + await ml.dataFrameAnalyticsCreation.selectJobType(testData.jobType); + }); + + it('inputs the job id', async () => { + await ml.dataFrameAnalyticsCreation.assertJobIdInputExists(); + await ml.dataFrameAnalyticsCreation.setJobId(testData.jobId); + }); + + it('inputs the job description', async () => { + await ml.dataFrameAnalyticsCreation.assertJobDescriptionInputExists(); + await ml.dataFrameAnalyticsCreation.setJobDescription(testData.jobDescription); + }); + + it('selects the source index', async () => { + await ml.dataFrameAnalyticsCreation.assertSourceIndexInputExists(); + await ml.dataFrameAnalyticsCreation.selectSourceIndex(testData.source); + }); + + it('inputs the destination index', async () => { + await ml.dataFrameAnalyticsCreation.assertDestIndexInputExists(); + await ml.dataFrameAnalyticsCreation.setDestIndex(testData.destinationIndex); + }); + + it('inputs the dependent variable', async () => { + await ml.dataFrameAnalyticsCreation.assertDependentVariableInputExists(); + await ml.dataFrameAnalyticsCreation.selectDependentVariable(testData.dependentVariable); + }); + + it('inputs the training percent', async () => { + await ml.dataFrameAnalyticsCreation.assertTrainingPercentInputExists(); + await ml.dataFrameAnalyticsCreation.setTrainingPercent(testData.trainingPercent); + }); + + it('inputs the model memory limit', async () => { + await ml.dataFrameAnalyticsCreation.assertModelMemoryInputExists(); + await ml.dataFrameAnalyticsCreation.setModelMemory(testData.modelMemory); + }); + + it('sets the create index pattern switch', async () => { + await ml.dataFrameAnalyticsCreation.assertCreateIndexPatternSwitchExists(); + await ml.dataFrameAnalyticsCreation.setCreateIndexPatternSwitchState( + testData.createIndexPattern + ); + }); + + it('creates the analytics job', async () => { + await ml.dataFrameAnalyticsCreation.assertCreateButtonExists(); + await ml.dataFrameAnalyticsCreation.createAnalyticsJob(); + }); + + it('starts the analytics job', async () => { + await ml.dataFrameAnalyticsCreation.assertStartButtonExists(); + await ml.dataFrameAnalyticsCreation.startAnalyticsJob(); + }); + + it('closes the create job flyout', async () => { + await ml.dataFrameAnalyticsCreation.assertCloseButtonExists(); + await ml.dataFrameAnalyticsCreation.closeCreateAnalyticsJobFlyout(); + }); + + it('finishes analytics processing', async () => { + await ml.dataFrameAnalytics.waitForAnalyticsCompletion(testData.jobId); + }); + + it('displays the analytics table', async () => { + await ml.dataFrameAnalytics.assertAnalyticsTableExists(); + }); + + it('displays the stats bar', async () => { + await ml.dataFrameAnalytics.assertAnalyticsStatsBarExists(); + }); + + it('displays the created job in the analytics table', async () => { + await ml.dataFrameAnalyticsTable.refreshAnalyticsTable(); + await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.jobId); + const rows = await ml.dataFrameAnalyticsTable.parseAnalyticsTable(); + const filteredRows = rows.filter(row => row.id === testData.jobId); + expect(filteredRows).to.have.length( + 1, + `Filtered analytics table should have 1 row for job id '${testData.jobId}' (got matching items '${filteredRows}')` + ); + }); + + it('displays details for the created job in the analytics table', async () => { + await ml.dataFrameAnalyticsTable.assertAnalyticsRowFields(testData.jobId, { + id: testData.jobId, + description: testData.jobDescription, + sourceIndex: testData.source, + destinationIndex: testData.destinationIndex, + type: testData.expected.row.type, + status: testData.expected.row.status, + progress: testData.expected.row.progress, + }); + }); + + it('creates the destination index and writes results to it', async () => { + await ml.api.assertIndicesExist(testData.destinationIndex); + await ml.api.assertIndicesNotEmpty(testData.destinationIndex); + }); + + it('displays the results view for created job', async () => { + await ml.dataFrameAnalyticsTable.openResultsView(); + await ml.dataFrameAnalytics.assertClassificationEvaluatePanelElementsExists(); + await ml.dataFrameAnalytics.assertClassificationTablePanelExists(); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/index.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/index.ts index dd8de77e6d5d0e..fda0c5d203f2e0 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/index.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/index.ts @@ -11,5 +11,6 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./outlier_detection_creation')); loadTestFile(require.resolve('./regression_creation')); + loadTestFile(require.resolve('./classification_creation')); }); } diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/regression_creation.ts index 1a514f4ad44e51..6a01afe6183ed8 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/regression_creation.ts @@ -11,7 +11,7 @@ export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('outlier detection creation', function() { + describe('regression creation', function() { this.tags(['smoke']); before(async () => { await esArchiver.load('ml/egs_regression'); diff --git a/x-pack/test/functional/es_archives/ml/bm_classification/data.json.gz b/x-pack/test/functional/es_archives/ml/bm_classification/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..12ccf6ae605124649f58c89acdfa5ea0a8f2bf62 GIT binary patch literal 169485 zcmX7vby$<{+r|fjjTjrCjM1YTMo4#yC`d~=Bm@B&-4dfgDHRZ;M5Uyqq~_>GKvG&t zN=lmFzVG|zp5xh$XU}n8*L|L!^Sap*$jHJ#C>sFK-qzbzD2kZqksxQG&lR0ZcLX6X=w{=;( zzYkfht*trUtl#`qzV5yWK3VRazPY@)Ou7m9_xJtH*-hXvea^xQ|Ff-%%Usc$r1YEh zt8W!A&d(P1ly9zo9__XyBq?8>@AYdxZoB!Fc5__cb}m1(mveQv<$k_%a)!l!n5Bme z<@{24-_1=#Po za-iqM_1=2Li{)3>2P=n9>-Q9bj?F}e*|L?dFD^rGws&{vZ~m}_9+?g8{%Q?AZM)t* z?A8uFJ6Jxcxk$R1RGuByzB#J-mhN|bqa5Jt`*-_#CxQOvqB~TfHTZn%@&0ev<7~yN zh0v?v?zWb5m!#*pZI{x)-oDq{p*I^#E3@l2S4p8~3*%Q$>)EasZXSz>?B@PbJfH3l zZBxG9%VoX-D+ioaoE%L&R%E`i%_aP*@A}^l5`WtgcXTltfssqG~K;N?cMfq{j~14Wr8C$w6BS{(U!$Z63ZqGSD5N!rFegcvQ7m=L1H(x^cdN!OO39 zUWRbDa$%yAH4vw0qR2`jB!q$=7p}tCtYFx&k`yzeu>_}*LH;oK^{0IiViO5WQMWmB zWB$NiX2dT!r^kUi#QeKH>=wPbSF#y0lE$1ZeQL!;63a{fkL~y3((>=Ni_X99bzen0 z%^+2(Ye~)$BRPh|I*2FOND#4m>;nz#W1bL6|D@>tV41nri;e5En{P`=9cCmyF+wt| z;q0y;G=?ZeM-EXEM2`qoz1BtWJs^K@OMoqIO9nqlG6a}fd7{EfUz^)Qhy2z~94^L% z)E)*=Qq*gNMPJr0ZBBO{UiiHV9bsl9d(BRC?Ns#*(#`fo%zU1Vl-MD*nqvpD)@Pj% z2hCiOhLj^i*-~-q>`26FE7)2{u#G)ADu5P;vMi#63Kw@xF16_(sYxsAqyTD+*&U)#(k0Ybm;qQ~PvS1KzoAkOCUq!z*=}yyqmPZVZJi5$K@X2%Lky_;wl_LM z;a1}2@vvi}h{Y(yl_+8{)1r(G%Z7ag7;QbeOnu24q=N-q{ ze^cDEqt5)`D1kwjiP&@ZasA>IcGUJe4iS7|-@xUti`o@kc!ors&>?A2UqHjeQmXt@ zHd{f6=VCfoAr(!eidelF6x3AYT2aDDRe!WAFn8NQ0b{}kh@m0p%MZ_&>0wK-r$sW^ zYS>>Nj$D2m6mBW{JQyZ30DYO>=&pXKb9#4p6@!7}sLT@vj{BvQ9$a>q8}_(=XCDAX zva4JeoxED!oW=*m)M)FoD!mvzT4~3~^b`{5P#Me5J!rdEQ7ocNc$DVCO6nZncq#|3 z&GYf-T=EVg65JyzE0&7;F`3ss=4b=(nf%hFqbi5rZfVmSrWcMVunJE|twAn&BdrVg zaA`f_;a&EHS@YzWm5wmm&_EW%3)?w|KD3Mw=zOgD757?}TKGdpH>!Eu zx3_~hDsofoF$JapkAxlky3UTk{Qo9W_|=dr21|`K#IO9J-RNc5T1~ zq}@zkdfpR(&AIdAjC}Q*LDF9815>o%9^Sg^5h-z`v@;){f6aoXHqKgciQNs(V9>hA zG^+i1&^r1Q@J@^h@KF3cs|#!=FI<&FB?tfPp{YS7(GHL4Gz2+>IKb+0=*f-boughq zBd3DYV9SiZ2_p(SmK?Lh>|(>vPSoPa@6T*QAN8^P@(u^LC=}V_*qUemDUqGqv#oS0 zDqf>KUemvlcKN)NkM*t$f}HRFocj|v_OM1x64d}4;SF$VIJiK+p*z}_LS+CElJ~8S znJk{Z3~IhnS@LTEQa+_2UiCZYzTl#2lhDDse#jA?^Jujy+;~M^(^RiIe6)z|bz3v8 zY$yOdy((4y_d;gl*g=j`@jzFb_l{^eL+27q5npK84#d^stm5ie1QRB|hua1uKaO>( zT6lG+1WQ!m$-1$)#K{bk=wlzdzB)MBOu4fs_y8Np5soUF^>vY@d6u-0-|BIwgjMQU zR@aE)`D$z~>0Z@V@mY}zJKQ;R;1e$AO?}~+Yx|lcU7EbCuK3|N>YpYrS6OSfYK7~p zC0UyDomq3^m?&lPG9ol?*>N(+K8Qj38!h9}kPwtdNTRR3hYk__r6-N7Ns(zqBdzpNd!;|QlqLL^XRs#!$B zc+Es$0%n=zFD0@iWU$ROx_p%J&S4g|Z}G_-+%hN&30yz=JEE@z!eX_V^Jj-&FTP?~ zNs{b!enNDX7sxGn2}6c%Uw!8=n)3Vw%!2J{W)g$*_##x)LT60SQ-w;*Fal_2#9jkt z!c)@bd5$K_n~eYa;y;iFt)9aCmMA~@604p#03^Y$B6BxElw#tIp4ypT*i02{M?ll_ zm<+12BO+7oS6e~heSX0wU+L;2J<)HfkxC>gF+eRExKg0Tio(srJ4bk-RK)+eIfcAEO<XH5XLUr2U&Ni;fI&XVyn1^=zUefB86SRZv>BlHqfH_s zvV>Wq1uI4osK+rePXjw_-$TCJvov?H+*-RMgz5mDxd9|d&ECv&Yv`&pD0C}rn$8n; z|NUxcW9FPg5|IQ92AM}1>7+H$j`<$Xu*Tf$BKmYGg9syXqk5%Wp9WTst~J z4}SEygY2S2oudAP>s$}^xF)yV291W5n>XlREX0Kp9uX;>m&eNkujKY5QM`*m6IPE{ z2nNZJd@iXX>}mW#7lTGH+rypFzM0mtvtwgV)BP-mJkMfqh}AwkRx@Sd_6Q!e97!tE zjMkybEA3ibF3(e}qm)FMV=rYI&^Si)?j_m3MkpxyXR?p?Q(#oS8ihGph|gZAVWL2m zX)D<-n|g7-!p6O>zG3Jjf3B$Zo4$&p*~(QHxAsw-6_Sub?@8K>p>C{ zbuYlv49dQ8mX zmtk`@kI0bCHGF7Tgy{trh{K3u$dQLqEMFIVNZf7L}uh8ieG(kG=5Zyb|p9 zfqm()&+LFR^d+X5v%)GPkgJDQN5lMWVsgKY5CK#3Aa_D?3qO0C7IweLHQ zC{jn5Cw^cNqLNj|#})QEzw)g3%{4xXUAFno`z>cJOFb%Zh>!Wmy@aD7ORL`^zu(Rb z9`;1#ONALMb$+KKr>9$K(ux4$>BwuH6OuQb&OR1FsRTbz8MQVijH;f38MEC+ z$Wtu3$AUPc7PN?-up)2A;q5CsAx5&maaNUaDbq4YFVU(JH`YBW2@g14bDBC55o6&< z|`DBURShDtd%&Q)p=tD`&U=6C?2cpWpDVOO5KNZ40ugj8FyPazbprjm}YK1tBP z68kA+8HOyG?LI|-ihhCzf$Cn#0fkY_0_kw@ifp_X21{J&bvuqL<7RaqNRki^;}{w2t>-GGrKZB?x6( zQ0Ba~l2_gB%Co(0))H8{s#l#GPdfT9Px-133H1~Xua6~8xRv^ObAEG1W>tRDA20m2 zll@WQD|Py-{qNS@-|zgu$idqw4ymOQ?sUr4Mk{Dotx$dDBqmXb0swH#0>L=Tfs@=A zm$G~SLM7w?^jc>tQkD~CuSL>&zWIMmZC5ooy`8Du5INeI;*4WIgJ@>MBI`v8^4z>s%EFiMhEz>$-=bEojAsqA-G zN9$u&EQ;WNI$!}G*&Hft*6QbQ(UM>X#7eiZk$nN0?mTngqeFR{Jg3_X1?T^IL?*fUv0P=3qq(}p! z9g&Yxbk@QAO2%`h9n&0q`{sW}PGrWlxs>f)g;wvZEj6SYP(kr4$ygA`1MpI-6lY#_ zTS(V!0dUNYXCfDZW~pBNGDLYaC8_Ns-JZrjqm|hHH|P9Mmq+sRqB25=l_^9~!u4duDEw_LW+e|;5+V^%qBi0a1S;!7VE2``7^nA^X; zgsCJH%H@4`YV2#re-5NEVC9BL{Qex`jQ$=|K#8I@$EM@Z$^TaJt|fI}YDjL7%K967 zYMQW6Gk3+PsR8k9{I|eq<}jGcJuQW7ve;n+E7;&;@W)WS!INbOOX@K$ys8|q2c!th z|2g&AtK)0AZ15*~W@mOKIxD9=3$yY6DlS<%6Yd}8aYT_DH{%#T%m4y=o7&Qdm(?~r zbF8_N#WRve4-9_iqul%2O_LJ_Y7ycKEdv$bg-R?#@x6`;q1?;eYnrJ`{x5RuBt1Ua+BExggqn@=Zs!z_zbrYC>)kEVbB zdK-W@3v9Nnld1FQ{h?;f!g%Bh2m2?Q^}n9J{J3itScqfK;_~}$eD|M|R_f(DkH_q3 zAgrincFH{JB$F0_VC}76YvmXes3lUQf;TTs} zl?cRYsNCK*-c?P!jP|f0GMy#wq0$rkf$Lb>X1adREV{7K3w-O#j(;Zw@%*kBI^y4@SiHbpI%ssjj&O6%vdNX zIIeo%5KRiQO;)C1N4WCFPg}_~j9lla(%bi^Q4;k)#@#zC%DX>N5}#pnI6ZA3I@qjx zU0S~R8rOO2t}33ui79k|pBWnhv8@WvE$F=V3=arzG?8K%iD4r503_8`h>_J1s;UgkhO?hE!XSQ$QO9U?0K7+=1(;F7QI2I9i z2(>4uL4jES>=v=gTCb+?eYmF{+NH6^-j7w!lu1epCIsW$(8i}hU_fNTn!*0)m1oa z*@$r<9>G@)$oJEt5whQ0xoli=@fT)9JX5C~t&Dg>XA=A_8YV(e6x? z2aU_`7%j)+HP7F?9jur38fr!>&fRMASidRwJGUSLEM{+JpD|%tW$EhiD z(7qV^V%Ao zh%4k|Pz_ii^|Op*p5FI&ghj>wq!sOnodSzSL1UyQ>5w`ftqHN%pg?ki=Lhg#BpyP- z^VF1WgspeGP@m_EpBy=UP{uLzDYJdV3*&w9*LhI07i?zHgU)QobNMf)*25c%3I_QN z(q_NA2G)RiFP<8J&K{X_{;Mgn zmyN4QF9m!4WYT6TDHpMoRidsNIV`c0l&_Ka1JBn1knkOCy=*}AMPL(FqH^#Pd5G}y z8D|Q0$BTf1f5#_uG=*{ExlwMBagK!cAlLIP9-Gx(7cpa6#UVP5q@ra#TINiXFt^cc z>o;C_dUTWo`57Qj>ju%GX?(} zxuUB7Tt@;89Z%|%O2y6Jri3BRQV!C;b1Z*YOq}i<+Yht#Opeim`2#1|82(8vk}r>a zZEI;{=>$g4A@{z%zw1&yEnOgWeyXDHr1DYA_^gB z)vGw}1a?r|!;DbIHPh~wl7d%GUv1fd74zAV%PQ7SDlQ!3uRRy9eb~7O^_#xNg*XudJnm1 zQDdi)Z%R$u6qJLGR+m=jyW8g17TX&N0lNX-cX1)iNMw9Ts{6#`@&ucsG)K;66}W8^ z_?@><&Dpi?k5``(XJyPIQ8SP_>**{dQvj}z_U zyyq-i;IKphI8#OA^{Q$VP+@8^Og&^F;a>Dz!xwBqZ{LyrnXpXf7G=1gz}T(us<328 z6*-9bRWC%L0*XH?nltavm;IPGO1I3C-c2RwOg`rL^Cd=f-`$5VXSs4Eh&gdxYHVcv ztu^sb>C9m|cQG?8FV5`(p}yNWVfj%jhlDJ;TQ!I0l0-;!4nto%%L;l%=(v4=w`On) zpwNRMO?)X^u3}S2+tT2~0>qDN5dv@2uRRht;uYbCXNEJXir#OLBkX-Blb9~3kr@B-h8PkHM%&+h$nI0GxcP{QVG3fux#r=Mj{I|E^Og@5@^fZY%OuwXGEK zRY?OSlv2IYxokLF-d&{Bx%603xv-O6^WDj`o%}CDy};fS_!D)Z^*6gqdO=UN`R%{3 zKVr81zWTRdjJvvtKq@m3wW=>z;7@3;-aPK1YsA5Pgp!trmt!cq<=VW8*@z&z+F#+c?wV{k*)SDkJQ!TTq8hgx5B?zymNKi{wd{~of zgMe{kUyTwVe=>Dbj~4ezv>Tk(%s>fA!zM-nPUx)k=USV4<}eMWi^&KPFaxxA89DYv zV37NzlfhllUiN~1igJ*NkY;TH^LYnGCt`AGr=%FsvBU^JnoFq(XWXlkq;!Y*M{d`$t_pTpk%y5#!&5y&>ytJslB^8ZKa7o%X?bmwdrn64o}r(D0ft*v zn(XvEFoO9>QF6-Db~K+(0ATsq{N-oZ9nE15hO{I&rrpn<;@1qEEg`K*jJ~C36RYW% zhqdl&beigB-MX+sZ2--m7S#EOgrEkH6)*3VLp}phYNV8ewL2E2Rpaa9j_)%!4{Jyt zMAwp$UXTYOgiTv!2RgrtTb7-bg`@;d{28xHE?RQa-1U3XZSk#@-^?6rcxTs2X0%)uI?ZsaLm*kLM0Tnb>=%rHBsGzMV`v3LM2)Pz9aon zY`OVJd4q-w+xR4>Z%5LFK|-Il7zVjUfKrPq76 zqn56KdMEC{d5p`91n+(u5js*B=T*7vTZBCVe;eO2_OFzON^+y9r+~5K@?2@X1?ZhX zwpjE&crhXqqJSzk{%Zo0J#G_+zz)_pGN)EXR#e(*i`>fm*BMQ^VhB zG5}C5>P&WQqQ2)<@9&F*OXl5Qb0i|;ld=zDk@GNUyN8AXXhxZ~>xAX&{-kj%a94tF z>%QvXb#%ZrztK$yI$o*KuHUNae;e%zy6Bgs8?sV%((A zP$C<*Bn?j@=$=FomU)Ys2l%i$k77bKdG!La41k+Cxod_nzk+pkS1)Vv@%2&tc|vmo z6r}AQLoS8mm{ws?Uh@DL&CAfUiVD?f-doqiCOnC8G)1@zN5;D)`G}YHUWK?%KVmFmwh>7++%s z$sor{g4&R)kKO@i^gdu&@jouaBC^FSgOY-KRuNUt9118j73(&MiMwtZALI8tv}iC&$VY@@Xeqzo|aqN;#s)d5<;Q z8YHay%-gQXAE-j;RH?;OHe?NT1}O#4IH_|l(^Z0R4r6$gVaQ`rx2mFHbpeO;ftPvZ zySArCZR7cX+hH0Z82A^`%808^S8tGV-uptq=Z}9H5K|V7bf~38a(?Rb{3a)JnJ#fV z3^-+m$li=D8n+&$h`M~O%4mP<7*6Foi%TLkg(HE?PMK^&6d@N$NxeztPX z@z>{v>ygm zN%MC?KW~%LXWr4kzKztROX%ZGs>i4!5XL54O(}zB{JG#ioPS(oLQQ&G>|ss+rv$H@d|QtyOHU1l*)~%(vkOJvZjGomY?wg~kd|EZS@(H7``1_Ci;s zsUDIw;Es^aJa{%+fHL~9Ui3a}+MR5i=uU+^P_m)6`yWTFzBP5PJ0goB!0R_}80gus zl&kpCKi5exh>%f!>{%76{!A%n^G>w<^ewwwPa*r1@iOj5DZ4fWX2jA+Ch16LE0$dF zZ&%JT!4P|5w?95k)tn!GlJ(nQ!T9f&oMntdOWCoUP+fQ9L<{x8rh{zzGR5x6GY#3* z_VB6-Kmk@0-l*=>Gr@VGg3VDpSWE`SpUJ75qgcV@(7;`t1kc|a$!5M7Fb!-$q5-=d zp_|s({Y1nsE?roCLfRs@!Y^Y}_-*D;`nkoCLEv!asXn?RV;K=+p2eOfvr}vrsFIr7YI|&+-ur(hAr;X~C(Iy4H-2Fm8QFQr_4RFimi6 zh)B#&*gOCeLYG{Mr>W$G*`eym8U0~`+N*v1@{}`NfeW2u17Y^yCM|Mp$ybs@U8Yoz zvI~LS{Gk?`_X_e^+P0G*I3n+*N9HWqkM28$=mW8GfP2}U=qe6&@myJjZ_TsSOuKM9 zO?IC#}b0Y1{VZj;NNT6Kt&2FC?~9faWhFIsRx;3Y5aM6I}%#W z#gNh&&cyskK%^2Ia-tvTiLVY5WD-D7z?U49AaoIgX4_s({Pm+}NcG0*GgmCK19~zd@Vl0aUKi`O^&*I``EDG))98J^b{20??S9u?@2$a=qes+O9nfbO)ek} zAQR+(eK^o1iL{9?v=n**w9{bxs`kJtgkY3X6lC1RT;2t4{lNq6T>hS9&$v2uD!vAj zQ`IwY&mxVyX8_2e5PzE}{Z>tAM%s*#yP%eVWrO3SAx(|qKxRGpIMKG`F9n9|M~fXx zOQ9(=geiyDSWTD+^`20k%x0dop~Fnsp!gY+dPuVLBX_#2fwBiXOD=zzR@82db3TXD zjLVxmBZBTM%wB5cL2EAa8xE9z)U!Yy-Paz}Np0KwYGb{bF{%dK?j3l+a6a{PepJ2| zqmuV&SK@OBhjE418Vkq^Fbq63MRD^fb4rhsL0^yL)mQEhEp~9w)wb2@bM0-gJd!EY zml&-qZl>CqV%gpL^~e=I&Yo!)L-DtL5!x=iDV>04$`F(*uPpx%j$;V}IT++I(tiKW zH}HCdaKk|cd4@Vyge`w3b09)&7(ov|c=|m|qsh1gR*^t{sFx&BHVS}pml|CFNH~A0 zUK>P`$Z#2jQ&`HX)QZYxZ<4ApY}ew495mV246<-btXP>IeLOqT=N%KB*|XN|mF8tu zJbT>b5ok6Zv)#?Ohj&?O&x5w3zA;)oemORvAhW^1wPH_mwY>P(WOu#bO#OO+K?Ho} zYz~Rgtl%2VUJGcwJsh(naRqCvaP?sEe9NOJM$w=>eNuExs*rr zncQpS!4GK+%;%_c%dtza=y^cIW*Skb9w(y^43@Y|adwYJkE00qBt^QsR=%a`3`;OC zE8OjB98cJ{gBazYMK{>05!clMQ_*&`>UW0xi;Ue%NJ`7q>g#9GQt8+p`*40WZ zRGBStmq+x?rh#U2_g(W5N^g`u<8oveN-C3s5z&Oz;~6dN6|U#6@7V@Yhl)2~gI!k4 z5(*pc#>v-W$)e8z;+?~o6 z9dBOu)9q-y3~XP7c_LmXf%GF27}IA5o2g-Bt2pe7=H9ReL*Jz6-=A}_p+qE91p@X# z3klRCn0$|{N4bBNh}1&>F|)l-rmRK}RoZ6Dcklg8rrAPz^`_rQa1-)=6bZL*34;bI z5**;NWIHPU<{rFsWLq`Zzh_?&>EK!AkA#!Nq-+xe)lv}P$?MLSJdpe$Ep>YS@ub`I z^z)X%!m05x(|w2gu{c)yTVEcVr3sc9;x*a)(XlXbRw&3tsE6ftk-bkYMb`}Rh5-%r z-4Z+TUHOhJ@6v!FG_?8vjWTJk`%v9IyHl8u^M?lP=c-lpPxqDA3mO(wHapM?2HLR9?5&xm(NVs9iA#t)>j$KRZ0;8;|;2LrP&OS-s|fLi04y)!%s z-HGay0r{B`;TI0?`!p4f3~Db4_pR1k+kO6K(X(Ok{NuEy;gwy>vKt!^Nj;$QY)^O7 zAam5vUEXuo4;PVWq>K9&@20!jIRPdGStqka zW7XBT-iWl<3~al14Ei(;qI%j8hFC}(!`V_%zUY|Jm;B*>;U4Bh3*Z9kxLe%!f9W4n z(a{HHrtbE=EM&}#Z1ETP;T;u=K4n(XFMxncsL3U`wGm%QK+$VR(Q-$$M_y`7gp|pj5jOGxIs|-}`YbT~ zVqhsSg(%<5VaaV^j8wDeKLExmk-PcTaR~IUv6u5^jO-s_P4;dKz8=MpN$lev{({QA zZyF315IK(EYUwm_#QZKh`Vva`cwX1i`6x;>HmZvyHJyN^Ma8H}Dk21>Y`>E>LZx`G1sH$Z`$Y?M+&#iGf8H4JP zIcw;}{F6U9X{91p)u9rrAZjRmGl8f~nkG6j(NGiiGu$J|l{b>AUxzqvevUC^Q+Z^J zEDQXTsRz5AyKBV)i)1XqFEx&v{a3@DEUcS?qe2nQs&##Gxe%LZk_&G-*CRp}Xp13? z1xYhBL%2y}x}r8i5!tHnPm@mQ7w?uZvb7*W3TJ_TR@J8P;`4)orG##4u}ZU z-8dM4G2tjRLaQbulCI#g$~?7JcvSPB7UQ zqUzp&=Q3BHal=k-U)S%AlKN$k%@J*XP1^IFmY`^V`voN3sQayE2@uC~saO%KsF++jCFQ(WvUfKr0h6CnO4FACs^ssLI0gN+P-| ze%Z$fX%Nk|Fy%jPhYw)_g$8?}P&(KFw)$P0=&cdzd3{nX} z@ifAhYX*d`ArnU7RVy@~q!VLQ)qvp=03&=v^u3Z|TmJR%KejRN5uZRvouD+*C_!-S z$wYKVr6j>)p)BWYKScl??~JZgT%#`7aZ`q2pT?)ZpWH^TPdil;RK*C(>fN!tEcB_> zJc3L@-4pPY1VBC)BseSHalYSVabG5Ssw}@;Q&%L3n z|KS&fgQ>_uzh@=~%(n(Joyv-KJj@Bh(K!+yG~?AXsC{~_482vE;e@Q;#SLq;)50^o z5^BTC+49k)-|b zDLnmF5QAy7AT8vUic>}$8uM}4UVemdrA<|-|3oLMun!_gtX?-z`P9~gg_%$RZ{Pp0 zU-a`BYkgE;hQ)Uguq3m%5J=<^eKLMG&7alWrt)$AY za4(rctm!;@(^^2{{IK`S6pJI9RyuHn_LeLE(J1xEVX{khtN)q%i&5{2bJaaE2|1+F zL;Bm5W{PGQC#xuh4F?6>Co4x5T%U5A>-s}vB`GF>N$7QS?VALq;VTVeZMqi|-#%pU zV((9wKJeL6b2v3|S51LqecsHv)^z>3u)6GDPBG45;9Q{(9Dgp1o4b)vB(bXmLf}$|Y#;)Nl?|JV?tS7#r~8%u z*ft_4DPOk7ALC?(t8GT_jg2sCs5I*@!r0UFJwsArl_7HfoyK5mzk08}K*__b%cE6C zh{Tk_6#krz--6UPth?Wg9j6iQxvHGa`mWzjS|Wd5CdZ< zR8^|=7*fNKIek?mAsDs9kf`Q1-JwOF{qDaUD?a|h%wz*Z!9u19{epYRq|Y>Z2}6V% zR7fbqVMHRtKv29NDoN&a8mq#h5>fQ+K-pMPk#@;8Scppj#RZ;LaZ6EE-g)FkxivOc z6Y~%HG$}_R__DxZbcr*111WL0jnT@~KSNEKIMiNcBY;J3NU(Wl)YnaJyqnMn8_H39 zl^(3*;{>NY(vFhraYCi!uX%#uFLRed1_;?Nc$`k6W@chyqop5@oqx3UCfMxGJPM|5 z1!7hjm5?P#@J^ymQ3>H>j+8w-Xue`UyajRtgdf2>xrYt|)CCK@f)|IJ)T1&!stb%vLTc=hA_)R|+v)hL) z@kWYHm zI-Y&fGmRzHVtw5_cjAtSGv*w!rQk`Qty^F6Cu=mR9o#+2v1Acx|rBGq$|e^Lq| zz5(^W<$sAHu5}nFmEay74o}!$t;93SY;YS`DnLj%V`hiL&m(M8NF1kEfXsI!WnKgC z7M3{fyT>$V8-A^+O(e`qK~&URopb;bd8V6 zHINAcDPNi?19gG4j2U}sRum834&Le!Jw{-&sL1Sl>&8X+R?Q#@z&-|%RP`J%8RDMu zb63K9ss&90d}1A*?~5-1lDxXZ_?J{G_5?WiQ^~=~Z;Wy+*aTXiWNRK90G4a_QJXCz$UH6YDgLw%r^B!Rvd(NHrwaxm`44f->4 z$!z$ToZ9Ef%QQ3#=<&`@n+6Xz^bE|H8vGiol=LY|oy^ax{{j#;GY+5G!K!@@XH`Z8 zLl+I1K8A}e0r2fwr?&tkSotuitcfQ>LQ@fw*XX}ko1kI zyF|ozmyX5v?XUqdCDYm(@Qo84`gNP!<_!s{h!Yo-;^Y~}Y`6mdj-$`AxLRJQt@*c(B?d6xJ zaqaBw%YDnl?gFMY^SG}BHp6WOIWU`b z(8nPu25>n(R5IfynWvBT%D?BmMq}^Ca(1X{0M&f~h3pafq<8EPYs$ZEcKq6rp+8GI zB@x2E#6QJBgit?kd(vijrG--sefb?M;$Ps@EpkOkIQKPa>LEV$#3n_h8-D2Hgx&`| z@}DsUHsahj?8LP62tacUsSM4+ZN3-uty{oHo+Zq)J7z%2^tmq*XKoDV9QQ%BQ}Ym^ zYPpj$-`G_*#a|)=}nn!VG z*R#TQNgCvroVnTr5W-MF-?Br$5>9d1WgCZKWHh)MLt=a66n)J&SdRqCvq_WmK0a^_ zq-J?x7Yi*aRbzuF5hf9v3ih6jTmJNeY9Q=5rB^296(`BgIb_@SnHEhWS%%zo znaWI+=$;0G;UaQCX=5wx?uT$BVvdODJIBU{Mco?U&QhW(%jzEKjI%g)^Fw_I%hrX> zhMNfVdCioyo>PVF>lhcOhR=SERe755vL(=um8B~9CulXN(g#=v3%_`Z0WX#%fmGT0hjeD)+xYZIJ3l zJ9$LW^@r!56)*b{5*}F(5L-L|7^YQ?4I>4)1Lo=3;%42N`y%gWYOcpSK6rjjSPQa$ zIECO|ng@07&5qe4Jf5+a8rW7Du%d;=`BZ_i1Xgh1nj%FRG-tIN*Hf!7u7$iF>Cc0f zsyb#L{^m&#X@&ngKE>ujwo_ejy3Hp(y%&K`-#JTVNUcZsXoo1k0+$SIB@7C5n~HT=MSN41BlzP9ASfugb2Fd{bGN+n*?qaiR2V}b zc02zB?zEqLZ@1{~oKdmmuk~eiLd9*>-X;DU=?#whZ4x3duHIFxru6fe37IyYJo<7k z|M_oA92$C1|NT4h*EglY8&Asp3yb9Q)FjvX(SezdW@q9ZXk1$`#z$(ZC8fUnfKgg5#h+dS)n?fDxt$MjlveOJtB&-SdU{_KdWuqG@wU zhVT>I&ydpik~sX`u#s6~S2!C*Ls5^7ee3_9KZ*KkBB4E-ME3|AzPz}MYFW)weTYGx zJJ=Qi;d#;hOLB}k)J4G9ATcqWf-`X@%JlUeCgvOh-flm5;*k_Lyr&gkNCIw|jYox7 zMd&i;u{}?uBe(`vsF}_4uZ_fT^TdCuVfe?1QKaZpxEtxvhr{^PtR3I-uQwbc;12GG zkBQRn!y*6t|I-bP%c8`7fXNwfn`{WGkZR{^Kg(vTwLboC)<7F#oXAx?h5OZ?6d@^# z)s=j?H4Lud3*q$2i)tNao4%c|-!gvgD0t1CWR#A_Z!mkFy_X zm7WK)%!C6uwGE*KzN2dXIvYV^rsl-VIa_BebM_caO&K9`5@=|abFTC+$=F<^(%7O3 z;g9CsWxP!YVqU|Y^0d!oKE)FN{JG$2h~|?(79=@)?Bh{>)*An?fA>ZXmIQukUF~yyswe6uXK}RH$?&(^ z)}4A0Q=GoMDBML3KC}iAHcyo4?MI}ulr#`qVZW@|VC^+-J9(nSl!T}ez`gxwW?`JF zyL{CEpN=HW->{dM51*FX(EdZzjP|r~55$TM%tu@^-GjC`BoAacugMX)?Vq3UEaP~O zBv-q>dH*f;FHgUg#KK6WFP$~CTKQ>jmx>9eTI*Gy{u~b73=%30D*M6JcB>yC!MQ1_ z9)(6@KzTa)L+yLFISrXW39o7Lo!nwUuXyVv5?j@m_oWF?d3mfU5ZlczwBf<$uRBVH zxVG((?s?ENs1lpq-$J3NasLKid8fw-@SIm5DzfA#nUM>HDg-4{JBgt`dCu3fOp6d0 z+RcU=|BrVD?}Vhu65YdHe*5_2%#C`gYqO{X1TLjY#A^2Q%!*lr=c8qIo)vGHd-@Y* ztq-y@lbK${$A(E|+6^>EleR2=CUhbZe%+berVNp5hi$>tNt|p#S+4oE&jwQS=rN(} zzf|LvEkf6q-v5uL^A2b858J*SHG-Ci6{Dz{u{W_dDQb_{Y8ACtLlCPqTeAeER_!WE zi`3plQL1LmlG>&1@4fpx@AIb|lH*8n=l)*Td7bCyT7B%Ln)cT*QU2!a!sTLFArAFG zkMkQ9&V2H$0=@qHs=pt<6D zC{1Szr!RXRM~sZ-ic{l3w$+iRB9ou`P|(ucM~vWURBn!8*GAN% zGZN=FxObTBIfON_R#S3IKoTtmw5S}3kYEh%=SU@gFLm^UI>N{j9Gt7k56Q|Rnxo35 ziMVfhBOwaDtD*Ow!L^;lUN{Gs;*9KpOl?gS9aV(r@TN#wz^DrAVug`}#7z#@?%w`6 z)JR&A%%;Qsie30;8rZ27rsV1VV5|pFw6SARpiT-Eax?gVY8q-# zLM_(#8;4v?K?^Ptl?FgMxJhlrA>oE_L;FK~%50(hRK_`!q8GbefP3e$4f9)-%grZu@8b$cD5fs?A7o&j!M1`{Xzp zUul`!K?Ku#Ju2)G@!%^*hqx!Hgq#mVJHo@N$)1xPdXXo={`^MNo%RlY3dX7$15R)J5%L1sH95SW zL6ksD!VwS;)0!vB=a^LU-?ufbTapCiZ86N0{S~gG;!JT*%RI6xr~T04p5i+7bjL%= zT)@t3_8boAFVa(us}PTjT?SOBSKFc+=s^`2JTHge^(RVhjuCIipW0TFWPg^;JF;`3 zd3BrEZobabt^Wo80J%E_`-+=C1u`@F>D{Vv?fT`cGC*z!WqN?8%DLlL{)uvz4vOwZ#_&duTo1OmuER zQIgV{u1&QUM3t3W1~XOz2>tVa!0df1tJ3HQ}Vs zBbDo>1|Nu5kEzuj*5&^?CZ!0^=y3Ii=rA>QxO&dqWCsEP#$m~+YHj|4R^gvJp})9Y zOt>eRs)U|(R^}ND)7p2@aNevUr2_B}T9?^{^jz907&J(EjyTxG^sNOv!9k;--goRb zfz|hNVxKgjeO^lWv2b{XfPcbq&A?gPx0!C??+SQ zO5#;W9_OtjU`$GuT4+Zn(tP`TR{=`-kr?VqM{!@fm-_{GXwi;(*j1hyJCdzZEo-og z+hh|BxbuK=fQ?F(Z772nc0gxsgT~*X=_k%Bq3JStzi`$3r+WPs@VQ^qOs3D1Tt8f*05p#cyR@azN?4l4e1*~8NRa=7J&kV%j?7A+MI zIu+o+XC62l(O8Y6koy>&TFax<{C@J|tBA2)>@cDvuQIG+dj5C-FFOd68(zrUw_5w99OfTL>$2UnaqJTWrX zz>dTMLw~f*SikM2kDB~}NzBi|4bk6>a@aEF=Nb}Z#RK69Vv`8@7 z2>xRT>?mjh>%r-Lb*`}2Y|f#%|8;}#teDVfV+C7RXmHNF#}aQJLHM|9 zfTmptuvXOw$)q0j|3oVa2R00n#;V||78h52=JET5WnrEbkN$WAZlV|aV2W|mgyGgn zQxSpeXeD|_$yVTOuoAKPiSqugOP#$63|&`a5w?DN*GQiyb5Vas=gIamTn9H5*r5EQ zYNd3~uGo}6xFTqiXK7_=!lCn!`9=ha7FFlZhRd8qUnvWkbAzpcgbg)yz9bats5EvB z-8V#o63t9ucWL+yALbq+zXkmgaj_>(F$6Norh*DyTV_8+fNq-KOVtFCUX+|fAndsF z#sJaFWzRUDKn!Ue(b3!c&zI&(V}WaIbZlHbBCtZ8EYe>sdQdkfM{l#2qYykBK|OL0 z3$F2f?*ir6^p4r4fq_(?xO^2>+UdW?S=8&WG`>|iCd!|B1l0yD-jNrhC`qM17isIC zn`;?;Cz8(eZU4-%BL^+=RKd;bLDuUo@Ha_2@UiF#ia~@0_atJ$9Uu-Y03JPmVUq{l zbl~P;;(z`bEh~_R8f?9NYOEJeNAn6Z#^(ZWA57hH=DY5}IJ9XXbgayOI>CUVhs@<+ zY#&+dcI>+i8AHJa-!H#of$=e1Gn?ptPOMoh+&Eb8W2vxklDF9r(T3Ap*5 zXiD(VJ#*iiupf}Gk7PGeuI`E11#0Uo&!&MK{qpAqnKh2|Pgapkvd=UaH8oG-kDogr zW=Sr2Pl5qI?Qn}S)X7i$Bgnc)Q;hTi_hI&XS^DgiZNx5;m|O{dOO=F%n(Y^o2L`Rw z^G|l0&SxHQmL#QB-R;--6)X*vz8%q#QjHBIXacl!T6T3HWi z^m|PuZoQ3`cOqf6`SA6xo{5j{sy!-(`{{p@COkCrf90M;!(uEpsUX#xgk^VLP>Me= zTHL#o!clc2{Cr}U5VFa2Rcc3 ziO4Y{zGIw$->{#V^FHkQ3U;*!s6C2Nvl@ESaf(y>Q<=-d!jCbkG$3c%^Lo$J@RxAS zUCPY2tIU;cXY1M#)(Fd~QlvR1Vtvj4YxfBxdk@&QxK+@RwM0Q)?gT=b_P9@eC!HLe zlFZ5$y6k=Jy7<9?t+1>7Z7bpd*bvS;v;Xeu(ECv0srVMTYy_Lnl^OP%T0F+tzVe%} z#quM0Qp0`qS$pMo8pjlG^9=5$yK=tj2t6|vpC?zMHJ>I95!$ZF`Xw`bm4wfS1tj(S zQQyj(5|IDQ^dB*ai#<0T;{?y2YP&cMW@mhU@gy;X3HjkBTO2;=>K|`l0loKG#17m) z;nNw$b}0UtvH9-A>ufa%yQIi-#yF2QI>70|0k%AN;04(mUf{D{x>$$BDxocuunDYTl?W|Q@%@?v)9ugX-UmOTW(DC;#$Y(Md}{Q#{hnhc{#)Kb?^TI%-P7er5U zlh~9OLtGNyU)|C29SCMzP~ZLYisRhBgj)$M`Bl@9vHO^W;WHhkErFA z++##mr_ZNKHRa%$3^cD^8p3EcL7_^YJZ(9=Zk7La^#@s} z*(f(FAXOn-L3+A;h2c;baH6b65(`oDL)w_cK~GLg_QEzv6L-*Yq_pNBI=3ou3tj)I z7R9`xrJg~jFh?@@jyx|16-Om&)%HfSv_lczmQS}hhvqy32wB~H6ADQ}yO4J;RCdm< zm9BL5@dc&o_!=ZM;FFSX5%;cwo0&&AQ~PaL%{9%wqq1(YLPJ1PzAb1I^3V_6+j1@Q zE?qilD?&(&I|KJ_o9Vl;^JM&OihvA{vD_I|s6@w$ur0fmC)d23;&K=tjD*~3N$9y3 zpjDhD^)e$Wo~Xqhdg;NuBbg@7K9wxT`w`k5+5^koI33y_bxVO#$pSBxq?Ygb-~5+? z8g-<`kOgi}f6~A0-mU_NpEF88!NX{6im*`WoIAwebQalsw6Z3l2{(X{H?!|l860&1 zy3*5JVG8LbHku2d=A$6nXjG5k7B(IFV>5E{i8M0I5Je~86()m(6%`s%`wD-=W*dS{ zpug2%($gHUXDVrmU)kXT78Ycf#XgRQ#>Vj)oLb~2r&skPdnQ|LYt@F#;{w?xr)glz zdR!}^k=tN7iKsI%;lrs{n+M|_;*U+vl1JOoqe}yEG;3^Zwdr7W(ZN*v_?j4n2T^{R zfK0{+i+ab&GOVuovA7fn6_Q8nfeR_gC-0w=b#f5+Y%%uHm99$GP~QP-0mD*jtu)4_ zlh5g>K1l}#RHq6JHy13B59_1kV&X zhh9aL;p2>jk5!nwA*{=$#xD+?>ca-;VTjX7o5}_*I(n;T=Hl;XL6kr)KODarKD6IG zqM>oR;}T#7&+~75!Ny?835E_bab3JZQklaTsbU0JJu)UwjfHN0;>p(~z=I=_W_Y+n z&jMfJY4o2}qw@w{8v!NHxo*Bjr-a8+2V=99`ttvbEf>w)eL2$%5;uvcex|(qySc{A zxNOh8H>Lmes={IIej3nS2)($Ar@dg6QkwT3iIO*m;+#Z7+*({7-3J83A|RowW0;<^ z+l5YFep~vl6-tcT1{|$ld|ct1EN(zdFz}A$xFUP8q!p%XTJ)1k_6>3Y^`|q-`&O5> zM>kIzOD8pN$n|`dJQ|UA^Om1bzQGU?%;YEZ$sy}PWz6#>bT2y|u=oD+Be) z08OG7wt3-{pQ0JLN$iZ-{sO!OK9_#Id*J`zl<`XLqFOeDvP2O(>gB}!1=!tMK)b-^h1 z(+P=Na*LJCx?#=AfS;H)t;G*`UvB`_}{Vcv!`>HS;Jb6Zh2)h5S=v;qv%Ck|zNz>Pqm>5Q#!1O^?;gM+QwG z=lPG;+`Ld>^%x$o5ENc@YrFCOc2DpAU3w>4es+>Wu`gTncdYLyYs3Lyuem;JPaEyj#i#RPGB1SsP;rACg8C_j+hU5xcFWE7iokLWJ@(Ui6p1o)13Yld#2Z6f1$9E9T zymS16lSy9gRY?)mvzBqor{_!?5@OW!$vqrOS+^~-x$>epsh!%YcrP{pxwDOl%Xo^1 ze4sZyL7U1jx`7lJ*}H6Z^+!aCT74{T(3k^WHeL@)K`qFhq6}7cHJnH@FrvR|n&6^H zWVKeEXlf?(2=tPCXHGT8u@vmz*Pr$(&yoZ(6y@K0tuM99k@a_{k|Uj@9g6pi8t4 zRcf*%T{L?xS{I>g1<7wAk4z6qNV5Qe25Quc#qJqHgUyKd%^wPWJq*TKoqeQ8rBo{! zuv|CF#J!Rf#wldm*dcuy$0oAsXm79q?5e)U5Dx^4vs#P6Mnm5K7d4m8O_AnmHDte^ zw+)CSRsiwg){$P%o3NSpW+{GW2~T>NUB4%VEX-xFy_ydk;q%l{Tkth zd+zq!g;o(NNZ`tiaK3-&yVEQ4U+s!lAFrO2sID)v_g^B@k)?*TS)Wa~ zHmdh&`2cy9N{L#IZCgN@cjxq0LUkMM4iM1oeg3-(!meo*XHF226AE31m>R;OvgFj}KnZ z9Pbq;h2CH%Df;>v@**t5^!wFn1Xs?{O=x`XH|%1W=uCg0=%Np)d9){2BWM+U42jM^ zNnF5(v5`sb1b{HP+>F#YKVo&>>u}NHEaiO#cCbtFuY!!HNA?M@Sb3a;eX8CgEr-Rj zNe!Vv=_7FsN)RA=s21pI)j$3mv@OA_KjBJ zE-tpIEkhOZSHI}hmlI@$VF*3Xl?p~_$T1b8N66lPqF7{ZLI55>zv%a3*kNE`AIP#r z0}h%)qDcv6>Rl&wwp2D*EA`bNI`d;vl5n{dTiB~VR>HzrZi6Af8$x1v6+P|=v!4x> zSUyAjza{zpRA`8n2|xor%DC%VK=ZVrEwDC4;o~c_9s&bKhU?MG1kxH5 zyA^ZdRXS7#giQ48Wv>^Wk;dTmG)OjDNrJs3za_FY<1*;wTkqVUCtHV)>TmY0UQ$sQ zZO4^{@9w%#JRl)j6hYih?HgFIJY~zDtC((X3wiHH6Q9Vu~KH<+pjFkpY1UL#;WZ1tpsMw zfUq*%B&kL~hW*Thb&=O#pg&^IOwoiC|Bb7bmO>)|liP87UPB%KDvchLcjBBu&y0%S zgbD#RqR)&yAdjC4T*O|KGdH1>UBtl_!l`;SIvtloD)hZb4y2P`L@m{%lnV2kCM@S80|O81Xs}k zgv23yGt1>}sZ)qy0SOdpOWoL~kJi=Zf%d(p(`xWe6@w;%Rx4%olG*++S^J}O>2L<~ zh@7ApyWQ^q&yu|dEcBWp%<6Ndcp;WD@ep11SU4U_WD<;R`K!SGe%6H!_KXRB0of`r zRLB#4Kii@&I^mow{VX+TXWiu|9{_wrC-Ez$#(iHrr~CDR7YSO8RV(0?Ci1*{q42+E z9)QUkkq=&z+r95w2d8DLKXkiHxfjm&fd-WA{q=Rxw^ESNu~4@i7WK)8v>UuQIyO+- z_Q6?L2=T`nV3<6U?ztgpS^WNd`vDD=BLGJ9NogT`40k**+QI)kme%kkiX4-M8s>^V z8A)MRf}3)nZ~HYv1=U%-G${U|9SxzbcfU*}(Ep;{=p@Opw}#?E21cotfh5=(n=%&j zsD(JYXxkZUshni;4Q?#C^)c5?JdCa0?lLXme18geM^2L(o@2na(lk{sCU|J8ZBY5U zNzTfZ7O$lx`s85E;HOA=3(4{-p9lPYP9(eNWQ&P}_qA!%=oDK|X1y#fv(Y!&eDwxu z%DZks_{*EyX~nQEwLW|(O|M!EyE}Dnue_8w4HlF1kKEN%Ccu^!9<)V$wX>9qdq3kn z?-17#2IFzg`UNnW|0jhh%YOLtg+uqbQ;Xbu$kysq|NHgG#Cc-5-RuNS-|z&>CETy> z;^|(_dB3H1YXhQYaTtd@jXaU4)9NDYHcUeQMG7dN*xo+C7l`U?xCC&3Rd>!Taf7jo zZW6iF7dvVpmufS&llvU1ishhW(&j+2h2|^qSiTUlI=?{!fx>x=SzP2#nX$)d)nSFO z@i1_GjO$AslAF6mq8ALXz!}YVQ1QpJ<+K#}u(hzJhyvR-Iw?3A25>u4?xhP9JB4XB7rMZge3 z#TSry8NWDgp^Kgc&-Ha<>+Qf&QJw!$p)UKI@M~;fo^o4 z*J64>w=bLUBLMgH_bMjA@}|xHU^{TnRpabwe{in{W+pFPlUV%GtdSsz-{&&Zd;1!) zh9Yke?!!JNt)ULsD~GZxTZVq=X8*|4 z@@>G2MHFM{MSRY@<+%m3^*)C-TvL~e-8#)9BZc=6?PZ>Dqhp&|GCT}<7;#)3+}Ejb zZf%|EWcWLG)ALkt3r=3P?j2@bu;Rp}h<+vt$K>`LgS@XA-1{3qj%l0;QPd{4`QcNs zgubue0EvblQQ%80%SsHn4*mvTFM!M2%c!=Pf(aB)BSv49Dy$ILVr9{ET2>+> z1gb=}GcNh%X~o8NXQLT!gb)^M2m2>3=hF=ThG0rBvrEeIAz@V(bn2`uP9MXCHM~Jw^Clv zItN|1;r!^*n{lNZ3~7ai;gN1m&lBb>f1%sux5{7KiR!sZOQT65%EfT^Nk)0{o+;%d`NcGmn%Krmf{^F6XoLi9Mpd?;TKo4}ByK zUP9g)Hj@`zR0+O(p~lgpeT#406qFi>aR;GLa=dDK>prNI6QT9Y^4F^$kW;B>b=ffU z$jy3iwy}nCjK_iC=EINNuQgv2uWUzHdi8YuUvD`$8RK1&tO{k$N98^Uuv|-5?)>ty zv~6&WPW!4AAcF3EB7W{eHyc)2k(X{xDId!gZ99@Q2jR3?+i}ZM zvqLm+0SYyvmR_64-Fo-ymxF6212rUdZ_4}kiQKSjff=hkKyru5?bAJ?2d;nX7Q?7! zOHwGX!zk+jw*ZAUrUnh*6eYU&!fz3=P@%{>yzd!?vugPvEP*3Y4eaE&^@p^7>_KV- z3AT-C(bDqC5*v(O05AN_Rx8!GoJct{pEEf%_rF7C5wRA|oj10zFFE%x@Z0^>_mkHc zg_UL{Wj@kExAU(JRF5QOivC7dr3)&}+8WBaW>>DvB0LVlSb)+@kH$~t()gqOGqS#S zQ=|}RY z*$&>il69TLa89;2t2}h8vcRU&K6PIr=wJTwQ-#~KIBx%jyRK_IXYzp_kEL(T!#X7o zI0Xxh(4{sn55%tZbxX|?MDcz#4Gd5hdvAnT(`Kp>k!rXSVYX7zXbe{xjc@xXk%A)> z@M%|GRVb)F=gT&*O5S1 z|3zO#pu7pk>{n%XC>s)QO$K8Yh0$P&y$VH&ff;wcq3EhF3S7{XC-W7Q8`Z}}7g&A8 zO-7omz&J+OiBndZR_rz;qMJ2Gcs`j!mhA!GStQ^wnYf)!dOu&7>2j8o^{=x4od97N zk$Vb@77ip$uvL9?o!Zop5spdVv95=_!EVht0w-151Zj)Zn`?jdXVMsP001~dyIMA| z>L##-*>`^44tRoaP&q++Btvn*G^^-o4Sb$bg>kw5oiji7dxMuKG)GOw5WArm{w7k< zgAHWG&2zfWDwFz9tGcE@^lG+5qK&bcGQ1yO2b7@A6`l#&DsD-%T(5gq&6<9gv5_%H zpDc~OirCIwQA`Ebwm-DLPeOCnO&u{XG{^O8T=5j1))0?Q^oW=M1mDeQrX{xT^RwGNj#?1(1mKQ($z2BHWh ziit?Y{AjQ8%pUg<`}6p>z=4|p{9V?V$*BXlxrpOdZRnv52F{IvPqq|M#By5j(v8SKE8=D-G9aR+UG# zy3*USTSW)^U85)epMW1UJpO^^rOf$IX*pVVRgTX2DnBK|4s{S|O z-*7|Y{MJ1V7{9i9PS>6>0-cu}mPc(-btpeu`$Ww!?Tnlf8r&PFc2J%}61$^TQUrVr zn}g$!TNFXVZurvh@IYrc$p3K*Nq82EBvpgR0L%b^xlKsq{Xo;A1$u69%|9M!-u#}q zk6}n7YaHG}O0w1WQL~z|qwqD|xDrDIGqayH?by9a$n_P#YNg}S^wai|elx^VwyoB0 zZvSn&Z{OaBKXtqJI*YOFSCCgjXb{7_pA7RX(l*B{>@>hIxj{y`^M^-_u$|^Zjl17omdaOPS_29wveJ0@wkzgekCLF!*R{Jv(YAeqn%W`{@q-4O072fwqz3ms~FIfyI zY5?cxw-WoZGqN?35AXlc^aG5ZW=^eGI7=4EVNp@s>fUCVT0Gb5yE+y3pvEyn{e^eU z!? zSP?i9x!~|Rq7&amq?PsHkyQrOeVO@ECMjSS#Bnaq7+TZx$7${B8#M|_JHA43345y} zuO;4Plv-%lCj3?jOAU6{9onDrW4X8+w(yUx$U)PVPprV<=bY7NHLbBWUK#p)7pW;P zdtPDpBe3#Eh0Nzh@#88tVlQcN`5YJ8ABwMJDcp!UPxolYnqy{?!9L{lAPNt_BJ+Vi z`@-qlVa?Ptzi2cHxJHva{Bx|EyK(8!{jt5A2qp+UhYSywI^wreoe(hF=+{d`ajS8F zwoCZs50Yvcd|NM{tpyG>o0#P?RfOS-Dfjjq5*^R{BwUC_sRRSi)?zN90CO~5x_y`k zY-?Jx;JY1*q`}06b}lnJ=T%7(2ouuBg!prH_B&6#dY$eakI7)L-oOeHnCl&L8)j;X zCML5og&m`)ZPr*h4zNQaMr-4q`lIh*9IkS9E_AQ(#O$8+5b|ZVW(D9X$F11|o^4Jn z`f4E`=H>Ba&;c+a&8o?$%jf7B8-axKco#ujl$*eF6pn~^@@T_Vo-E|bQv5SHP}dFf zB(T;?VU0}t0O!py{nlWw3FNF3W^WCcR)L}yw)wk98k$z10=AX_fywrWT!%IMDJC^~ z`?aoPhA-+LVVPf*5f@om)d?#J56;}RsRz>?Ic|gRY;X^ArsDTR;)m_+Of#ykJ=??o zQwW!1q3b!zAZav^_!4oQ{+mREiR-b1|m-SUQbT?3G~wMGYd9UDR;rOLL5 zINKZ8$WMKDJC;(H-Rz5}6X4{cdhEL0+=#k75QlL^KM!>B1s&9QapEzsY zP-^Kk`KUuPK~x0p;nP$8{9@F%fgsqD|8-6WIMHEuZ+0sLH#h#q87?|oP= zcDJ#qs*U7?)>36C9tg_-?fp1@#0NN~-p-I4q2r*o9a>#MO zy1!>Gxp}FP6P-m-_yYhUfPs}9YmYL5_ZCs@{`JesuuptTy(EB{R7|5;ZY$kJYxK20 z)`Vr5_-;1ECh>}+bgfcRT;k*@g!#rBSb*CeaFYjo{vN;>C_Nv`M?^|<_Pd5Y)8{{6 zrr-Tc%Z4TcJ$C96&4&VXv7Y;tT7ON@xiT6tH&+Srw?C=Lytneq+>%-1fG19#wI(t# zP#n%ZGgfqvjKgmOr2htqKkT&h45-Y#ODo+l00Cf)4lE=OwHgg~jHsIY9S3ZW2}h&{hasP z#ghtp@a4NpWYq8~?dtCl=j09ot=Kq1O>Bzz3tJv%vfDSO1cF4+s^4Xb8jCAQepc*! ze~0s*B>Sj4@12Y8qX&C!?FeET1rxEPu`;|YU~b}@9XHojNhRcSqG1KoCGS5ej;0m% zU(+Z4%^}7%oJD~lU;*aTCajO5ov-1scj5|qfU2?I^(hlUDYPBPZ^G@ss|K-xQ+cya zxsb(utDn-hDfQdBUiKp%QxP;u*OF>YivA{Me&W2@$ z<8~_pLHR#8*P=%WIwQroo;OYCvK_R6I4&r0QjI?g7^inmw3@%#ccJ|zVMPizc?cQ& z!r&_jO3fh!=b`SCP{;P@}`s>h8g$Vr+RR7xK@+|RO~k; z#{?i%T&~ipyK|UxYVW{-_>%GvT22lVeye~Fdq@8x;JW!k91qf_M(}P%4*hXB?csY2 ztTsY~R?4=24xVmSY-xM_oi1XN+Xo^(E2%Ke?nyt@)7}EO+=aYMDW&{_x64)N=p)W7;k%g5VZntuz}tIjBV3k%2|3%k3A-YScOZSP>X(Bo6h+0l z%ar&i|J;CRgdi~d{*$KnvA*A!LUV(4-a^NSJ&`vKgsUvPR6`*0{wQ9hp)PJ8M5bMy zzraGvasuBT1h70M?rF3pL4ya?Zg0efalm4nb?MMLeiWZDoZnJ`QQtI3_BCqM=!Fo~ zdFhWYf%E+L|9i=-7i~qmM_pE}QLiAiQ(-VL%u=+S@S+mLeO3O;`oyCc>D3$+}#HOy^~$ zWlXH)_^jt(^bb{34(iXOH)alt8(g-J{4d4)zF1oAg+?(shqqR{f~A;Bwu zZarA5f2y(UzHCdRpL}>yb?+Xb`EgMopmtH`1vwEv*As-P%kOJ2zZ+kop;ZrAWYc^q zO8f-sGA)7zQcvL-yRHRj@sy)j0eDd@V6rceUEnsEqa!_5jQp zz2&#>=p#sD6~>OQ)<^KK-Q!rc3s^kc@pA1McZ@D&kR5mn<7BTtm*1Z{AkOvx_M9A( zrxGnNtpnhStVjmgN510am+fUY%_Y6Z71PjO{%HyI537Vy!}InfSN$!Ikdn9=wms8Z z&jr5smHtUy7X`l76So4AOLKqxhz@-K1v_I4pC#r|4Ck13%2gdW^ejT;<`=twilIQz zA7#%>j|kZUKP_Xd60Bp3*tyL|t!Dfpa6qmlgAd!zn8_|N-B?y##{0;#pFDVgAV1_u zs85o5k(_8H*!(_~<-}0T`WSsWq;!}80tw5fb+=I_Mbb>GwS9K{m3CAjEI~|cpXJir z4tRC;h*(d)^H^GiqtFi!8@Ak~YWITjQe90wRA|A;V3#`JXMqAsu&!IU*1o_dzFs&K zJdwP|MjzwxQDG>QNrkaV#m$)UeU4ekptV*o3NRNmiyRYyC96kf%Cb2ZQoa!06@TEy zC9(;+2|(#SL9aA_2F>jpOkp03&{-lr)B)t1TcYmlUL3go$zt@CU>7Dj8~AU+s>GqKGG?^J2~_Ne{9mGa@m!3^ysj2>iMnjrs|%^DNPBr5QenIS}=nH%Z3Q zZ!B1Ji9?`gOcIbX*`7GZf$;u9?llIC$dH6PP+7fRcX~%*K?UY)rgq-F+(0EcdJV1KgqdwZ^hlR z0O$?C4Njz*=8A5&f}H&?j@D84iTC#hT#Xd;Bth_>J;LR007nZ@xh(Nz_y2hIqxfB$ zNk94ROxERf5s2jXE|d71QSjVtIhWS_>h(AP-UjJNBJ{ijOR@_yiUteCV~Y2CiD-Ir z&4B1y7IQ@6up7l}tPT22DeY&uCCDlv2iT>+W&{gBFQpLKK7ZKRe=AI5Lr2!+Y_H8I zgz=`KJ?i4bt?k<~+7GEXNuV{6D?umjo&VkpL`; z;c!)EGq!VjhbxVl`c&W~NzmIz^ z=l2XK{5FZ>03w&+4;q@5-uBe)zB_S%KW>uT#?C)`BW3R4g@5XWtVTG3F22`cCKL(^ z0t6$q*=D0pwvEF{@0mA#>#Jj&+wiV%@4iKzCDQDpth#iC-{VMF&)Ys8y{Ykvi3Trg zrx$UFk5AP(%7(jXXG;g_?vlm<8+4`viOjR8l8{gJdG>vTT>4ehPN)#<77yvEw!S-O ziYe0%KR!))i_sEypo*R=C~BRG0wRbw$WaPyMe7cci7-`XDKG~SJ3YuUSa3xkKR_D) z>VxQkH4$Q`hIFY~N9XH4GaNOKdb?0LPQ&Z?X6Ul#^8?fIm7sYUhu^ht-%zH;isl!& zMY{)|t~qua?$Ha!>xE4;5M%dXpu}^cx-&>RX~^gFLA5wc*=Yt%j=*E3sUR-pqPT?Q zUN%J0ORbJ%j30>helvh{b*2Y}k4SZl6b3B^j4RR6OcMtcS{(?R%F6}$wMAfu*RUg}m4Vi=63$o2z?cRIqP=$cN(I=VLQ-neo+E81hWu$Hr@B{EMcvO-4#K&etFIz13 z-}Y76fS8cQW9m|7;~yd)y*@mbocu1<>=nsX278RI1bvTU;AU;!PURRr1I*%py*hNc z8Q4EceqlBKP!w3f)Xz;KtjPl@GCuT?e2N{S3?up(u(E1EyrD8MUXKRQH9%S9tOfeB zY`zzmIzI5O@urI}G08}|Bz<*sFX`bcps{SLljH3^G-PBMjDqwfS}bc6KcB*FhY=t* z;uIzOuB4R4Qn2|H$`G(${Z3lx`;8Q9lLv&VwZt05XSQaoF`?4?74l^1=6Ad@HVL0R zaIxHyL->!xbH0xz-wek3QLsb0<*LG)JZ*}Fsg1lMfj^17vOoJu4p+Yq1$M)tC*36B zBMRvhqYQi8Xtjp3JycUZROU)pt}Y@WAM9IG1y^z7YrPE}k(^IKJ{d?KvDP7+Cfev-Gi6{;?* zoSr6nAGbgCt_6;+d^T`z8SxMO#E%a+Q_e3I|81)573=xhV^sO}K5UOo7#y)r z#dve@)KKi&)3qTp76b&UbaU;6Ke@efPA=mRZU9HTu>_^^V&Q^6j{$GdYa)DqS6nnd z69e2oOt`5Yzh;?8AbdV()98@-`n@~zgM=q0jqG8T2=xPe(k-T+xG+<3wl3NgnT-ks zxt(YB@*tka?QWL3!O)mRVB)gP>Kito@LthM7fJpqWndEitwWp5ScfEqM59-9HpsfY z7CLQmlfAwvk;Fg4`emUk3wRlM)yo>V>i&*_%-@Zhs!&E6?r=Eq-=8m+yO@6nWNaY|Xk1xWKFJ z>m#yuBFp`^rK*JA>}S9Fc6$nhid;lKC1s+2@XeLGPOx*q<^3QEz{SyY*00N~yGk|! zguuW$9bF1fN^)w$kPIx9Dg;PZ8Hzz(g^A|ax`n{4yC#t^MCc70d+}-wLFn~N1akE{ zEf@fZXyhdw>W0~^RXST*%68KxDb8Lrh5Zv`@(ICc@m(VdxbSQ?aT`&qA;K0w`wg3O|7BxklyO)`&J8-XTq^!D={jN>G(q>8xe?AzYQ($Q$G-t6~ zT_l?SFt_d~n9CnacExb-i!c)yUZZkhJKgzh%|*GR_ao~z007ah!3Rr4siSDCra#J? zEL$S9J_-3Y+E_n`DGPNMpIkNTon_CmkM;^i0;`@-eRdhLPiu^HDtxshHIHurBa{t} zWp@PUdfdR++H=NL!rhH2=UZ!yVYl>d+HcXy00VfScJTDC)so23nN=gDJyQm!Nm7SL zZaq!Kwl0wVHka}V{s(V1qh_JQ((2A8CxO^$rQ3n%-sEW=7(`I;-_Y`%!R`aRol;-~ zf4wl^nu(!ME`O=Tc-HA%PjBoO!If1at_fnHZZTQ4RkQcQI=g%~;Ib>~{;gG1V&4{t zIqO)-tQ;nXbmPldl#d@#Jjj^zCCOrM?-kqWa}HXf^h9M)u+UBzQMgB_GE+mlljT~Q zM}HSNWom{IaZ=Ua1vZ*i9lHD^$H<_!m?Ajx-sFebivLs60*daYGg+3Q=RPxO1PsNI z#wwvleS>NZqz8=N=jb#)U&zfql=y&Rv_>xp@#>hK?Y{UC)(0$4+MUadkilZKwlamn z!spuW92)e?>3IZrZcw?Wt^|3`q>-KR0mJ#mMGZJE^DuAbsq684soK%|K5nT6GW92= z>S$mw(^#)<`)-5Jr$O@nynArWd=E?;52!+{P8;hlQv#mdI2~eYe^tGdVl~0N*qeE1 zNwY^(Y4}pdv;^cC(UGh`8G--j<4;XYzxeRk`+Xo-dgC5vV%*Bmz*%Ag%XVI~L5TEf z`Xj+XPVZ8hJHwwGC56Ef9+WFQ{$XE!t-tVj%!*qL)x2{0#ue5tKC^ z5e@7vmcgdgRxZNS&`W6uCt?HUKsA0uu}SKqL%a2Sn`k0Y6-Z-cPS)z zc@3mZB%PF;3h@*8z@sT}(71=e6o2DpH7w-5I}VUjL}O-8^e4cE5JPFf_P~Bi`_bnO`UfXzKw(&okdVS=)1@BVs`$@h{iRFsjt> zP)Koa&i1i^NtEvN`t$$tMjDmi<31vO;yECC1Ha7ECN6l+k6GV%#bswoAd~1ukQj^M ze3TKiH(&`--959g`uJbZ-dW<^O43m_9s0+RkKkSOG<47k%HbcfSQH}671Lr5Ea%;)ro2v2V{ws0AKa_kt?LFFRW%bOUR7j)eBBlYxSf4BZIID3!Rb~e8 z>&h_brrL`4AX${yY@M~oFNN7x$o~ecEPC)lse5O>(2ZvL0Bou1(5*X{4#>j!wR8Ok z@9f!P+cmi8QD4#+qTp4(%9@rNlk3FBzxY|$P@CMkXG+V zKI-k)y-PEoZe2S?#G;-SJ?gL;1~0=s(p0E_r3N6b{b~?5){v*sh;OM^j3biD-*|cG z%~i+ROfm#>&hC9Z0rbq{o?WEA{4|c;&iER!?;$% zN+tX(E_yHRAI1hyR>`E_NZJ`lk)&gYSj%D~sQRYOf0Q(WAlb(S=mC7`GT%>WC;y1?5S60K(A`~0m zfL_#ObaJU!|7J0sdrPAVSZj`i5rtAhu-3@rk!bcWmeoi9h(+t?XjwQdyva&B5`>(p zTMMCxz|d`-z8b5VzUl$Ml|9s3BF-L|Gii(H43sWdf|urL)AEE%o$)yG@!lI$$1soX zga|T&-7(eM6UZ^R{(4|Uk-MzIBQ3#6t-MM9pB{=%uQF@Y%jsFsKr;^rV8(Cq_i>kM z_<3yw3b-fiAz~u_Eg;+T_fJ*3{Z3M4tA3L00?i@n@ON3O^upwhU5{A(T{?odOV&lN;dY&kMFr4bRHWnxMeru--tCPoRU5kfs*olG>LmD< zFhExJ>|k2w|FLw|VNL#T8y-1uV;h1nq(-N73rLqVNK2Q1q)6w0QIaAcsI(y6AR#ci zQBnbEr8}g2AHTo%-yH`BI-WiIJokNF=LyA;3#OTezU9e?auO|Pf6906{r@MN#`&TP zmHqjZB{s4v^k%2x^D%7mO8G__>~7mmqIHTl7_V0xD?!{&5!_(pOX>2p#V>N=&O4wD zvmsqT9G|T&>%`S?96Zu7H%OhEZrGM{qVuW6f3fkD$2O;IFU)t?$g1BLcXnq(R`XIsX`DxZ2gx1iFEo@Ou~kQXqbf~eV{cV$pBUIuDR{#S+t0~nJ>=C3h=YqdRz9q|2odfFRAgZpVPWIqr6MiAf^nrheMBr6~lxV*GX}K)g z9NhRwdhtn1t+C4j$y5-}i9ktbG7lw5v}k@?&7#{lDS`AWOk- zL5T&QKUhvbzNI++=d*l7K1S7+$^M&w_}gSU1H0mgNbOW`Mfw_FC7~?rVO-*8C`Hhy zyI8-G5s7Z6WW3)5l5;DL;3`X&#&*C;iN$Z8Xu!&EAPjB3*`N&EN^t9U;&Lb)ZQ;pu zA=^|;I$4~e=+7BH3-NHa+}c2xsRGiG{A%k!4t%j|tXMa`l_hF1^8U|IY4YZ0bhOc0 zfB6#l>|n$sFm-TH=lk{Po{u3XPF!XJn>r6DIJ2IiOWo?Ol`D^(ST)}?Rjr5_IQDCb zqL(Azk0Lb8w6OG7MJnUY-_JwcP0|{`O{{7jF3ed3~Q1m#~4^dj~_S>GKlH z->{t8Uf_z%{z)bEp{2!%0P!BcDcvNEj!^JFs%o%zoCNfF>d987BrgpoDwfMc=r*Y; zR8Xq^cx9R5hwq61E zN#SDbJ*oco$Krp{6Y@>iBA+~}@suCE`X6qjTLY2*)aZ188r}5e7#;GZs$Yj#x6In> zrjY_m5%QG0@tY@+zLo_KL(ghg-%<%@eqM^6kj48h>*l;HY2@K`JTN5ea_*7qXX%JA zzV-hU^HNLSh(Y{QLnAQGxkjZ9JTMOAO0e?#s#N<2+G#;$51zZan-ccqtJ^wbu0(QP zu${ugP=ZD%5wQHp8k*>z&L0SA>qYX=zl)7v_EJfd>neUVSh!Umki-lObncsHLkX+K z=ebQ@D8I3#xLL)?v1Uy-Gsdk-3m)nsQ@ zGd!0AvQ*>uQLk)uG35mhToW(#X;DPiI4<>V7C=u+J^0Y8dLR!ke=Pu?5a-Qzp3w8K z5}Bum-`P!RK4m=sn8dIn7t(wJZ=(lH#Xt|DRXGRN!5?7c;BM7?1tZ*=5}8NBS_cx`x=k_WHRsOuXr3Q4P0GC4(PQ4XF9>ax4=Z7#t z+~+dYSoSe1mb}i+OzS!>h$BF?uT7+&^nDi5ZP)Q{tOW^D2+IRdll*PDn3vsfs%{)w z(@k_IOI_{#t~wAQuA9oyH}~}sgH-Aqy_XR~cO13XNKzH~8M(jH)yc}>VHZR5qDTt* zpa1^q28&7r#EHZY_)0u&Wq-b)^juT^NAiVAE}}{zWyHIgz$jZ8zJ9E!Ud*pIp6*T0 z23>kVr=-{w6xK?W&~z7shrz5WhGP5hZxMD=3@9(=^J)7s!DCQvV|Das9`W=02~9L& zfJjV*`H}Bro?1EQY&txBIpEp)=!Q-2XSw&>j80+#;xh&VQKdl9M#0UZI-F_YRcw^! zoco-B{&OuX8?bMb=){vJI9dGf#Y5xyGd}?;3Z?w!K<~g{fBww`HZn^=I(J#L;yeP_1(6iAaJjWrC>(2VkOy z11}}Gq-`Q(!^q+oFJG`SGMZR&M0}k&m*B&t9Ul=vik9qS1&ZYXFtICG+r5sW`pa4{ zFO7*q!W#vuyB`E;Qe{eE9jNq=PaO23tulw@@ofTdaJ!P)-b+}P;Y2>PJ>#~x`jLgk zweewKi_0V5VsyjX!5%-n(f;@=a1uk%h8)Pyo&={$c0X;rwyUUeGIb8_M9|raH3{VA zWjY=>H_zok77qxpoE#zC8Tka)1`$o$Dz)QIwbCqG5RJp!Xb9_h0Ke? z3N9%UhE3i-OYB2(JuC!V;5l24H3-_3R^_6Kw-T+F*nQxAdD#DWk1z9rE>K)#PF`d( z9ROp*dzNzDJxXf%EW@ZT;#0g4BSwq>045@Fai09}r%TIRy^C^12#6YFmC;()GDv0UDD+nI>G0aiTS1 zZOJ9}#=JFM9sB!z9{n>1)ge!XSIxeF6l6pRPCyNJP!IyNfhbh zccn{3UL85NLDryEbgqb!DS@=jT% z&!JfKb-ma{p~{uU%eQ9Y@aM;jr||ev4Zn{uJT?7V^3`5@FzI`b_~v27cb*YYmofTw z#K0Z+dJEefDsd@o+OT_1wKMS~e~z^FY{`0GBzrjHw8Dy~ zLOE56n&-7AdKy=hd#hPLH-HxXJ;xNQX${z9>mCXG#_MNI=>KGT(4 zX$e2mF}X~8K93Z)+9*^S4!Et!$&Jy_;u#Lz&$yWtSzbX-)~t-2l8m0u=<{XK-ov8S zQBP_kH&5Q1n+r4BjxYwP(94(bUWijS%FH!k9!T{85sphk9iRI?pF@?J88l)60Sg>9 z#pU^kl0b{sWDKVnJ8Kq7gTM?X$L8RCupdz=;ki!}*ez7T##4c6;4c?CDyf-S|#m6Mo;@|i(>BeQkmv+Dnz_L zv=ZU}##*;sM0q%U*4*^i5{^oqX0|#|lbCQE4Q>nm>nE>H}b)4@!L>vj@q<)+os`3b3aAF%Mq$69J%U>074tWbEE^$x#Wvnh3ksh zndb-1Rn9#nyU#A(<;sM*5l9R(epKqbeNygM+dd;Z_Q=mj^LgXy8|o%Ur!4%SI+W9Z9HrRM z{W^fbq;c|AHV1AwD~VsXQE-X6L&16~L%+9er!3ndb?!aeIC{6s$|Qt(PtbtU6Vd-U zQ$?q0CG=I~<(bj>saoy4eeOds->LG~f<{?K?jNjjUz1j6MnI!Rssu|vSN44HCsY$P z&d8rX{5dx7BC;CsM{n#ZRPD4lIyAktx%%uCZ_Tak{PV$w5Vs?ti#j$PO_1EN4`s+! zq15oU{ULjVr)-_nZxZ;)+nO#ySCNuK8Pn$Kyu-0>Ji@1-DZ)i#h0dmEe2mMTxlX4m z9PAcqTZ@!nz#|~p@~F?3`ZkWB=V9Ev*Gul{83fKe{!1iqs#XTQ#I-lSGmG?RFkmH3 z@(wKSLC2|%mSb{e9A?Ak9za6IL%}-3l_}~bMJvT5ZTZ0BXIQ^Dyf~q>hvP1{w_C~P z&5AM`Y+K)|2xAyvb5h&(x2of^pca<+#BPTm@8s#a8-dyMdX zEqodFy$5+cs*Cp`1H1LVz+nE;18DFd^UUGWM8EiJCmeVp75J?{?)=?@5zncC{zPBL zC{1bmD)9J}>q=gNbMs+dBH(NKzyX;q9l&YK8d`7=;Wc_>AS5k@($f8jDa_u_1&jQN z=%fpkY&NO_8zxD;IBfIH^C|K>pxC?A=CiDUEASa0YZRJUP%0j;QYX&xb?6xZT8H## zt&vAjIIqI0()TYXE4g2t7&QI+9yK|xM_lnLbup`ic<>8+AVjLrSepF#Ek(+oOL^ih z1JQI<@Mgb|JK^)Z?dgw}^D!>Y%W4^aJ3(lJl9THuz$y|;^1F(t($We#ureb9ph@!7 z4;G2w$6--mxlr``xTC6!x{n%B%Q8T4fK-CSt{`U-7nJ8qt}CvQwUKW8~NVvg2)=@b+s z^@cO!DN+gAXNK0{4>yJ{mvVfAtQa*YB>Of|f?HJbKA}|Btm3GKAeAYcaE~B{FL1hd zK6+`Gj6v_;!h1nYw*9ag_Doi?()l#H$XvB{LYV&qeAGCH;TgGCqrfwB0bB5&ovti= zvcB-%t+StmQrLnldjKSVq3h@Qv>!GGkTtKXXSJyW&u#eAcC1GO>9&TDffYWD;!K>X z39rHpZ*9h3gF1=cDLizANM7hVk8CipeGr7E3Xn58&9nFvb{dT2$)1gv{rOw&;43z~ z&_eVf*`|NIrr1`KmX&jB^8a@ zN5zLA7r|9qO4QJ!B>vIspX8AEILK0#P;#k!r^a~&cJGofp3Q!z%aPL9*! z_q`Wa1jESr8nj;%8*pDrLdl%TE4Vgy<9F*GRJ4F)_~uhlaYCUF*hEWb8PWtqX^YVL zdm*PaY*;4`e^Q7m|9yJC+bKPId$*Kx(5~lN`^eS{ObAuj)Db6F&U@DiU6Pi}z7@Gp zV2+L&(x&j6n%K2T3g?`=V6*;OtIil8RWJ5nH=bu7LF{io0Q_cf#W*NEQ}Ge$$POoK zTVQdh99{8HX}CiRZBq$bTPXXZ+-z`#oo+69Nm^Lfrm!vTwM)%+o~9DVTZ$q6vXuTy zYf_Q74LW`)7$T`2s|I3b9>@2)Xl;q8r@D*!-L};wO<#M3=wfD=y0!@v3CJQPWEIv% zvqk4t-vW3{#ZaS84r7-}IT_+9c{GAFH~qqLwRmyP3H3Ym;hP_)F$nWYCqY&+gJ))e zx(g6eBsoCD`fBYtx$D9<_Prpw!(A%$Nn_7_Y$Pd91|FCYon*MMoZM|mQ0 z>#=ts-oBQzEbCNwigR88B35s1G+b42piirSh)qxuK{Wc|E&Zp(2-2Zvx>6BOlyQSp zWC|2g$Qv^!LV=g4)a{jwjZf#@&Wr~XdV-?Qpp^f5fo zRDsUnjH}p2EhpSt6xq&|elD;%Nv#8Ku?Fveq&n=_7btTUdrHJHMlvKg=C{KgDn59ILg4GQ2&p_75B z-(ls&-w{E6$A)xv0HYKD5-^mMw7RT>dhO9MNf;T)@G!@p8}j$@{NNvpmoAN^`6&?K zM(rck2?)BT_O+`3_7H3HZ+^^(^)jmt4!%f*FnW zhCJ!Bqd~WWE!I*RjH7v$*{K>&t0JGad*P?=7XS0!xCMvmG2B1-%2G))R1*2P%XiC6 zXy*=dMS^4*bQ4F&41cM@hl~s$>6Iw=6@j8PJiPghxtkk3T&*Y(7rs-i7%Eb{$xPjTtOC$IAnj6#!`tpj($AXIcxytOpqQ1C1iL=KmLbDJnrS#)%dTM|5jQ8StrD4@5DH1%)|}7Da(h!Q6PrW zDZ@`OCjvV`96>eU+k?`@BS7gUj+SNduF3qhY)BZ;U!$ZmwI!Aw3*Tnq*)oj!BO8Uq zIm9?=x_?HXf&piQE5G>4DJk)5>|Yea$~$NBuG)&vv^!5(OiPs&r?bMB?}v!dLYLxa zDwoi*Fr?6)Y588<;ixc7XDJB8P{8i>_4f6N>38*sFf2_gbAo&=itv>WWeKxQknh83>^FI|J~ z?+sfp&r2y%-~{hO3}p;&akXu*$cR>9c{q8--MUSNT4Wk0I#5&i^}s}w$;E`G@fMek zh#S|XZH7vn%8Y{4=c(_nuH3YUm2)IFuF5n{Ar`4kn@o8XCD+e z;@>@_IX^wboKs!nw8A|aNXyUbU-~q4_ybT_mnK*!jY74*~Nm|0^&pSn$ zJ_8|ANrZr3240P}=TrUzW`_(Evkc#LDtod}7+W|sUt_I@UU>Jq-&t>qh|(Vhv`l~0 zcRX@be*?0%qjT#$CJ|_yDs<;`pqTzJWNE#$O1@MXyG z7|D2PHveLNVbkT17%c^oi0NW0aU{aYT=&$vX$k$8RAJXGf)UJ6KrS_*v}%C-PL%nN zhdU{e&Lo!d-{Xq{#@n+1Zn}!-!;6NeU;uujMb)cH z1p|>#laUZFEXOy>;^G0fi^tp{*~%rp^~QUCj)BgHR5s2GT-wyN?qtF236UP_i0ZAX z4)=$lD$OMGXI={_2TzfWH@Gy&6ukcMOpUFRv)u*)*_uDTonwG-uD#WRbRb!uujAXo z&PS`h4WOeLI4U;zdOd2Xl^7Gh`sN}0E_47#7MERHU*l_^XJZTPWMyy#rP!wSPXa@< z+WJw{j#hd`k8WeGo6*N?TTJ?XB@hRQ?}Bu2*-463FvZeSOvPRv5TGB01~Y?^ zEa(1|gyir{lP)|P2b>G}?2he@5jYyR8x(<0q+~?S{>WH;R-T(x>&(jkBE`x-=@Avb zQ7ht@PwJnR`U~igUZrY-6Y*GFVlMK_?c*py9JLl5YjV-@`!AX8LIlk9JjRDPS06_a zF+6-GUo}zaq8R<1aIEs=Q*9nW`AOeTz8jT{%l7*go0)7+)hNVKo#_v^_rgtr3)3Yx z)#-?T4ccWo&~H6C?-_S)GqOiL(VYOHcl+i!I+mS#@5)Q>yw`d4Q2bBs{OZB~Ftcu= zdVAa3C(GCRVj$^FQW}9LB}oLBw7o3n4Z9{yYRxFzn(tsxq`D#}{ElJ&AS%TzG&lE(L$)U22!?^d2P9l)Hy?OM$W(c*6tXC%4;_yc+MeT9EA2S~%0XH5M$EKOZTk0|$3K=`RN~?xhKkqil zr{n@-mD0Uo76Uqp6s{-&Ze@|$Z`W@+Xl1@_S5eu=DLsT(`oA|e=tH19wl1BM?X>r?D@~7HLIzFeCDue@cI0`e&DYq9C%i>T z7z8@ZU$ph(AN8#Td1U>A64Ov>Xr6}K*%>^CcR!E9liO34VAfyN?2$Jbr?gQBBl5N6 zBQnkws@9@#>GJz`luO_d7<0#?X$8$I`m}DZ?n{1S zGpPRAd>g@~I&`Q26x9O#jzTjiLKrwZ#ICBRUjr(Xs%B#v?*NSb@cb`^AII7j0>e)U zok0@9-^}5P1NgO{Fe|8zFrcWKQUHnT>Fc6YuXPLTm9*L6vUO|ovg}=>_yCYU9nYp$ z3cU6Nqb%-nS5c5nSan0~b7XnF?ox23q~BX-E`~K>GZ!0HmRbjFt6#S?^#8P^Zzn zpkL7C(U5z2nI|1{ zd%xw@-%iq2Icz%o0>fNE&zUbwGU^1c!lhJSQCshEiFjvS(9^_c(RjddX^6TR2D}tA z9<1!#n2drZI#2h4dZ!5ZwBF9!HfuafpBj95tkt^(=p7}*Hf(Y1RKM)<|DYVt{S-iZ zYnudi|KV1*^*&k3={M{yN+_LzqQ+KrE*GP7tOc}OEQlZrR@8krZbT7a=!k3)N#Bz# ze>m1U;D-f}ilB!4u{scuY6V!Ez&r~1;|`xKj|l16I_%{0QZk(Uv51CKW*gkmCKoT} zjuaeDOwJg#C=_Qb*lm?J(uEf$$2?vp#uxb6;12Hc^cU8Vjd93e!y`0@a&zMv?0Y=}6AxIgpLMI>OsGz!BYQ99TM~%=n z{5!!I?XTf-Osxe`94S7_cAf^%n_IYm;%N&jnJ)2jH;?cazkFyCal~Wd`KL(_5RT2p zFZvz9r13WdVsk>x6LAN&Uh{TOF<7#;t~j(8>ppu@}|)F z0R=_;wbz+GVp19O?|+}Z82cTR!y4`PJK;W|GoYjnGLDOh{7S_(I7Jx!1$YV_jfhE& zT$SctjqJz8w@JlOt5p{&38yM}Wnp<;*hdFjT*GHlSS>X0P8x5SKT&$he&FvFq4cy5 zi1XF&Whm+u5~;}YACa|Qruxz=XRXc+ZGrY zFc^KI0K2Uezk1dW7mxP+#P+Cl4bIvwZiM|*J@E;6$k5*p{I(V}OZgX7#`_rr_!Wk0 zK+8pQ%;In_ijFA;GUAW{BwAu@ff*RX)^Y$ zfBw3zc&Gmdh9YTozEQrlT*1*vrv1RX{NV+*DtyHetv#~|iJYe{{5U1kikMM@29tDu z{?~@Vg0Z~aWnP)zptY@&kHg(3*j+00HjNCo33N2$ZA_9E$^gk)S<~YNRBDaKVS)N7^BYhzemC zNbD491V$Z!I@$N%;E=YydQ@b&WOnrj9m16o&DD3|PYNoG8r?X57b2LA5TKa}hP%T@V zIn@IJkbufuLv9P?|5H4W$LavyIG-A@PwZ3 z1jY*+HFh5eAI`n0?CG9;+W+0Q>F&K!hrrNeS(`~)8VkQbPrlT7#o7`d=xGd#ZEoj>DX>9}1uxX=NoO zUnga0Ki=&-ESc|Aq-W4I=T+czQR47g-|@_}_6`CflWgl@->kpUHt4<^aria%FW5l= z1Ur82DW3nUTL7&Y->m3Vo_N?kAdroe9`2L<73(0jehKqO^5Oyl3Y_Jqbr;r6^=6;V z!0*ar-9nb1{G_EikqxF=vCUKbV?9wPHEYF~kBeuvdNIeG_0p5`ZntTn z;fDfMMJZD2R}>ad-vz0Jo639!j=Rw$onZm*2yE8h-q>cJNHY^+K*9l?%%x&HE#}eI z&d{uwVQo2#HfzvZFmbJ)JNcY+$=5G`3^XX& z{t;3-){9gNO+p7e?^Ex8UmGyyuetQa&{#j2F{q*k1^l%d0T4K)*8<~9kPerDd z-8Lhl6C5do7henqY<)g;4ibO)K3XwRhM8P?fCb@LH$FkO7o-EV?R z1wNu2#tp^AY2_o1o|;c< zFqxd~k-2VET2Zc%gDf!qVYarC?OF@_ePpuFEMvBc_=tzQhK(4o?meh513Q3ot@r=E zxQfF;%9?A9Q$!&euo85auEHZ4*B6xBBRK2sS$Sp|)Ch|#-cLQ`LCb@(y!nV@ImGPi z=&Wye!X7dI30T(9=P-*E;5@6y4+APw3E!?9`K`(hq6KFZj3EJH&oGBPVbaW*(oF;C z^RPksyD99)A222ek3q;G;Ihb}F6T3jp03D=tGOwk4#2++t*Fyx#2=K*!C7oq=?(h;H_vs>~He~>DQ8OnOXQEf#)Tt>U_7mV>9BCwID7$&~NQku% z5%_%o9+FwIgK@~%#0{XtukVL@KevbnMnkgFTdzi~7vl-Qqjc=uS5N}n`BOSDhz`%% z2DeLINnFkNFC+?n@*Az3AimefnD)b7_ygh?2eAGwu&U9ly;&u%Qx>*JJjTKP0gbk> zRZSJ-Sw*T%x=RB@76f=Lq8ceYV{L5xE*eh1mC3eXtK0PZOlKfi&{}#jA3^bBkLyj` za%C`{Bp(!URJXY?60Ya!YRLX9@!JLlhz&Tt5S8g_-z(D$nxfy#WPQ+N8}HcA|I}F| zc=InQb|0k1t1e-LH~%alOM@658^Q2aCB^XbY8px1R2vMxcWHg5b7dO$r&nSWGck;w zOa)tw2x94)w{E1xN|St2V9|W;1_XgNKu38ntLh8g2>rduyr5q@|6b}+Ua+_JR`C+w z7?;nR?}Iin&pK!>dC5L+b$99ZrsssZNFj_R%^d`=XGPL;eJ7CnHG{V zYbi~IRnj{1rJrNK>*7bi=9|w-ELBqquG)QJuM%q6M7PFaBU>o8mP{tT*01Irblapw z)%f;kb!ox=P;4i@W}l42-vCUh?s`03GT%CfuD{i5Tj;SW5-EQAhs4|pUOiv02rnAY zqIv^%A0-jkE$O2mAn`2Up8CK)3C>i_I}-YIQI+A4FZqbK_@|{^US~_t zE?fBT@i6Kw!QM313-!<}$vsv`xv;Pt(7Ajd4H$ioa;O|E6CbNf`uh3{iWuKTNjo&L z*wGKCa4TtuW-dINJ}bY-uC4?n(^uK1ex8OBV1{W~vQFFnv22^x)EQ$aBQR!{jc@*p&B?W=yBq1yG4!=inM`SsL1TcC zpTO?|5D?~BLGQ$>Whp*<^Z2(ZzggCxju-q-7152 zjlpYiTq4yak?XwrH>1)Xb#DXi3#UAo{nl~Sy|{ZjR(_`>SRP{7F({Ln{wERuHl8(K zyaA5zbk(Z48wR7)Gm(1-518zA%`GAaR{Y%i&e!lr-9m|gC-FWb$4(7%$31G%$R7*R zpiVv@0E>U5m-%d3wtaykRvrJ zPSY7)sT{G`$trR13x*_EINhau8aT%HC}(NH^01kGreGEudME|l*}qVF2~FfL@lPZ9 z6cny=3@bJnx12sME{V-%i$HZ}#yeE9@DF8zYpOKg#HTt3WB=gU1Tk1Fbvxb#3+8WH zBTHETq=azTec^nXfQriwE!Nt5f(J#W3sM za#&Lj;5V)FGczwblUB%}7O7LELI7XZpLkLR*qF#$`!@0J`cr00hb4b={E{iWw(S^4#m9Sxf)<#|~*!##K>Ri@!Fm{5Gn7OQYD?D4t^vC-v%C=bUU++fl z?Th^w#=!^pmI0`L_5kmOO_Et*Q!eKjPuExv@gS-U?gAoQ|LR20^&qm0}9Jf z5i#Fil>7u9B^kfCv=S5p~q z03tF76)xYlFkd)rlA!mJ-+taM8qooYx;hiX38JfFlLcF-qTwl7locBW!{1s>TGJJb{YbAnj z$weq>9{v#RnP);o+dMfqg=lUyQN=D;t=T`C#NaC(MsgdpkhDT3RlVk(l&42V+6ThV z$(>v9Je58AhQ$W`j#0VkPigHTiwK5SG|>ps!-rTo4r&w(=Xq<%iIqkGYh3WI=NCb4 zga9TqY(pPLy3;*y33Od8-le?KeVQQmVxslWLoA5FS?BQL<>TFe>P>tK(c`uY`F$cL z{J7l>U7nS8%+bgrpme6)cZY`G43EX^<*%aw9v-p3|4s9m=udEhgdLT1w=FWjb$#95vSW z`~s?e{4GfbEq`=8YR*Vz6qcsEi$dQI*i0@L9d>!1U6g7EF!c}krhr35Pst5CePlu;B;_>~+t*7pM- z1|Vceq}sOiC|J)KlK4R|A?q|Q9~GlZC-5~&x1b4}t=8OAE{RAa4-k6_68&qYLSDCU zz8v9OK&kYs_80fd*&mS;m!zxj$rQ3odQq9bdlhR!i@3Xg5FGj0$a4po$)C2kB z*0);P|E!o*t2-0c2sejTn;DM&S9Wknd@mVzowtsk+vi`HJuyZxhhEGg%$~sz^$N)I ztgEwneaxERl7%Bn_p=T&tT3RmuYj@8xhQ@;DxPj2-uUE`?n(?>_L?l;pzD(LZ#vROZW$ ziEmOa#e1#^j(V09>~3pQLbnTsSt)HRirQg(GEPD@Kx(@{CRBkx?w;smG3L3h3|6o0 zR>PrY$($mMFdi^4D${M?vuf-q3Bn}TmShtp#a@*4)k~@u2B!Y0*t)cj`v@GNiYJ4u z-c)z#d}ENGbo_M>bQtgP5=Wqi#>B~0U8X6V^mUMo954E-Z3hwAck{JieiPnt|6U{0 z{`{iIg-&d`RrV5*r6rA7e`;hF$SbFU+kix;CKJ8)IO+gJhM6;_vGsctQ4H`mh~Wd7 zMkTq3!m7IB5ge})!?tHDK1A&;_g@3rPL+4~oTs+dX;9~8j@r}e6>;nyD)F9`61B!r zqcwdxxqaM5XNQV^lAkTe9$Xkcyo- z3HE!jE!hX-%4dB)fq(gxK>I>v~NH-U-_ndwzWA2pI>>ww$tdl zi@I&f;2jI1s9jlu{O~vm=Mk&ap)NFC%mE#d?&NwT*RQI5T*P1C9CDOlEv{a@u-V&JyBOExhJ0QNjh;$GWrNu1#2No@oc|tY%o$nq|BUev6k>7m+GAR!kqUF z9DXG0(=wX>e)lv2p)tMS-2A2>YqTZ_r1MjBl>jXhitlwYf`ZK0Y`#N-mCVV)U0a zlJ#peNu4N9U@_2|38IO05wE<)t*j6q>Y4i$u;=V0cRhqc9NlHQn3BtT5y$ILQxU26 zzQ9BadM7O5G*qZZczSObwR@J&&mWICQpPI7Ul1K}@uQN##8G!~pfj)qnrG?m}4 zCc6<+{&${zNjkJNC%Pz_PuAXKWNus?^;}E{^iDAG8PaXnmaFPPux3x0jqvVv(P!~QLOxM7v7EsJ?8IBQiqXczqDH2 z*x%w=BsqsE_q9QS%4*)$u9_pS@AS3%*!%we`CP|g8Kwzg*31IqG$DY;JZdn zc&kwPTT3zcd!qCK)?>u-kQ8ZoEvBQX3smm!O!esb#(5W0ngl&m)Z(TQH>S9$F=J?_ zT7bby(s>#~q<9Bs5|c(8%|)WoU7HPj3LSz%OHh-P)^5sPiBLv?6EKj$|Lxr`G`SJR zkPy2e8X;#*<370GC8A;ZJh{1dFbH{;eRckMO5J}GzK9X%Gj1Nvo&7N$80c+XMfgXa zR1W{K>sA@nN6(w#@(@m>tEylACR-|pV3h6v->EIZyYT4D>PFD!nRBMXSM@#jvbc8? zCi8)QS7nrvn=5yU?(n3VMP*8W5%Gltp8?o*F)N;RqE(GWroX)rYbUK+rh*q0{2)QJ z$rG!%>f$*u1P}ze@yW(*`Q>C3WY~`|RYvSOd~ZecqQmuTG7-m$i4wyUZt@3(gPG3a zdyj}o{Qck_^6R^1kI3^#Y^o3kRY1hgIIXVgvFjDN>azRqYRO>|&i4j$o&<0LNW7oBt> zgaI-=trStk;hI}KAP+~=V532*JlC)EZ0wm~Z2lA#c)v^qYBb)HO(sppzo* zN_wZId+SA$7+-?0Y}CB{a%Q)SXCN=|lrT6>FGD-(p;vRC(SXIThE{=Zi^o=>fJ8mp zY!%D?Y_to#GSw4hfYg_9@t}`MLLL5ZRUa}`KE+l~ef{a9tkE#i^}g(xu`j^>!Ca)J zN!=N%DTXU#=$o{d8vA|mdL^5k0M;)N4dbxMU%?TG=C|vQZt7*wRK^#ZMw@C@w0&NB z6+)To`EQIwut+p#vHK2eLntgid~cX)CtRuZaSmyR{W~*UYnFR=A#&YFNft?jBYsMe z2%nGcG6FKqRPP$$o8EzvQPDP`MCqq|p$vd|UdGpkqyzA&)JX==QO9~}#eBp>Pv43Z zuAhp)hX`qotz(2iftnq?yR=k*?|#xk`V)F)q(DUL4)n26D4*i3y^;AT@F9qT;;4Br z#@=7k9W2y;<0s77ty&s?PNaZ4jD&oeUu#D|KJU^B_J(vGNCWE`2dHSKdN!St?rOk) zhuf5EiGm({I8O3lO8VcCGX6z}18|alw)(|dY&(T$J3KtAChk{Qu3V@Jf8Eqd7sH$H z0YC9E_U|$9lRyOBZd)_4*ZQ3npH_JN97@`9hqt4IA{;MWsw_W66hH``q2i_?g95)Q zM2Akf8>F3ra~wis2%PkpS77kv`2dqWBwc0cOS(QFNH zgcBoWd=h9t+maLJFf8!VJ%5I8sx#}YHZv$9m_BG8@Fa}C?WalqF_ z)EL!gbnAwhC?udfON|BWUEOhEw$IEoJMVH&9p!TK_Sj2mafvmHfUT~F9Unaj44A@Y zsAYysYLHw87NatASf^tMu#IoF=D<~Oe?(C|?^KJCJ%#|!n~!xSGNBa2&r*TV@j*^w z%Yj!7lK0@qdVJt--tyz9u)8O0BclexI1Codg;efLV)d58~n$H^qZ~6JE@a;=r~g6XF79da&0X=1bxSyC+=9)8(E8gar^RsVdbdx zX0V`=DhuK_wCfNDwmvH|G_K#Av=ID_*VNGTHNCW0A?=6RNiye|aF}9fcg`Aj;2Dqa zz4ZAX-mkq=r2hyr;p3WZJ!id&^Ls!kGSg0UND`(zERzdeTuC;1{KOB-)YiD|Cq_&X zyEx!@G!UITpFM$Q+MtdK(A7+qXzH;(f)WT?d+z&gqXFtS=nAbx0gw1F_=;-x8^-N4 zR;+D+qmm1GJ$|v=)962q87WY7<^V_X@bI?Vx$uW7ZK{KGTo)Gb(y!mh(#fb+#-yLA zK}U={7Kx(vk$d0+G2hfqT+`j!o&o2Oyp3$npPL-;xLOq|A=tivP!JI{vE;MIaNYvENUh(3Ov#Fx!dW+Z_qjVCOzp@I8B?=(0}%#) zb4#S?(nk8#h{zp>Jkx-n*;VoD__I@qcVx;UtW4IN_E`tk?KcJ10dut6Pe-jU@Pb!E zNsW2jMbtmdG@0q&Y2fZN(17Le`Y_8NGc?RFNb8382_6t-0$Da(dDl=&6u3I;`=VQl zc0Txgv(7hc_d+{JbL~>fMnq&I2A`|z5Fj$$Ht%=SdQ-^;F5+Go@xa3E{a zeKmTOmw#KQ3=Y_bo;hCnWfk_17kwi6_))6E&%vVgA$lz=VLsbH)$8JwiO0U#tJLX9 z^<&LgA`Pg7135j8uWFo`QUua6+iLi~ipgHFDAabOP5?devwcOwyIB*{t%@m{Z#8-W zN$EgKh=S3E35V!-*?1D7P0XYb$U;pgN6DNN$7uZ+JuHtSACtgk)VHbr9%3pck3&|S zm00}7ss@DcQ_5lji~4V?k_LxE9`c(fJ*HU>-yC`3z9Fl=kEs2Yz@|yA`=nu*T>*b~ zyZ5i6d*)g?ZUW-nB8#i|D1aMdMGhga7kvyqJlakP8k^iVbTyyb?)4*c~h^Cvs0fAjX}T_)}tN_G0|TzyV?H>j~oXUAM*VarQ-jh8ISfRfiG z%lk!52118UOfDe7GTUZ2Cl0C6H0*q=6ZLaqzWf6zJB~7knc7|h78rB5Yj!Vp<}Owi zZhL`wy~RWhHWAy(i+WKh*f|^V0=5g;JPUauzBoP}Q2ZQb9m~qW=n=u6q>3xE{7qtK zc}j>)1OuLyT-cVucf=9M-e;SqQyi8PN2^cG2B3HJxfEi!z@OtFN+fXL#jL45A26B9 z62xG{Sof164}Xz`TV(pqTQ?{V%68ZpV($S87&kN^&uZi<($>dDC$E48>G4FbNZcCX zsY9o`*A5+&&_LV|ZeFYnb(U?u`|^Vf?;WA6J#&Iseb=VS{!@Z-F2ZYYXEE(hH&1pL z-onrh)>+^2GRJ$cZK5#M8cBt0JepU45B4#9kBF_anhJ|FU(xd&mqXbhv&vqVxs*ME zltE6Rew&)7Dpa=GkjMx=eD$NK>yeab#qs_R_sAP+))&k&siRC~C5mZeaFiAE(BkgRh_@hC+`xXe)kX6Zk%8)%dp&ZZ&J;th8c%zI zU}6GbQib6C~I@C`B#h`Z0syx?``Wz)Dsm`i?5 zX1H%KGGw5W9H8L?VzHfAm8dN4%OAW0+S zEcq=e@|Ic4@lQP_McXjkQR=g@NHZ0};FycG&>ng%$;uuN(s{4;80qsUZ{S(6Vo%dp zR+RV5c6E(ERA#*HVO6KW!@xanrp0I=bwwo;6934%fR#MpYhOE2oKbz(PN*y#nK*}Q z|E)>%KwAtwCGlCbMaC@9;)dRwMj}SYq0ucqFrP{);K_zMA6fBCSNk8UGXh}L$xD&! z54S+bSD;wV*kZfcQ*Kt(NJRYfd0xGMeMAhS)YvdR;SA=n&;;<)`yyqST9xn*CzFr= z0c%~isBHPYk^xrXQRb1raIh+R3K$}NJ8_b)tv1?u$j^L;`Ap3s7unyI>C#E`b@Xy* zl1K@c%|mOF@|X750677QL>78LHXc0>^Kv}uU zt=#{%$#_vy0XW3rhRPu;{T9L^4y84=PPaZS1m$2wmBbQi1RQ{O*K?NIsIiwkoxmS} zP=5*dk=(20YD$5Z3pb4miHP}k-)_s+X(I8Z(cKCWSw2lBmE$y;H0jxJpQ&n@u5B;0 z4W~2QVm=aWALs~mt5?QTd1H{=jCgqtH+%L?P!~og1Ylc=mM8D%Fi8zdU83YAZA6(i z&**m4N}qK77=t}somq_KzEJz3n#oFp75bT~55l;*mQ*{E@QnJ*u5RB)w{=1vLa`25 zwQPw#JV&mol4}1#R%gZ!7On6ykB`W;y&?x!Omt3{^}h`IIx+VIr8w-~KlZor{S>I> zx88}_yokM|J%}_n#``1pyQWp?f|js9lv3nuvT|P@t+7E~nF1R#fBzIH{sPu^lt~5R z5RPtnCCcp7BRE@6f${)%6Q*f;ob*!suNp5iMG#`a&cUdY!E!SgNJqK4(Q3`Bd!mS79 zz)i-Rt>m~<#L5`T?5w8GWuCnI>o@Mt>LX1z;a~jlGJ)Xc)tD=(BzZJpbl4XKD%*97 zNCqq+U*iQv(^|TFY3av*1#k$o?%eVX*3&9UC0W-(Yo{^By^Pf*ea4j8#>4sx{eIHe zJyywIxR_JdR1oa&vQ7Xccu1On$QT!$0Dw~~Yb2aeGe_yB2$QGRJK!ZGC@z{&Y1p|c z!$srP?rDHsh~TBzNqJwmO?E)Oj&$L4IjdTJ>Dc}ZNo5uw`~>&pCMs)m>3JM`Qk$>} zH1=~*?zIFUqlDiHF;TGlOO@)~AfvDWA5TR_hT}eG%o7W|lbu^e0w`kc{r5#xg{DApU%a?=bws_MRbi%YF_UyKr}7g&SroLS)C!CJ#qkys)U$Jh@@Uo&6@`sVJIP{CltYJ^?}@9IT7N zjO+5NG95G6Mg@@kFum1$fg|D0o5M}5v?vW12537We0EvpJO&zxduuRz8z?BC%@AwB z+T-vN0ki7>QnrAb>kUI%fMDxjXeTwHe`%bWTi{ZkomDl71Yf(vIDt5V4r-bCl3Bjh zOQA06TLrd_CnI0S&g}Z+&!Op-^e7NkfJ~}bkC8+2n8V>jI1bQym^Tt!dZo({9Z*q! z3O(VH4^4RX7^%n&q!FG;&WSm|6F`v3KkCa6Y3fl(;{^3*G7wJNunO`S4l8Z;;}ii1 z>D%onMmSAYVhZitdM;{Y}AFJ`baluxx7d9SQ;d{r={yU#H z^Dt;74ShAUBGTv7x0;MJbIev;7pX5MLq_B=pJHtEHHFTyibXc8#_8{$K#uRzMN|AA zZ)zDY0MVOCuixU%n;9>V{^`K%2)>-gtu&D^b(T9_Z9oqLIH=SN%hq}SmGC=l1EhKe z47gUS#8ogdN|aL(e^v?#Kz_o;sb(sN=y_ka&MNNvoJwH@VFXpk%L(ZHZ=3Jf2}?n& zAo#53@|b@C#jS8Ng8Aev=YM?65Eck2&Mn3`=l`?1X5kyts$ zRz4J8^Vfq=k&Z7@^xY1%`P#`rU!@9Ebjxbng*o=HfmrKLQ2NxP*~=;E%zI=H z!{&c;lE6mtn})qD#xPl?&3bjJ#4jH?pWlS1QS}+{V1~&sbvK%yv$wzh?pu>qkSdkE zAFR9zg&Q70wd&|*C4MmnGCjSATmbJ6lMI<-4Yi=$6g+iU-Q=gnzC2bm21!|f{7%*k z71P18|CM9YU#)m~(PU?Hky|aV-@i%aBQQu*qn0jOc8tpO+E@N>ruErOeUBLcC13s6 zKdG-2@ifN6oxKu896&5reUR!pJL>*LB3+Nj=*}nkmTi-4VAicWc_+K2@c@}fwET}# zk~qN?)rM~ZhU_Yd7-mzM<{bV~iP-BXj;1t(H6@B;>Kdwj6-+pECcAZ@)5F54}4 zHUJZI0nXGmlt%)<7VT?~a@l{8cj87uN~;3$v8?HdV(Dad2cBIIFtv%d2167|@Pl#? zmsE8Gy#-B*Rgwf$*_>4wd%wFap{EPKKB!z5q82X~8Dg`3Pk~GBQy3L62>F>)lg2r; z(Ly08ZHA(Iy)Vi%E*)3it)v_4Tc7_bj**mU(vaaKa2RAo_;IqS3WcoyDYFinAsi%f zTay)w_dU}_<@_?U9Op-B;--sr3s%{-N3XA#<<9J7A+?5OO#t8WVzNw7O~zB*o`=gz_qNYp%g0Z9!jxGTGK5lq#m!h0 zk!4Lh24vYC_gpu=ZNeGQ7_|Q$HU;qdxP9)5!SdVxD9>PR!3C?t$0UTWPK3YEcg;EP ztx(2%&q8FvY-X=yURUm9WKXYzwQuq|lpd|q?K?|-DKrr#H>)#L)S<#2oPT?YO-Q}z z#%t+#g;0OMC;GX|!y_EUSzr1yW+F{i50Jry0BQ0mmZ3j};Zi!j%?vT_)GqgQ&n%T9GteL<%f@@TadwRGAn&zAL8lHV7OZQQ>&Lmn`X*j4Ma7(iM98cEPkA7~ z0%F0sXpx{`8VtuVcZ@Tp0|N7{$wcEE+jXGx32G!1v!_NAOMA2*n>4spN@;()^cV!j z(#ySmR4DBsgqAxhy>BSJt?33;rj_+cVsd)`AnQOq$i}u`APFcv4ZWPKcZ6Xg;tSXQ z$(-lck*O6ccI8jbD;9+VoK1r|r<|w~SqCm;yYo?)AONPzgV%P1ht=QP-}iPU>SQ1R zGjG0)rgMO*Z^#R)KMw6MNOjivk(h}oG3l`j#Fbw@?3$1Ijn;UDu%p;-+@7gW;&pr} ztTl^wAMlVKxCYc_T4;vzaP$)-J>CVcJ;xCIeSGCZC=i#i6m8jP!tCVYiB~A&_asDK zC)W01a2?~k_TDV-wDyDtXTaagjOCo~>czCpa?-Fj+3cA8e7^BK5`-yDn-#6=#M^v+ zWYv7ZEFXpujK3Nf|4FA8-O4C)Pes^#{)w-buA30`0xsA3MUg7yl#&?)sMIr-4~txt zp@7{&8R|t<9wTT8)l;t6s+f~v8UOM1yOp6!C$n)N zwbZrgI@%M3Nmjw^GMAN!W~U+fiW_pRatz*BO~jI=7wCOz4DrGMZUlimocqvQnQ@7} zfk0x`Kbf*jGM2UVqWvIa-ZDFAkLVCa-Zp<24_>PNU|3v?*`~@LH8?2-nStq-Kz$Ai zw}XR?M9U1uO?7*j%8AYVPDVXD#&xPeVQhj_7&vE>$vw4NNf4FHfJsBqMztAE&XM_cN zW3wqU{B_giYxghRR;qko&YLXjyX}L}&i9<+g<-GmU<#!_DaIPONkoH3plA|qozMKc zsO400t#XNZ0jlS7!FaGEL=$D5vg>RL`KXY8^CZI&0-SQXUz4Y&!8a>=WaGviJo5%!;!!yen7G>Xg9f*pC1!<)cNmy zXjJ;M+5L4Nn9)`#_F$MbJ3fp>5|It5V`>Br8BWbnI~^jY_*NfgQHN{1eV1?%%Utbb zr^YK?vCFB5>RUC0H1dzB&B zd5_=CIudHV)s*#rhH|yqS6#HR!Wscv!5(hdayWjKMK$Mcc#x}eFLr~dC8Jg`oq}69 za1;Xfs;}I zM=!ke$QihH>wJO_Omx5q-ZY}qa{w8e9hE1{zHE<%xPNI!WM3IsT5=;~EVUHuJ2%s< zSo%BoX#wH&gQ@}~98h1+>9lP+12qCX%qldYWV|6$0q^j905rJ-du0@|mx#aP>N8^Y zNuEY@rbPGe2=LFoEm^y+eVxknn#%PXY88#CR%G{Ynt!f)oD)cp5+!QHGSH$jaz7Ot ze;R6@qo2{KXmpI#C#%=6ckADetwe=a>JKc*6mos;;rbHJcdhXhX7fiT{<$a>5}HU5 z#nOxU(L28)!*h}Ypijvm2eG4i3k3pR!stwUe7;^P?ni-1<+?6NJh6zWZ6yttw)Oy7 z)g8PX%H^am;`4lKjWM<_TTNj%lVRQZbO4!lH}3E4zwmv$lAmSWEHF@z~gvv6tAgo6nEd+i9 zsP6i1;U#?vNW8u9U-R0H0I>thQCT2D?PW!BfCqZ3$skyi0r;c#Z^qkjt~+n-Bc}2{ z#{)LNK@m{&vDb)tm$a66)Xskyu<5*a%IQXD(imm~u4SEz!1SHNy8{Nng?}&^WZuW*?=g=8x;$nT|^;gZs8xV!CM;NhxG`pbmH-K9|-G^7$kMPeFpiB z#ij;=*sP`i2FgFHUu_u;+gsRB4fDExi8^n__4IhXPrxdMUNlUZe=FMIKWJz=To8c^vz(J}QX(#RW-QxVWd5c2IlYmoI$0oV)E*ow zOa^9WlyT01EM(_pl51_v|KpJRLTa%m4=#49pcXmY537wDvE~7IC*G$L8N|25i$CAK5^p3VZMSYShEl zlYO!6A53K`2mwUD#}qS!74Oc7S)+=c_XYwy5B3VLCgmz#A($LCrGU>*P`U-$O)fjZ z6JM1gyx3-c8nM!7=MfXlNnnsQ|A6hb;_`pJQ0A7t4FT}9u9>jKy@bxCpbHS zoX?EtxqbVAGMD;g<`y`Up(y-4y?yXku7>+p=W(wwoC@t_aP4A>rv9}8R2iq`i3a##AR;dGHH!{ zlxyYW@q&iztsc{aaDt5ZtM;o-Hb%MXXm9@31utCG#Fb3G@DBk51;K9xU|valL}pE9 zbDx5dm%%AiNom&U#?yS~@}S1HdnlCDJXJ$<`RU?B8c{)-`*_PxbjHLf#aVyfri}C= zj3b#K6g^Cv_yX>>gc2!`SHKDd)d{YzQpTvan2ZIy49wuqUvb^Rta@6C&LDdKvdxlI zJ6^FP`>$HlM}&imkM%tju}Zuw9i5Z?R?;Z0)c?}Dc{^1;CN;GNlrOs6yCx+_uTl66 zL@C~rPg1_U9nFe?`8A>bCF@60VpQfbg8#VlDY@3Ks&YwJJTO>b50rHrZmLwJzPxR=A8bAqh9w5e>AY{y= zCalfqUv*DQw)16(1(siNGBv7LHAb~YMXvr|fW~#x6)=U7$%rkFU>l^0P2Jl-os^r; zQZ}e)TYxOap9knVItvmVThQQO^~;WrSbu1Y8_TAX1g31SV?w2=$;pXH=3KTy_-{r3 z{!Ojz#F$VeQPJ7dozx*Z-rlfYX;os7wj3C683~uJC{i_Pzc%ggj*|xBJqYXY0RX8m z=k>h;<}|<3I>9?G@hOGYS+r!QxyLTfXdUE z8fM~+a((XO+M3)g(54`rD4jdzw*}FVNiT|4u|ujedKix zqr!0cv8B$8nEQ{zCi6dQSJ-lz=o*3)&UDIeDuioyI82zM-fH(Ld6w;GRq;x}1v~h$ zagndT{A$8E;Gc=9w6=(ocId|k(_%J&qI8N{V402 zP(4P^6tj%9azg_1`rF@AF2yq{$%9ciWS*u~m$xd+{A;V*UPglp%-_aSP8e;nFi)-N zIA0d(sEQO2`%g=UE1jaGS$;=h&Z=^xkk&DhRC<&)Rpa2I|E#pHp(F zzkMz`c@0;N?3)Rn)IZ$o@%`tdM5$V(_n~3Vs8W(NXRnekdqkFvg!cPJbb$XcvQITD zQ~cS>Vd$!yy@2D+GIwywY_RaT0(y~Uti@8_@$ByC3oyxdlk*=rLb642$#ipudqqPh zTCAjPe&YwM9a1^jjT4vYpk<#r*GU5(;whXc#qEPSlz+dW6UegTg<$xQjX+KNvY%VT z-t&e770=2jWKe}{Yv+A5)L~L2vsmBC5L<%l`djj9&Irx@m*jLAO+y|VPbN7uVm^4rH>UJ^vK`f_F*yZ zCw~uT;W#qDp5uS_I|vg3TySBP_6T?21j^qh5dNy(T7y>nGf+*}v8M_rQ}w2QD+Ip3 zE%t8MWiO>f)IKd_>O;WmFCxK=k8RR?@q|H6ACh8<(x?y5qWB2zF$*E$@m{jS7DNwk z>uH|uy7Jw}Y6Ol1HonwUb!FNz2%I&xGItT@_bHBPb}I2DW1+wWB|CiGRa!KLF`YA2 z{s?~x;?1MmGB!d^y=WjwR@v)Bm2~zg1`v~2?3Y0>%>%tvo=DEn9;0wT02b;PHIno# zo&qg06!!7Kqa}&IO4KSvxBPn)HqhzTY?uQzIJwDbO~>C{jpr1b!z7-RCJ%yfX4R?{k~@)7N9W zpA-Cy_OU3lW_*^yaH*vZ>bjnIL}7eY7D<-%=Ai<89k^OIiS#4uj~*CU4-`D?7$)70 zA(5tzy?1IDs#8uN^rYSb5?mhuL;B8Qzw+ZmABVep&>$A}OdH?h1xpX3Ev*W{)m-~G8OhDV%@C^`Dy6A{KrQ$i zU}0oxWKXzedi@O|cTw6T$w^Xb4?hwM6NaB$D7#Q39EPVT3EW0Eu7<2Jj%ROEgRWUE zP-CXY+*69FYuwt(Sl!TT<8KLTj=o7b1SmwzI~`r~8IRay@*L=Rhk1Y&lQNHmn% zr~7&D0xX6;9wj#+LpSkcua(N>hUoApJ0=DzaqpA+&8 z%d^7(hK}d7jiYy3-wjX5QR2umvlyc6NBmK>y<}_MP&#DU(t6V~O0{UGF3663XK~Ez z`kb}wK37C`)Lu>Qj<0C=NSJ70*-U~FYT4r}`ljAoZpJzhL}7{LdPJ&!#v7GGgQv21 zX5B-z+T@f`f{VD%_xspN$1ue4R>GF%1a6^RBv7xlz8uqyU2WZ@9=71Lg3*Q0rBj#q_0`sU@1DbC(XX@3#7 zcqi$@Z@R24Sq$10PB--w+@6qB26h;Kk7P{!q8FhB%UO$-C@LV7!#nC_#?;(iS@sY; z=GJP$#+NFQ&(Pwby$VH^Jc{^CGhw;TD<6qbB^p2XUh$~0`d+naX&oNokk#eEmgj z97y4Xk{E2DJ8g2 zOB<-vBb^W(MzHn2#!em_?5=*y3HuZmqhDJ?NOP6 zd+dLQkiE}KNBp9+uRYN~SP8T<^XP_^x_4Zo@?>-6CnwgENm)0W+yvvUS)T54K&|BI z9Zj(?UnE3|-!O0^!tupSzGh~7P!=Yo@6fYrUtKIG*H7>gx$zFxaIDEz$9~6+DVK^z z_YGsWM&c@dmzNPX{zT@x^;l@q(>xwDa7Vzmk$o>!+}z7{j_6PEiqZgrJKsZWvtWx*yEa=d_#3H) z;Cdf6o?il+`i{0XtzUU-wPLL3PwqZG!A z#t0~Yib0&OzL&HIBXT^sJ}P`=c6+`1AuJ04Vpe|c(-@8~b%Pn3 zrN=Sa2NYiv3IDi0?jea<%!KKR<6GuvUSaBZnmHBAmK?PMZCun_U?ow~&H;MLwy`kt z$uIN2PX(>`nCrFXA#-bQOa9Z$HN8GOoR}J61E5WaKJJ)-eZ=jI0BO$@Tn0zkP)$Q5 zSWNon|4#l?+cB?JK%J}j6avZv@-iOOm+S<@vBY?LHbNX?-4c}t#5>Xi`AtHD7GR?A zY`nT6n-iVGXu}`B`!_Mdtik|5EFvr8Xg*9}>t7`MuKcgH3!=OBS)HaA{-iYyliza1 z@0XT1&oOz$jKEZRmfN=Y^eR-bTe!P3COjD^E_BqK?iMe=7`yRAkFezXd;hrj)p=Wg z{5;%zS3bPf%NDz6?JB>EDomE4ZW&_xk}NKB!{)?-U2?WwN;E24TaTU-#s&skoi-vZ zxp^^y5dM$1KlE$~c}FD!kGSmcCU#(ZJHy<3M#M?_WrNwUxSqnr;d0Z&tia=9VWU%; zz8j=9gPIqEX%AICe)%6kU$G{oArUP$(F1IOai zX+99sX2(HCdJaAIkUH*LaPH!u=Di4GShv&eB7Dr}^~)Brxu(Nc6==@gg35wsjsMYE zrn6j$bv;e$flWDm%in-c-px4mJWA!zHvoiBbY{ajZ*gxnT7o9Rf{mom$zN#3h@dap zm4KUUsYD9oE`O%}QzH+{aW;*zD zN!`dJ8mmE`G~hx!f~pVO_JkEm{b6ei=6#rEhIk6p_PEf7dR4v#e*A6%*QF4vdLNuU z4<m4GZTU zG@QS_$UX-g2wW!ekp6_q>by|T*NpC>R6g}8xY#rBNWW8YTDnkif9RbhGD0-n#Pf1B z(i3MLgSVc<%}O=px}TwyZ7AY15`S=-)niE_re;s?cjzwAhy&lkzgz3OYgbe6=#awA zR(r@n{hp@>kNW7QU@a0n7Nc$!iXnF@H~k-F8M^6@ZBInQA4!qetnsXc=1F4I2cJ@^ zHfBxjBd$^4=>7Y(fwF|(bzFn$1d4qgX8bMMnrkdWIHYXBLcP9(o&qf0k%HrtRN0P- z`!S}3+C(so0LETlGEV_IEX$M_#fr|Xv0y=g*}YqRqc!me(LbIE(@>G*rwJe~KD?3? z;4u&FzPHqg7ECvJd8l1c-Y0KcgAZlJ)2FuOiyi!9C$U|s49EdKJIcX|gQvZEJv`-& z`~=S#$$;-o;})*Zw4G~MMplE;FaBXzai)?eT&gZvZ9}F=3!T3Evtb2eta;%xa8puG z_WT|(y4w!39{dK1?k{yQ#N-+MNEbICc9<%aKuaXHYrB9?e4=|+u z^J^9AUN9I6U{vI~AjW;7vvb4^3;S)}xBi{EICK8;Lq3rL z9e`uceclS{qFD|PVi@8|j3VefBYMUb^DY~~nCi6v$B{(%huk*_V z7W)4`Y=KLBUCH{}ac+^XQ@EC_Js8mF-9F`vWDG!Ov06Bj!a z6imZkH+Y@3x?4jAK;<)& zF!I>tMH|Lei;aeY4NT-#mwB1AEqZV6a9Gz~Hhx`?_>Hi2$v#o>xR&wE;%8e;Tc@1(wGsE^fB&nNx5rx>h;`4WU*I2Qn^N zpjqPDg-a_vz$j0HBlloBUayD^eDhMx5V2?^6-%>dfwWG$6#RF`aBzL5UhYF%dr}&S zsTl-7I1V%Kr=m&xzWr*CDf;Ig;tF4m#+>rlQbr-n4rEZd0Ig{Djua)9`yovQTMn7Y zpJ}+eZgq`jlpL<;nJ8_`lyIgr^}NI+7{ro?@d{H23~rHlEYlJ30K?7`*;j+tu8lRu zd@sbD)gb8W3?S|lAE{ZdsO4yS1s!#JM#HdgW+NJvh>(6s zXN9348e{csNz>#9+lKBSX{4lQRkC7BhIjFsCBmN|DSQQEwJutliA;bQ8;$Yq?w9|y zu8Fs~7_RssdkKEuAA2yd+=I0Y$yzA{M{G|w;w zc^yWoNZ=&KM!}*O%$W3?uOFU*1*3+6ZhG%(a@*;uP+wC`ye~a3{^Ft(kzB_USY()! z*(e2oLB-AAU6}^yW`9W`lum@slBSuEgjxsyHQ%PG7CzR0#imwvhfqcq$z5Nk)+ffI zHu=wy)D~diy1~PoTj^o4uPWqY6`?5X;u%Z_cIB!YNnmTCZ3FDo0`rRYlLl`E`mO8c zy>wf3W9J}OU0)?OOcPnRTGFYj4AXoeyk*sZ+0XWEtFKVmkMDzK$Bzp_fjO?HAb0YB z{#cPzu`vM=-_CeUefnqgS^Ze#PE7TM3v@D1iDz#~qE)*!9UByh#ds|CC+uxd8AGhl z;hE8uDr1+Osdqj>-+GpI-9kH&_MWp8U-$sYhU(ddV-)q_&%ls>3hQnVoFnqK(J(>M z$SY?yR{Dk*IGS8ht!SAZT-i31%fS@JD1`*8+U9P;&N+qh zpVlwm>jTOkP`RR7Wz^X~Zn6LiFjkEWkD;_*X)$sYg)1n$E=hm+VQMlfOW5Ej+QB0J zR^yB8ujG2=q|^GXt&m{kRQo2_p+_Yc)+-!n^phGKsGYLEjtoiqq#22hjCBc_`5$&- zQCuN7|KbY#MDEhi9r*PJ>kKh=E(PFuGfcY`_TH$~0ZB5-_#zr{9`N{aBSgQvS#ls_Z_OL4Xd&C{&( zUsDN&>h36n{!>2j z`%?QXg$b%z_9UtE+7;{Hcv``U-PtNd#JF+EX0N&2e!bM3EVYWit3+B2Fu&D9Vi$Bh z_E3olwu_C_wez(Ew(H;$k>vS}5z^ z(k{hMCNA=1`HwJ*LbR^FzhL*B{(+l*pK0ZvaE2QUD5{d&7XSN{?ilZk2LT_-7DMnM z2PCGV{tZ9%cRi|ITdpHK?L);cx_x91wM<4o7#&zbEAdvCLGizO}DM`VX%-UMAXvWC6KUp0t=!mH2YK=TDtTbW^|tgOw4IX5+9Y z@j639eiad5-mW97fh#-ckysHL0|k!rJm7uh0W zp?&Ur-J#v^j&b-l2!zw<^UXaiZE0^ccZi|JbcDwtzd|e*uP_UX26k zy=>oH^?mhDyFQ*mwJn8G#rkzZ8xHN!e?}~c>v(?|5Rr0eVWKx3cD4uUVgSch zM%*I|m1l6(A(iFCAp&!c>Xufu8{juaT8@q(G3Jp#cOYH+kp6ARnpY&Q6p7+2+ooSB zAWhG;_me5>*ORQM@o#ppS~7`&c!j9k^R@Z5)*XVNtPGrc5LV$J|8lY4 z0Ly54nui>{xE_qg^>+A|e>V#CFKw>>Jz~NR#WgW7F<@ihk4-vt3KAmyB-JZ74T#~% z7&Ln2zw?gN9K9_*@*RI&NOh7xw)b@PbU3V2ncjsD_Un-k!&e+1{3HCnD)w?=MD{+t zQSO4#&=1>IYjI!vl0Q4(5!tz166w?t#z{H>E6W9SJnA0yMU~kRu|9xaF3kM?Y3q4U zLE}Knk4HeW2xd#eY2;Fib>5q@Z2VNvRoae47uEM8Kxic4`;|2MXY5?MVLZ(eO49Xx z;hWM9;<$v7A@COF4+jla{PvnN)_KRA z!VfJDC>W!46O9U5si3RRJ}P) zxOoXHXA68tH{AwmHcmwaC?j8J3u@(;j@0jWU1ohlDeddRc%N?Maoe)*B(LezOK0Ae zeSGPqs6&w|waVRd(wIOB6dW)RbT>NJcr$yyc5=1BK<@a!n%~I0VhidS@GI;NrgpqJC||y~~eAL0E4troe)iM!;#yTr4dvDlI-- zU()Lf^*Z)~cUHDjn`Nog43Js45TtV~hztp9;b;_#Ok`(Eg%l zBcPsr+WLZZo8FNDIuEuPDU7XN8X4lRve-gHt%4E=ULtM`3%#NXr2N@(_EVjV*|on{ zIl!$f0HW5y&%ogKq?Oezqw+mV9dq-KLiPs~DocYizehEcYt-+Rt+O=u{e0W}epq`# z>IctrW_K@%hSS=q*pem(K**R+A|I86aT34Mxy@hpZ44~WG}E$@3KYt(Jdy{Z%@r(& zh%dFi53MsORYn|?H2Kr6{+a+)Q%!d#&~>*C5GoOUQWv1;m~fVsJBl!`W`*Xqq#3nz z(8i+xb>|vpTqY})kXnhGzMm*iX?(_Z=rTec)OegA542kS9n6wm_axA54!p`RK;i;3 z8=0WPgZP25>i-!b`)?Ls_Eq{=2Y|{