From c5a60b94a9114c41afe34568df6e8c30ad884026 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 13 Feb 2020 17:35:18 +1300 Subject: [PATCH 1/9] =?UTF-8?q?address=20flaky=20test=20where=20instances?= =?UTF-8?q?=20might=20have=20different=20start=E2=80=A6=20(#57506)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apps/triggers_actions_ui/details.ts | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index ce9160abdb086..95371b5b501f5 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import uuid from 'uuid'; -import { omit } from 'lodash'; +import { omit, mapValues } from 'lodash'; import moment from 'moment'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -210,59 +210,60 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify content await testSubjects.existOrFail('alertInstancesList'); - const { - alertInstances: { - ['us-central']: { - meta: { - lastScheduledActions: { date }, - }, - }, - }, - } = await alerting.alerts.getAlertState(alert.id); + const { alertInstances } = await alerting.alerts.getAlertState(alert.id); - const dateOnAllInstances = moment(date) - .utc() - .format('D MMM YYYY @ HH:mm:ss'); + const dateOnAllInstances = mapValues( + alertInstances, + ({ + meta: { + lastScheduledActions: { date }, + }, + }) => moment(date).utc() + ); const instancesList = await pageObjects.alertDetailsUI.getAlertInstancesList(); expect(instancesList.map(instance => omit(instance, 'duration'))).to.eql([ { instance: 'us-central', status: 'Active', - start: dateOnAllInstances, + start: dateOnAllInstances['us-central'].format('D MMM YYYY @ HH:mm:ss'), }, { instance: 'us-east', status: 'Active', - start: dateOnAllInstances, + start: dateOnAllInstances['us-east'].format('D MMM YYYY @ HH:mm:ss'), }, { instance: 'us-west', status: 'Active', - start: dateOnAllInstances, + start: dateOnAllInstances['us-west'].format('D MMM YYYY @ HH:mm:ss'), }, ]); - const durationFromInstanceTillPageLoad = moment.duration( - testBeganAt.diff(moment(date).utc()) + const durationFromInstanceTillPageLoad = mapValues(dateOnAllInstances, date => + moment.duration(testBeganAt.diff(moment(date).utc())) ); instancesList - .map(alertInstance => alertInstance.duration.split(':').map(part => parseInt(part, 10))) - .map(([hours, minutes, seconds]) => - moment.duration({ + .map(alertInstance => ({ + id: alertInstance.instance, + duration: alertInstance.duration.split(':').map(part => parseInt(part, 10)), + })) + .map(({ id, duration: [hours, minutes, seconds] }) => ({ + id, + duration: moment.duration({ hours, minutes, seconds, - }) - ) - .forEach(alertInstanceDuration => { + }), + })) + .forEach(({ id, duration }) => { // make sure the duration is within a 10 second range which is // good enough as the alert interval is 1m, so we know it is a fresh value - expect(alertInstanceDuration.as('milliseconds')).to.greaterThan( - durationFromInstanceTillPageLoad.subtract(1000 * 10).as('milliseconds') + expect(duration.as('milliseconds')).to.greaterThan( + durationFromInstanceTillPageLoad[id].subtract(1000 * 10).as('milliseconds') ); - expect(alertInstanceDuration.as('milliseconds')).to.lessThan( - durationFromInstanceTillPageLoad.add(1000 * 10).as('milliseconds') + expect(duration.as('milliseconds')).to.lessThan( + durationFromInstanceTillPageLoad[id].add(1000 * 10).as('milliseconds') ); }); }); From 06df2b0db65a97888d47fed024597e627a3290e0 Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Thu, 13 Feb 2020 09:44:01 +0200 Subject: [PATCH 2/9] [Telemetry] Migrate public to NP (#56285) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * NP telemetry plugin barebones * fully migrate telemetry sender * license plugin to use NP telemetry * fully migrated public to NP * finish components testing * fix all tests * self code review * remove commented code * bracket notication for private methods * bracket notication for private methods * update license management tests * afharo code review fixes * type safe private method access in tests * fix typecheck * more type check fixes * i18n check * fix welcome page tests * i18n optedInNoticeBanner title * fix advanced settings field settings * field name * fix home snapshots * listen to app id change * NP code review fixes * NP code review fixes * update telemetry configs with np deprecations * pass telemetry from setup instead of npStart * type check * update core snapshots with new api exposed * remove debugging logs * update home contract * update home contract * fix test eslint import * navigate back to dashboard before start of next case for reporting * gitignore reporting failure_debug generated dir * use gotoDashboardEditMode instead of switch * = instead of : * merge master * escape unused forced types in Field * rename mock to mocks for eslint * Update src/plugins/telemetry/public/components/telemetry_management_section.tsx Co-Authored-By: Alejandro Fernández Haro * fix save/clear type Co-authored-by: Alejandro Fernández Haro Co-authored-by: Elastic Machine --- .i18nrc.json | 5 +- ...n-public.applicationstart.currentappid_.md | 13 + .../kibana-plugin-public.applicationstart.md | 1 + .../application/application_service.mock.ts | 17 +- src/core/public/application/types.ts | 11 +- src/core/public/legacy/legacy_service.ts | 1 + src/core/public/plugins/plugin_context.ts | 1 + src/core/public/public.api.md | 1 + .../config/deprecation/core_deprecations.ts | 3 + .../core_plugins/kibana/public/home/index.ts | 31 +- .../kibana/public/home/kibana_services.ts | 4 +- .../__snapshots__/home.test.js.snap | 1 - .../__snapshots__/welcome.test.tsx.snap | 46 +- .../public/home/np_ready/components/home.js | 11 +- .../home/np_ready/components/home_app.js | 5 +- .../home/np_ready/components/welcome.test.tsx | 24 +- .../home/np_ready/components/welcome.tsx | 56 +- .../core_plugins/kibana/public/home/plugin.ts | 17 +- .../telemetry/common/constants.ts | 5 - .../get_xpack_config_with_deprecated.ts | 41 -- src/legacy/core_plugins/telemetry/index.ts | 36 +- .../__snapshots__/telemetry_form.test.js.snap | 80 --- .../public/components/telemetry_form.test.js | 83 --- .../public/hacks/__tests__/fetch_telemetry.js | 55 -- .../public/hacks/__tests__/telemetry.js | 29 - .../telemetry/public/hacks/fetch_telemetry.js | 44 -- .../telemetry/public/hacks/telemetry.js | 120 ---- .../telemetry/public/hacks/telemetry.test.js | 306 ---------- .../telemetry/public/hacks/telemetry_init.ts | 53 -- .../hacks/welcome_banner/click_banner.js | 77 --- .../hacks/welcome_banner/click_banner.test.js | 128 ---- .../welcome_banner/handle_old_settings.js | 85 --- .../handle_old_settings.test.js | 208 ------- .../hacks/welcome_banner/inject_banner.js | 76 --- .../hacks/welcome_banner/render_banner.js | 46 -- .../welcome_banner/render_notice_banner.js | 38 -- .../welcome_banner/should_show_banner.js | 40 -- .../welcome_banner/should_show_banner.test.js | 91 --- .../public/services/telemetry_opt_in.test.js | 148 ----- .../services/telemetry_opt_in.test.mocks.js | 60 -- .../public/services/telemetry_opt_in.ts | 154 ----- .../views/management/{index.js => index.ts} | 0 .../{management.js => management.tsx} | 40 +- .../usage/telemetry_usage_collector.ts | 3 +- .../core_plugins/telemetry/server/fetcher.ts | 3 +- .../handle_old_settings.ts | 59 ++ .../handle_old_settings/index.ts} | 2 +- .../core_plugins/telemetry/server/index.ts | 1 + .../ui/public/new_platform/new_platform.ts | 3 + .../query_string_input.test.tsx.snap | 66 ++ .../telemetry/common/constants.ts} | 27 +- src/plugins/telemetry/kibana.json | 6 + .../__snapshots__/opt_in_banner.test.tsx.snap | 54 ++ .../opt_in_example_flyout.test.tsx.snap} | 0 .../opt_in_message.test.tsx.snap | 1 + .../opted_in_notice_banner.test.tsx.snap | 0 .../telemetry/public/components/index.ts | 8 +- .../public/components/opt_in_banner.test.tsx | 64 ++ .../public/components/opt_in_banner.tsx} | 13 +- .../opt_in_example_flyout.test.tsx} | 7 +- .../components/opt_in_example_flyout.tsx} | 31 +- .../public/components/opt_in_message.test.tsx | 4 +- .../public/components/opt_in_message.tsx | 25 +- .../opted_in_notice_banner.test.tsx | 6 +- .../components/opted_in_notice_banner.tsx | 32 +- .../telemetry_management_section.tsx} | 102 ++-- .../telemetry/public/index.ts} | 9 +- src/plugins/telemetry/public/mocks.ts | 85 +++ src/plugins/telemetry/public/plugin.ts | 118 ++++ .../telemetry/public/services/index.ts | 22 + .../telemetry_notifications}/index.ts | 3 +- .../render_opt_in_banner.test.ts} | 27 +- .../render_opt_in_banner.tsx | 35 ++ .../render_opted_in_notice_banner.test.ts} | 31 +- .../render_opted_in_notice_banner.tsx} | 17 +- .../telemetry_notifications.test.ts | 55 ++ .../telemetry_notifications.ts | 88 +++ .../public/services/telemetry_sender.test.ts | 272 +++++++++ .../public/services/telemetry_sender.ts | 100 +++ .../public/services/telemetry_service.test.ts | 139 +++++ .../public/services/telemetry_service.ts | 165 +++++ test/functional/config.ie.js | 7 +- test/functional/config.js | 7 +- x-pack/.gitignore | 1 + .../telemetry_opt_in.test.js.snap | 576 ------------------ .../upload_license.test.tsx.snap | 5 - .../__jest__/telemetry_opt_in.test.js | 43 -- .../public/np_ready/application/app.js | 7 +- .../public/np_ready/application/boot.tsx | 8 +- .../telemetry_opt_in/{index.js => index.ts} | 0 ...lemetry_opt_in.js => telemetry_opt_in.tsx} | 51 +- .../np_ready/application/lib/telemetry.js | 36 -- .../np_ready/application/lib/telemetry.ts | 24 + .../license_dashboard/license_dashboard.js | 4 +- .../start_trial/{index.js => index.ts} | 1 + .../{start_trial.js => start_trial.tsx} | 66 +- .../sections/upload_license/upload_license.js | 29 +- .../public/np_ready/plugin.ts | 6 +- .../public/register_route.ts | 21 +- x-pack/legacy/plugins/xpack_main/index.js | 18 - .../translations/translations/ja-JP.json | 6 +- .../translations/translations/zh-CN.json | 6 +- x-pack/test/functional/config.ie.js | 7 +- x-pack/test/reporting/functional/reporting.js | 7 +- 104 files changed, 1815 insertions(+), 3095 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-public.applicationstart.currentappid_.md delete mode 100644 src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts delete mode 100644 src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap delete mode 100644 src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/__tests__/telemetry.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/telemetry.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/telemetry.test.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/inject_banner.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js delete mode 100644 src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js delete mode 100644 src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js delete mode 100644 src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts rename src/legacy/core_plugins/telemetry/public/views/management/{index.js => index.ts} (100%) rename src/legacy/core_plugins/telemetry/public/views/management/{management.js => management.tsx} (52%) create mode 100644 src/legacy/core_plugins/telemetry/server/handle_old_settings/handle_old_settings.ts rename src/legacy/core_plugins/telemetry/{public/hacks/welcome_banner/index.js => server/handle_old_settings/index.ts} (93%) rename src/{legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_opt_in_banner.js => plugins/telemetry/common/constants.ts} (61%) create mode 100644 src/plugins/telemetry/kibana.json create mode 100644 src/plugins/telemetry/public/components/__snapshots__/opt_in_banner.test.tsx.snap rename src/{legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_details_component.test.tsx.snap => plugins/telemetry/public/components/__snapshots__/opt_in_example_flyout.test.tsx.snap} (100%) rename src/{legacy/core_plugins => plugins}/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap (97%) rename src/{legacy/core_plugins => plugins}/telemetry/public/components/__snapshots__/opted_in_notice_banner.test.tsx.snap (100%) rename src/{legacy/core_plugins => plugins}/telemetry/public/components/index.ts (77%) create mode 100644 src/plugins/telemetry/public/components/opt_in_banner.test.tsx rename src/{legacy/core_plugins/telemetry/public/components/opt_in_banner_component.tsx => plugins/telemetry/public/components/opt_in_banner.tsx} (84%) rename src/{legacy/core_plugins/telemetry/public/components/opt_in_details_component.test.tsx => plugins/telemetry/public/components/opt_in_example_flyout.test.tsx} (84%) rename src/{legacy/core_plugins/telemetry/public/components/opt_in_details_component.tsx => plugins/telemetry/public/components/opt_in_example_flyout.tsx} (91%) rename src/{legacy/core_plugins => plugins}/telemetry/public/components/opt_in_message.test.tsx (89%) rename src/{legacy/core_plugins => plugins}/telemetry/public/components/opt_in_message.tsx (81%) rename src/{legacy/core_plugins => plugins}/telemetry/public/components/opted_in_notice_banner.test.tsx (84%) rename src/{legacy/core_plugins => plugins}/telemetry/public/components/opted_in_notice_banner.tsx (75%) rename src/{legacy/core_plugins/telemetry/public/components/telemetry_form.js => plugins/telemetry/public/components/telemetry_management_section.tsx} (70%) rename src/{legacy/core_plugins/telemetry/public/hacks/telemetry_opt_in.js => plugins/telemetry/public/index.ts} (81%) create mode 100644 src/plugins/telemetry/public/mocks.ts create mode 100644 src/plugins/telemetry/public/plugin.ts create mode 100644 src/plugins/telemetry/public/services/index.ts rename src/{legacy/core_plugins/telemetry/public/services => plugins/telemetry/public/services/telemetry_notifications}/index.ts (88%) rename src/{legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.test.js => plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.test.ts} (56%) create mode 100644 src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx rename src/{legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js => plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.test.ts} (52%) rename src/{legacy/core_plugins/telemetry/public/services/path.ts => plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.tsx} (59%) create mode 100644 src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.test.ts create mode 100644 src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts create mode 100644 src/plugins/telemetry/public/services/telemetry_sender.test.ts create mode 100644 src/plugins/telemetry/public/services/telemetry_sender.ts create mode 100644 src/plugins/telemetry/public/services/telemetry_service.test.ts create mode 100644 src/plugins/telemetry/public/services/telemetry_service.ts delete mode 100644 x-pack/legacy/plugins/license_management/__jest__/__snapshots__/telemetry_opt_in.test.js.snap delete mode 100644 x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js rename x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/{index.js => index.ts} (100%) rename x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/{telemetry_opt_in.js => telemetry_opt_in.tsx} (84%) delete mode 100644 x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.js create mode 100644 x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.ts rename x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/{index.js => index.ts} (95%) rename x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/{start_trial.js => start_trial.tsx} (87%) diff --git a/.i18nrc.json b/.i18nrc.json index c171b842254ee..6874d02304e49 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -37,7 +37,10 @@ "savedObjects": "src/plugins/saved_objects", "server": "src/legacy/server", "statusPage": "src/legacy/core_plugins/status_page", - "telemetry": "src/legacy/core_plugins/telemetry", + "telemetry": [ + "src/legacy/core_plugins/telemetry", + "src/plugins/telemetry" + ], "tileMap": "src/legacy/core_plugins/tile_map", "timelion": ["src/legacy/core_plugins/timelion", "src/legacy/core_plugins/vis_type_timelion", "src/plugins/timelion"], "uiActions": "src/plugins/ui_actions", diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.currentappid_.md b/docs/development/core/public/kibana-plugin-public.applicationstart.currentappid_.md new file mode 100644 index 0000000000000..d3ceeabcd81f4 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.currentappid_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [currentAppId$](./kibana-plugin-public.applicationstart.currentappid_.md) + +## ApplicationStart.currentAppId$ property + +An observable that emits the current application id and each subsequent id update. + +Signature: + +```typescript +currentAppId$: Observable; +``` diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.md b/docs/development/core/public/kibana-plugin-public.applicationstart.md index 3ad7e3b1656d8..433ce87419ae8 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.md @@ -16,6 +16,7 @@ export interface ApplicationStart | Property | Type | Description | | --- | --- | --- | | [capabilities](./kibana-plugin-public.applicationstart.capabilities.md) | RecursiveReadonly<Capabilities> | Gets the read-only capabilities. | +| [currentAppId$](./kibana-plugin-public.applicationstart.currentappid_.md) | Observable<string | undefined> | An observable that emits the current application id and each subsequent id update. | ## Methods diff --git a/src/core/public/application/application_service.mock.ts b/src/core/public/application/application_service.mock.ts index dee47315fc322..d2a827d381be5 100644 --- a/src/core/public/application/application_service.mock.ts +++ b/src/core/public/application/application_service.mock.ts @@ -43,12 +43,17 @@ const createInternalSetupContractMock = (): jest.Mocked => ({ - capabilities: capabilitiesServiceMock.createStartContract().capabilities, - navigateToApp: jest.fn(), - getUrlForApp: jest.fn(), - registerMountContext: jest.fn(), -}); +const createStartContractMock = (): jest.Mocked => { + const currentAppId$ = new Subject(); + + return { + currentAppId$: currentAppId$.asObservable(), + capabilities: capabilitiesServiceMock.createStartContract().capabilities, + navigateToApp: jest.fn(), + getUrlForApp: jest.fn(), + registerMountContext: jest.fn(), + }; +}; const createInternalStartContractMock = (): jest.Mocked => { const currentAppId$ = new Subject(); diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 17fdfc627187e..493afd1fec9db 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -612,11 +612,19 @@ export interface ApplicationStart { contextName: T, provider: IContextProvider ): void; + + /** + * An observable that emits the current application id and each subsequent id update. + */ + currentAppId$: Observable; } /** @internal */ export interface InternalApplicationStart - extends Pick { + extends Pick< + ApplicationStart, + 'capabilities' | 'navigateToApp' | 'getUrlForApp' | 'currentAppId$' + > { /** * Apps available based on the current capabilities. * Should be used to show navigation links and make routing decisions. @@ -640,7 +648,6 @@ export interface InternalApplicationStart ): void; // Internal APIs - currentAppId$: Observable; getComponent(): JSX.Element | null; } diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index e4788e686dd45..1b7e25f585566 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -121,6 +121,7 @@ export class LegacyPlatformService { const legacyCore: LegacyCoreStart = { ...core, application: { + currentAppId$: core.application.currentAppId$, capabilities: core.application.capabilities, getUrlForApp: core.application.getUrlForApp, navigateToApp: core.application.navigateToApp, diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 48100cba4f26e..19cfadf70be1b 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -134,6 +134,7 @@ export function createPluginStartContext< ): CoreStart { return { application: { + currentAppId$: deps.application.currentAppId$, capabilities: deps.application.capabilities, navigateToApp: deps.application.navigateToApp, getUrlForApp: deps.application.getUrlForApp, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index aa7ca4fee675e..aab88b0befba3 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -98,6 +98,7 @@ export interface ApplicationSetup { // @public (undocumented) export interface ApplicationStart { capabilities: RecursiveReadonly; + currentAppId$: Observable; getUrlForApp(appId: string, options?: { path?: string; }): string; diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 3aa7f9e2aa8ad..4fa51dcd5a082 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -115,6 +115,9 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ renameFromRoot('optimize.lazyHost', 'optimize.watchHost'), renameFromRoot('optimize.lazyPrebuild', 'optimize.watchPrebuild'), renameFromRoot('optimize.lazyProxyTimeout', 'optimize.watchProxyTimeout'), + renameFromRoot('xpack.xpack_main.telemetry.config', 'telemetry.config'), + renameFromRoot('xpack.xpack_main.telemetry.url', 'telemetry.url'), + renameFromRoot('xpack.xpack_main.telemetry.enabled', 'telemetry.enabled'), renameFromRoot('xpack.telemetry.enabled', 'telemetry.enabled'), renameFromRoot('xpack.telemetry.config', 'telemetry.config'), renameFromRoot('xpack.telemetry.banner', 'telemetry.banner'), diff --git a/src/legacy/core_plugins/kibana/public/home/index.ts b/src/legacy/core_plugins/kibana/public/home/index.ts index c4e58e1a5e1ae..768e1a96de935 100644 --- a/src/legacy/core_plugins/kibana/public/home/index.ts +++ b/src/legacy/core_plugins/kibana/public/home/index.ts @@ -18,30 +18,7 @@ */ import { npSetup, npStart } from 'ui/new_platform'; -import chrome from 'ui/chrome'; -import { HomePlugin, LegacyAngularInjectedDependencies } from './plugin'; -import { TelemetryOptInProvider } from '../../../telemetry/public/services'; -import { IPrivate } from '../../../../../plugins/kibana_legacy/public'; - -/** - * 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 chrome.dangerouslyGetActiveInjector(); - - const Private = injector.get('Private'); - - const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled'); - const telemetryBanner = npStart.core.injectedMetadata.getInjectedVar('telemetryBanner'); - const telemetryOptInProvider = Private(TelemetryOptInProvider); - - return { - telemetryOptInProvider, - shouldShowTelemetryOptIn: - telemetryEnabled && telemetryBanner && !telemetryOptInProvider.getOptIn(), - }; -} +import { HomePlugin } from './plugin'; (async () => { const instance = new HomePlugin(); @@ -49,10 +26,8 @@ async function getAngularDependencies(): Promise unknown; chrome: ChromeStart; - telemetryOptInProvider: any; uiSettings: IUiSettingsClient; config: KibanaLegacySetup['config']; homeConfig: HomePublicPluginSetup['config']; @@ -64,10 +64,10 @@ export interface HomeKibanaServices { banners: OverlayStart['banners']; trackUiMetric: (type: UiStatsMetricType, eventNames: string | string[], count?: number) => void; getBasePath: () => string; - shouldShowTelemetryOptIn: boolean; docLinks: DocLinksStart; addBasePath: (url: string) => string; environment: Environment; + telemetry?: TelemetryPluginStart; } let services: HomeKibanaServices | null = null; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/home.test.js.snap b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/home.test.js.snap index 4563b633c3dfc..9d27362e62739 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/home.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/home.test.js.snap @@ -1054,7 +1054,6 @@ exports[`home welcome should show the normal home page if welcome screen is disa exports[`home welcome should show the welcome screen if enabled, and there are no index patterns defined 1`] = ` diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/welcome.test.tsx.snap b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/welcome.test.tsx.snap index 6f76ceecbba13..df7cc7bcbaed0 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/welcome.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/welcome.test.tsx.snap @@ -67,44 +67,6 @@ exports[`should render a Welcome screen with no telemetry disclaimer 1`] = ` - - - - - - - - - - - @@ -200,16 +162,16 @@ exports[`should render a Welcome screen with the telemetry disclaimer 1`] = ` /> diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js index 0c09c6c3c74fc..617a1810028fc 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js @@ -51,7 +51,6 @@ export class Home extends Component { getServices().homeConfig.disableWelcomeScreen || props.localStorage.getItem(KEY_ENABLE_WELCOME) === 'false' ); - const currentOptInStatus = this.props.getOptInStatus(); this.state = { // If welcome is enabled, we wait for loading to complete // before rendering. This prevents an annoying flickering @@ -60,7 +59,6 @@ export class Home extends Component { isLoading: isWelcomeEnabled, isNewKibanaInstance: false, isWelcomeEnabled, - currentOptInStatus, }; } @@ -224,8 +222,7 @@ export class Home extends Component { ); } @@ -264,6 +261,8 @@ Home.propTypes = { localStorage: PropTypes.object.isRequired, urlBasePath: PropTypes.string.isRequired, mlEnabled: PropTypes.bool.isRequired, - onOptInSeen: PropTypes.func.isRequired, - getOptInStatus: PropTypes.func.isRequired, + telemetry: PropTypes.shape({ + telemetryService: PropTypes.any, + telemetryNotifications: PropTypes.any, + }), }; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js index f6c91b412381c..d7531864582a3 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js @@ -35,7 +35,7 @@ export function HomeApp({ directories }) { getBasePath, addBasePath, environment, - telemetryOptInProvider: { setOptInNoticeSeen, getOptIn }, + telemetry, } = getServices(); const isCloudEnabled = environment.cloud; const mlEnabled = environment.ml; @@ -84,8 +84,7 @@ export function HomeApp({ directories }) { find={savedObjectsClient.find} localStorage={localStorage} urlBasePath={getBasePath()} - onOptInSeen={setOptInNoticeSeen} - getOptInStatus={getOptIn} + telemetry={telemetry} /> diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx index 55c469fa58fc6..d9da47a2b43da 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx @@ -20,6 +20,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Welcome } from './welcome'; +import { telemetryPluginMock } from '../../../../../../../plugins/telemetry/public/mocks'; jest.mock('../../kibana_services', () => ({ getServices: () => ({ @@ -29,27 +30,32 @@ jest.mock('../../kibana_services', () => ({ })); test('should render a Welcome screen with the telemetry disclaimer', () => { + const telemetry = telemetryPluginMock.createSetupContract(); const component = shallow( // @ts-ignore - {}} onOptInSeen={() => {}} /> + {}} telemetry={telemetry} /> ); expect(component).toMatchSnapshot(); }); test('should render a Welcome screen with the telemetry disclaimer when optIn is true', () => { + const telemetry = telemetryPluginMock.createSetupContract(); + telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); const component = shallow( // @ts-ignore - {}} onOptInSeen={() => {}} currentOptInStatus={true} /> + {}} telemetry={telemetry} /> ); expect(component).toMatchSnapshot(); }); test('should render a Welcome screen with the telemetry disclaimer when optIn is false', () => { + const telemetry = telemetryPluginMock.createSetupContract(); + telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(false); const component = shallow( // @ts-ignore - {}} onOptInSeen={() => {}} currentOptInStatus={false} /> + {}} telemetry={telemetry} /> ); expect(component).toMatchSnapshot(); @@ -59,19 +65,21 @@ test('should render a Welcome screen with no telemetry disclaimer', () => { // @ts-ignore const component = shallow( // @ts-ignore - {}} onOptInSeen={() => {}} /> + {}} telemetry={null} /> ); expect(component).toMatchSnapshot(); }); test('fires opt-in seen when mounted', () => { - const seen = jest.fn(); - + const telemetry = telemetryPluginMock.createSetupContract(); + const mockSetOptedInNoticeSeen = jest.fn(); + // @ts-ignore + telemetry.telemetryNotifications.setOptedInNoticeSeen = mockSetOptedInNoticeSeen; shallow( // @ts-ignore - {}} onOptInSeen={seen} /> + {}} telemetry={telemetry} /> ); - expect(seen).toHaveBeenCalled(); + expect(mockSetOptedInNoticeSeen).toHaveBeenCalled(); }); diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx index 6983aabc4c7b1..7906caeda1b38 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx @@ -38,13 +38,14 @@ import { import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage } from '@kbn/i18n/react'; import { getServices } from '../../kibana_services'; +import { TelemetryPluginStart } from '../../../../../../../plugins/telemetry/public'; +import { PRIVACY_STATEMENT_URL } from '../../../../../../../plugins/telemetry/common/constants'; import { SampleDataCard } from './sample_data'; interface Props { urlBasePath: string; onSkip: () => void; - onOptInSeen: () => any; - currentOptInStatus: boolean; + telemetry?: TelemetryPluginStart; } /** @@ -75,8 +76,11 @@ export class Welcome extends React.Component { }; componentDidMount() { + const { telemetry } = this.props; this.services.trackUiMetric(METRIC_TYPE.LOADED, 'welcomeScreenMount'); - this.props.onOptInSeen(); + if (telemetry) { + telemetry.telemetryNotifications.setOptedInNoticeSeen(); + } document.addEventListener('keydown', this.hideOnEsc); } @@ -85,7 +89,13 @@ export class Welcome extends React.Component { } private renderTelemetryEnabledOrDisabledText = () => { - if (this.props.currentOptInStatus) { + const { telemetry } = this.props; + if (!telemetry) { + return null; + } + + const isOptedIn = telemetry.telemetryService.getIsOptedIn(); + if (isOptedIn) { return ( { }; render() { - const { urlBasePath } = this.props; + const { urlBasePath, telemetry } = this.props; return (
@@ -154,24 +164,24 @@ export class Welcome extends React.Component { onDecline={this.onSampleDataDecline} /> - - - - - - {this.renderTelemetryEnabledOrDisabledText()} - - + {!!telemetry && ( + + + + + + + {this.renderTelemetryEnabledOrDisabledText()} + + + + )}
diff --git a/src/legacy/core_plugins/kibana/public/home/plugin.ts b/src/legacy/core_plugins/kibana/public/home/plugin.ts index e530906d5698e..5cc7c9c11dd2f 100644 --- a/src/legacy/core_plugins/kibana/public/home/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/home/plugin.ts @@ -20,6 +20,7 @@ import { CoreSetup, CoreStart, LegacyNavLink, Plugin, UiSettingsState } from 'kibana/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { TelemetryPluginStart } from 'src/plugins/telemetry/public'; import { setServices } from './kibana_services'; import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; @@ -30,14 +31,10 @@ import { FeatureCatalogueEntry, } from '../../../../../plugins/home/public'; -export interface LegacyAngularInjectedDependencies { - telemetryOptInProvider: any; - shouldShowTelemetryOptIn: boolean; -} - export interface HomePluginStartDependencies { data: DataPublicPluginStart; home: HomePublicPluginStart; + telemetry?: TelemetryPluginStart; } export interface HomePluginSetupDependencies { @@ -55,7 +52,6 @@ export interface HomePluginSetupDependencies { devMode: boolean; uiSettings: { defaults: UiSettingsState; user?: UiSettingsState | undefined }; }; - getAngularDependencies: () => Promise; }; usageCollection: UsageCollectionSetup; kibanaLegacy: KibanaLegacySetup; @@ -67,6 +63,7 @@ export class HomePlugin implements Plugin { private savedObjectsClient: any = null; private environment: Environment | null = null; private directories: readonly FeatureCatalogueEntry[] | null = null; + private telemetry?: TelemetryPluginStart; setup( core: CoreSetup, @@ -74,7 +71,7 @@ export class HomePlugin implements Plugin { home, kibanaLegacy, usageCollection, - __LEGACY: { getAngularDependencies, ...legacyServices }, + __LEGACY: { ...legacyServices }, }: HomePluginSetupDependencies ) { kibanaLegacy.registerLegacyApp({ @@ -82,7 +79,6 @@ export class HomePlugin implements Plugin { title: 'Home', mount: async ({ core: contextCore }, params) => { const trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, 'Kibana_home'); - const angularDependencies = await getAngularDependencies(); setServices({ ...legacyServices, trackUiMetric, @@ -92,6 +88,7 @@ export class HomePlugin implements Plugin { getInjected: core.injectedMetadata.getInjectedVar, docLinks: contextCore.docLinks, savedObjectsClient: this.savedObjectsClient!, + telemetry: this.telemetry, chrome: contextCore.chrome, uiSettings: core.uiSettings, addBasePath: core.http.basePath.prepend, @@ -101,7 +98,6 @@ export class HomePlugin implements Plugin { config: kibanaLegacy.config, homeConfig: home.config, directories: this.directories!, - ...angularDependencies, }); const { renderApp } = await import('./np_ready/application'); return await renderApp(params.element); @@ -109,10 +105,11 @@ export class HomePlugin implements Plugin { }); } - start(core: CoreStart, { data, home }: HomePluginStartDependencies) { + start(core: CoreStart, { data, home, telemetry }: HomePluginStartDependencies) { this.environment = home.environment.get(); this.directories = home.featureCatalogue.get(); this.dataStart = data; + this.telemetry = telemetry; this.savedObjectsClient = core.savedObjects.client; } diff --git a/src/legacy/core_plugins/telemetry/common/constants.ts b/src/legacy/core_plugins/telemetry/common/constants.ts index cf2c9c883871b..52981c04ad34a 100644 --- a/src/legacy/core_plugins/telemetry/common/constants.ts +++ b/src/legacy/core_plugins/telemetry/common/constants.ts @@ -43,11 +43,6 @@ export const getConfigTelemetryDesc = () => { */ export const REPORT_INTERVAL_MS = 86400000; -/* - * Key for the localStorage service - */ -export const LOCALSTORAGE_KEY = 'telemetry.data'; - /** * Link to the Elastic Telemetry privacy statement. */ diff --git a/src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts b/src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts deleted file mode 100644 index 3f7a8d3410993..0000000000000 --- a/src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts +++ /dev/null @@ -1,41 +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 { KibanaConfig } from 'src/legacy/server/kbn_server'; - -export function getXpackConfigWithDeprecated(config: KibanaConfig, configPath: string) { - try { - const deprecatedXpackmainConfig = config.get(`xpack.xpack_main.${configPath}`); - if (typeof deprecatedXpackmainConfig !== 'undefined') { - return deprecatedXpackmainConfig; - } - } catch (err) { - // swallow error - } - try { - const deprecatedXpackConfig = config.get(`xpack.${configPath}`); - if (typeof deprecatedXpackConfig !== 'undefined') { - return deprecatedXpackConfig; - } - } catch (err) { - // swallow error - } - - return config.get(configPath); -} diff --git a/src/legacy/core_plugins/telemetry/index.ts b/src/legacy/core_plugins/telemetry/index.ts index 2a81e3fa05c6c..ec70380d83a0a 100644 --- a/src/legacy/core_plugins/telemetry/index.ts +++ b/src/legacy/core_plugins/telemetry/index.ts @@ -22,14 +22,17 @@ import { resolve } from 'path'; import JoiNamespace from 'joi'; import { Server } from 'hapi'; import { CoreSetup, PluginInitializerContext } from 'src/core/server'; -import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getConfigPath } from '../../../core/server/path'; // @ts-ignore import mappings from './mappings.json'; -import { CONFIG_TELEMETRY, getConfigTelemetryDesc } from './common/constants'; -import { getXpackConfigWithDeprecated } from './common/get_xpack_config_with_deprecated'; -import { telemetryPlugin, replaceTelemetryInjectedVars, FetcherTask, PluginsSetup } from './server'; +import { + telemetryPlugin, + replaceTelemetryInjectedVars, + FetcherTask, + PluginsSetup, + handleOldSettings, +} from './server'; const ENDPOINT_VERSION = 'v2'; @@ -76,16 +79,6 @@ const telemetry = (kibana: any) => { }, uiExports: { managementSections: ['plugins/telemetry/views/management'], - uiSettingDefaults: { - [CONFIG_TELEMETRY]: { - name: i18n.translate('telemetry.telemetryConfigTitle', { - defaultMessage: 'Telemetry opt-in', - }), - description: getConfigTelemetryDesc(), - value: false, - readonly: true, - }, - }, savedObjectSchemas: { telemetry: { isNamespaceAgnostic: true, @@ -98,11 +91,11 @@ const telemetry = (kibana: any) => { injectDefaultVars(server: Server) { const config = server.config(); return { - telemetryEnabled: getXpackConfigWithDeprecated(config, 'telemetry.enabled'), - telemetryUrl: getXpackConfigWithDeprecated(config, 'telemetry.url'), + telemetryEnabled: config.get('telemetry.enabled'), + telemetryUrl: config.get('telemetry.url'), telemetryBanner: config.get('telemetry.allowChangingOptInStatus') !== false && - getXpackConfigWithDeprecated(config, 'telemetry.banner'), + config.get('telemetry.banner'), telemetryOptedIn: config.get('telemetry.optIn'), telemetryOptInStatusUrl: config.get('telemetry.optInStatusUrl'), allowChangingOptInStatus: config.get('telemetry.allowChangingOptInStatus'), @@ -110,14 +103,13 @@ const telemetry = (kibana: any) => { telemetryNotifyUserAboutOptInDefault: false, }; }, - hacks: ['plugins/telemetry/hacks/telemetry_init', 'plugins/telemetry/hacks/telemetry_opt_in'], mappings, }, postInit(server: Server) { const fetcherTask = new FetcherTask(server); fetcherTask.start(); }, - init(server: Server) { + async init(server: Server) { const { usageCollection } = server.newPlatform.setup.plugins; const initializerContext = { env: { @@ -145,6 +137,12 @@ const telemetry = (kibana: any) => { log: server.log, } as any) as CoreSetup; + try { + await handleOldSettings(server); + } catch (err) { + server.log(['warning', 'telemetry'], 'Unable to update legacy telemetry configs.'); + } + const pluginsSetup: PluginsSetup = { usageCollection, }; diff --git a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap b/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap deleted file mode 100644 index 079a43e77616d..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap +++ /dev/null @@ -1,80 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TelemetryForm doesn't render form when not allowed to change optIn status 1`] = `""`; - -exports[`TelemetryForm renders as expected when allows to change optIn status 1`] = ` - - - - - - -

- -

-
-
-
- - -

- - - , - } - } - /> -

-

- - - -

- , - "type": "boolean", - "value": false, - } - } - /> -
-
-
-`; diff --git a/src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js b/src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js deleted file mode 100644 index fe0c2c3449af1..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js +++ /dev/null @@ -1,83 +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 { mockInjectedMetadata } from '../services/telemetry_opt_in.test.mocks'; -import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { TelemetryForm } from './telemetry_form'; -import { TelemetryOptInProvider } from '../services'; - -const buildTelemetryOptInProvider = () => { - const mockHttp = { - post: jest.fn(), - }; - - const mockInjector = { - get: key => { - switch (key) { - case '$http': - return mockHttp; - case 'allowChangingOptInStatus': - return true; - default: - return null; - } - }, - }; - - const chrome = { - addBasePath: url => url, - }; - - return new TelemetryOptInProvider(mockInjector, chrome); -}; - -describe('TelemetryForm', () => { - it('renders as expected when allows to change optIn status', () => { - mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true }); - - expect( - shallowWithIntl( - - ) - ).toMatchSnapshot(); - }); - - it(`doesn't render form when not allowed to change optIn status`, () => { - mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: false }); - - expect( - shallowWithIntl( - - ) - ).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js deleted file mode 100644 index ad9ee0998e3bb..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js +++ /dev/null @@ -1,55 +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 sinon from 'sinon'; - -import { fetchTelemetry } from '../fetch_telemetry'; - -describe('fetch_telemetry', () => { - it('fetchTelemetry calls expected URL with 20 minutes - now', () => { - const response = Promise.resolve(); - const $http = { - post: sinon.stub(), - }; - const basePath = 'fake'; - const moment = { - subtract: sinon.stub(), - toISOString: () => 'max123', - }; - - moment.subtract.withArgs(20, 'minutes').returns({ - toISOString: () => 'min456', - }); - - $http.post - .withArgs(`fake/api/telemetry/v2/clusters/_stats`, { - unencrypted: true, - timeRange: { - min: 'min456', - max: 'max123', - }, - }) - .returns(response); - - expect(fetchTelemetry($http, { basePath, _moment: () => moment, unencrypted: true })).to.be( - response - ); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/__tests__/telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/__tests__/telemetry.js deleted file mode 100644 index 74f1de4934a78..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/__tests__/telemetry.js +++ /dev/null @@ -1,29 +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 'ui/modules'; - -// This overrides settings for other UI tests -uiModules - .get('kibana') - // disable stat reporting while running tests, - // MockInjector used in these tests is not impacted - .constant('telemetryEnabled', false) - .constant('telemetryOptedIn', null) - .constant('telemetryUrl', 'not.a.valid.url.0'); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry.js deleted file mode 100644 index ede81f638a3fc..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry.js +++ /dev/null @@ -1,44 +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 uiChrome from 'ui/chrome'; -import moment from 'moment'; - -/** - * Fetch Telemetry data by calling the Kibana API. - * - * @param {Object} $http The HTTP handler - * @param {String} basePath The base URI - * @param {Function} _moment moment.js, but injectable for tests - * @return {Promise} An array of cluster Telemetry objects. - */ -export function fetchTelemetry( - $http, - { basePath = uiChrome.getBasePath(), _moment = moment, unencrypted = false } = {} -) { - return $http.post(`${basePath}/api/telemetry/v2/clusters/_stats`, { - unencrypted, - timeRange: { - min: _moment() - .subtract(20, 'minutes') - .toISOString(), - max: _moment().toISOString(), - }, - }); -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/telemetry.js deleted file mode 100644 index 8fa777ead3e4b..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/telemetry.js +++ /dev/null @@ -1,120 +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 { REPORT_INTERVAL_MS, LOCALSTORAGE_KEY } from '../../common/constants'; - -export class Telemetry { - /** - * @param {Object} $injector - AngularJS injector service - * @param {Function} fetchTelemetry Method used to fetch telemetry data (expects an array response) - */ - constructor($injector, fetchTelemetry) { - this._storage = $injector.get('localStorage'); - this._$http = $injector.get('$http'); - this._telemetryUrl = $injector.get('telemetryUrl'); - this._telemetryOptedIn = $injector.get('telemetryOptedIn'); - this._fetchTelemetry = fetchTelemetry; - this._sending = false; - - // try to load the local storage data - const attributes = this._storage.get(LOCALSTORAGE_KEY) || {}; - this._lastReport = attributes.lastReport; - } - - _saveToBrowser() { - // we are the only code that manipulates this key, so it's safe to blindly overwrite the whole object - this._storage.set(LOCALSTORAGE_KEY, { lastReport: this._lastReport }); - } - - /** - * Determine if we are due to send a new report. - * - * @returns {Boolean} true if a new report should be sent. false otherwise. - */ - _checkReportStatus() { - // check if opt-in for telemetry is enabled - if (this._telemetryOptedIn) { - // returns NaN for any malformed or unset (null/undefined) value - const lastReport = parseInt(this._lastReport, 10); - // If it's been a day since we last sent telemetry - if (isNaN(lastReport) || Date.now() - lastReport > REPORT_INTERVAL_MS) { - return true; - } - } - - return false; - } - - /** - * Check report permission and if passes, send the report - * - * @returns {Promise} Always. - */ - _sendIfDue() { - if (this._sending || !this._checkReportStatus()) { - return Promise.resolve(false); - } - - // mark that we are working so future requests are ignored until we're done - this._sending = true; - - return ( - this._fetchTelemetry() - .then(response => { - const clusters = [].concat(response.data); - return Promise.all( - clusters.map(cluster => { - const req = { - method: 'POST', - url: this._telemetryUrl, - data: cluster, - }; - // if passing data externally, then suppress kbnXsrfToken - if (this._telemetryUrl.match(/^https/)) { - req.kbnXsrfToken = false; - } - return this._$http(req); - }) - ); - }) - // the response object is ignored because we do not check it - .then(() => { - // we sent a report, so we need to record and store the current timestamp - this._lastReport = Date.now(); - this._saveToBrowser(); - }) - // no ajaxErrorHandlers for telemetry - .catch(() => null) - .then(() => { - this._sending = false; - return true; // sent, but not necessarilly successfully - }) - ); - } - - /** - * Public method - * - * @returns {Number} `window.setInterval` response to allow cancelling the interval. - */ - start() { - // continuously check if it's due time for a report - return window.setInterval(() => this._sendIfDue(), 60000); - } -} // end class diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry.test.js b/src/legacy/core_plugins/telemetry/public/hacks/telemetry.test.js deleted file mode 100644 index 45a0653cd7a54..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/telemetry.test.js +++ /dev/null @@ -1,306 +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 { Telemetry } from './telemetry'; -import { REPORT_INTERVAL_MS, LOCALSTORAGE_KEY } from '../../common/constants'; - -describe('telemetry class', () => { - const clusters = [{ cluster_uuid: 'fake-123' }, { cluster_uuid: 'fake-456' }]; - const telemetryUrl = 'https://not.a.valid.url.0'; - const mockFetchTelemetry = () => Promise.resolve({ data: clusters }); - // returns a function that behaves like the injector by fetching the requested key from the object directly - // for example: - // { '$http': jest.fn() } would be how to mock the '$http' injector value - const mockInjectorFromObject = object => { - return { get: key => object[key] }; - }; - - describe('constructor', () => { - test('defaults lastReport if unset', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce(undefined), - }, - $http: jest.fn(), - telemetryOptedIn: true, - telemetryUrl, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._storage).toBe(injector.localStorage); - expect(telemetry._$http).toBe(injector.$http); - expect(telemetry._telemetryOptedIn).toBe(injector.telemetryOptedIn); - expect(telemetry._telemetryUrl).toBe(injector.telemetryUrl); - expect(telemetry._fetchTelemetry).toBe(mockFetchTelemetry); - expect(telemetry._sending).toBe(false); - expect(telemetry._lastReport).toBeUndefined(); - - expect(injector.localStorage.get).toHaveBeenCalledTimes(1); - expect(injector.localStorage.get).toHaveBeenCalledWith(LOCALSTORAGE_KEY); - }); - - test('uses lastReport if set', () => { - const lastReport = Date.now(); - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport }), - }, - $http: jest.fn(), - telemetryOptedIn: true, - telemetryUrl, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._storage).toBe(injector.localStorage); - expect(telemetry._$http).toBe(injector.$http); - expect(telemetry._telemetryOptedIn).toBe(injector.telemetryOptedIn); - expect(telemetry._telemetryUrl).toBe(injector.telemetryUrl); - expect(telemetry._fetchTelemetry).toBe(mockFetchTelemetry); - expect(telemetry._sending).toBe(false); - expect(telemetry._lastReport).toBe(lastReport); - - expect(injector.localStorage.get).toHaveBeenCalledTimes(1); - expect(injector.localStorage.get).toHaveBeenCalledWith(LOCALSTORAGE_KEY); - }); - }); - - test('_saveToBrowser uses _lastReport', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ random: 'junk', gets: 'thrown away' }), - set: jest.fn(), - }, - }; - const lastReport = Date.now(); - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - telemetry._lastReport = lastReport; - - telemetry._saveToBrowser(); - - expect(injector.localStorage.set).toHaveBeenCalledTimes(1); - expect(injector.localStorage.set).toHaveBeenCalledWith(LOCALSTORAGE_KEY, { lastReport }); - }); - - describe('_checkReportStatus', () => { - // send the report if we get to check the time - const lastReportShouldSendNow = Date.now() - REPORT_INTERVAL_MS - 1; - - test('returns false whenever telemetryOptedIn is null', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport: lastReportShouldSendNow }), - }, - telemetryOptedIn: null, // not yet opted in - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(false); - }); - - test('returns false whenever telemetryOptedIn is false', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport: lastReportShouldSendNow }), - }, - telemetryOptedIn: false, // opted out explicitly - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(false); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/27922 - test.skip('returns false if last report is too recent', () => { - const injector = { - localStorage: { - // we expect '>', not '>=' - get: jest.fn().mockReturnValueOnce({ lastReport: Date.now() - REPORT_INTERVAL_MS }), - }, - telemetryOptedIn: true, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(false); - }); - - test('returns true if last report is not defined', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({}), - }, - telemetryOptedIn: true, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(true); - }); - - test('returns true if last report is defined and old enough', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport: lastReportShouldSendNow }), - }, - telemetryOptedIn: true, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(true); - }); - - test('returns true if last report is defined and old enough as a string', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport: lastReportShouldSendNow.toString() }), - }, - telemetryOptedIn: true, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(true); - }); - - test('returns true if last report is defined and malformed', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport: { not: { a: 'number' } } }), - }, - telemetryOptedIn: true, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(true); - }); - }); - - describe('_sendIfDue', () => { - test('ignores and returns false if already sending', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce(undefined), // never sent - }, - telemetryOptedIn: true, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - telemetry._sending = true; - - return expect(telemetry._sendIfDue()).resolves.toBe(false); - }); - - test('ignores and returns false if _checkReportStatus says so', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce(undefined), // never sent, so it would try if opted in - }, - telemetryOptedIn: false, // opted out - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - return expect(telemetry._sendIfDue()).resolves.toBe(false); - }); - - test('sends telemetry when requested', () => { - const now = Date.now(); - const injector = { - $http: jest.fn().mockResolvedValue({}), // ignored response - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport: now - REPORT_INTERVAL_MS - 1 }), - set: jest.fn(), - }, - telemetryOptedIn: true, - telemetryUrl, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect.hasAssertions(); - - return telemetry._sendIfDue().then(result => { - expect(result).toBe(true); - expect(telemetry._sending).toBe(false); - - // should be updated - const lastReport = telemetry._lastReport; - - // if the test runs fast enough it should be exactly equal, but probably a few ms greater - expect(lastReport).toBeGreaterThanOrEqual(now); - - expect(injector.$http).toHaveBeenCalledTimes(2); - // assert that it sent every cluster's telemetry - clusters.forEach(cluster => { - expect(injector.$http).toHaveBeenCalledWith({ - method: 'POST', - url: telemetryUrl, - data: cluster, - kbnXsrfToken: false, - }); - }); - - expect(injector.localStorage.set).toHaveBeenCalledTimes(1); - expect(injector.localStorage.set).toHaveBeenCalledWith(LOCALSTORAGE_KEY, { lastReport }); - }); - }); - - test('sends telemetry when requested and catches exceptions', () => { - const lastReport = Date.now() - REPORT_INTERVAL_MS - 1; - const injector = { - $http: jest.fn().mockRejectedValue(new Error('TEST - expected')), // caught failure - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport }), - set: jest.fn(), - }, - telemetryOptedIn: true, - telemetryUrl, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect.hasAssertions(); - - return telemetry._sendIfDue().then(result => { - expect(result).toBe(true); // attempted to send - expect(telemetry._sending).toBe(false); - - // should be unchanged - expect(telemetry._lastReport).toBe(lastReport); - expect(injector.localStorage.set).toHaveBeenCalledTimes(0); - - expect(injector.$http).toHaveBeenCalledTimes(2); - // assert that it sent every cluster's telemetry - clusters.forEach(cluster => { - expect(injector.$http).toHaveBeenCalledWith({ - method: 'POST', - url: telemetryUrl, - data: cluster, - kbnXsrfToken: false, - }); - }); - }); - }); - }); - - test('start', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce(undefined), - }, - telemetryOptedIn: false, // opted out - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - clearInterval(telemetry.start()); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts b/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts deleted file mode 100644 index 1930d65d5c09b..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts +++ /dev/null @@ -1,53 +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 { npStart } from 'ui/new_platform'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -import { isUnauthenticated } from '../services'; -// @ts-ignore -import { Telemetry } from './telemetry'; -// @ts-ignore -import { fetchTelemetry } from './fetch_telemetry'; -// @ts-ignore -import { isOptInHandleOldSettings } from './welcome_banner/handle_old_settings'; -import { TelemetryOptInProvider } from '../services'; - -function telemetryInit($injector: any) { - const $http = $injector.get('$http'); - const Private = $injector.get('Private'); - const config = $injector.get('config'); - const telemetryOptInProvider = Private(TelemetryOptInProvider); - - const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled'); - const telemetryOptedIn = isOptInHandleOldSettings(config, telemetryOptInProvider); - const sendUsageFrom = npStart.core.injectedMetadata.getInjectedVar('telemetrySendUsageFrom'); - - if (telemetryEnabled && telemetryOptedIn && sendUsageFrom === 'browser') { - // no telemetry for non-logged in users - if (isUnauthenticated()) { - return; - } - - const sender = new Telemetry($injector, () => fetchTelemetry($http)); - sender.start(); - } -} - -uiModules.get('telemetry/hacks').run(telemetryInit); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.js deleted file mode 100644 index 44971e2466794..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.js +++ /dev/null @@ -1,77 +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 React from 'react'; - -import { banners, toastNotifications } from 'ui/notify'; -import { EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -/** - * Handle clicks from the user on the opt-in banner. - * - * @param {Object} telemetryOptInProvider the telemetry opt-in provider - * @param {Boolean} optIn {@code true} to opt into telemetry. - * @param {Object} _banners Singleton banners. Can be overridden for tests. - * @param {Object} _toastNotifications Singleton toast notifications. Can be overridden for tests. - */ -export async function clickBanner( - telemetryOptInProvider, - optIn, - { _banners = banners, _toastNotifications = toastNotifications } = {} -) { - const bannerId = telemetryOptInProvider.getBannerId(); - let set = false; - - try { - set = await telemetryOptInProvider.setOptIn(optIn); - } catch (err) { - // set is already false - console.log('Unexpected error while trying to save setting.', err); - } - - if (set) { - _banners.remove(bannerId); - } else { - _toastNotifications.addDanger({ - title: ( - - ), - text: ( - -

- -

- - - -
- ), - }); - } -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js deleted file mode 100644 index 0caabe826ae57..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { mockInjectedMetadata } from '../../services/telemetry_opt_in.test.mocks'; - -import sinon from 'sinon'; -import { uiModules } from 'ui/modules'; - -uiModules - .get('kibana') - // disable stat reporting while running tests, - // MockInjector used in these tests is not impacted - .constant('telemetryOptedIn', null); - -import { clickBanner } from './click_banner'; -import { TelemetryOptInProvider } from '../../services/telemetry_opt_in'; - -const getMockInjector = ({ simulateFailure }) => { - const get = sinon.stub(); - - const mockHttp = { - post: sinon.stub(), - }; - - if (simulateFailure) { - mockHttp.post.returns(Promise.reject(new Error('something happened'))); - } else { - mockHttp.post.returns(Promise.resolve({})); - } - - get.withArgs('$http').returns(mockHttp); - - return { get }; -}; - -const getTelemetryOptInProvider = ({ simulateFailure = false, simulateError = false } = {}) => { - const injector = getMockInjector({ simulateFailure }); - const chrome = { - addBasePath: url => url, - }; - - const provider = new TelemetryOptInProvider(injector, chrome, false); - - if (simulateError) { - provider.setOptIn = () => Promise.reject('unhandled error'); - } - - return provider; -}; - -describe('click_banner', () => { - it('sets setting successfully and removes banner', async () => { - const banners = { - remove: sinon.spy(), - }; - - const optIn = true; - const bannerId = 'bruce-banner'; - mockInjectedMetadata({ telemetryOptedIn: optIn, allowChangingOptInStatus: true }); - const telemetryOptInProvider = getTelemetryOptInProvider(); - - telemetryOptInProvider.setBannerId(bannerId); - - await clickBanner(telemetryOptInProvider, optIn, { _banners: banners }); - - expect(telemetryOptInProvider.getOptIn()).toBe(optIn); - expect(banners.remove.calledOnce).toBe(true); - expect(banners.remove.calledWith(bannerId)).toBe(true); - }); - - it('sets setting unsuccessfully, adds toast, and does not touch banner', async () => { - const toastNotifications = { - addDanger: sinon.spy(), - }; - const banners = { - remove: sinon.spy(), - }; - const optIn = true; - mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true }); - const telemetryOptInProvider = getTelemetryOptInProvider({ simulateFailure: true }); - - await clickBanner(telemetryOptInProvider, optIn, { - _banners: banners, - _toastNotifications: toastNotifications, - }); - - expect(telemetryOptInProvider.getOptIn()).toBe(null); - expect(toastNotifications.addDanger.calledOnce).toBe(true); - expect(banners.remove.notCalled).toBe(true); - }); - - it('sets setting unsuccessfully with error, adds toast, and does not touch banner', async () => { - const toastNotifications = { - addDanger: sinon.spy(), - }; - const banners = { - remove: sinon.spy(), - }; - const optIn = false; - mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true }); - const telemetryOptInProvider = getTelemetryOptInProvider({ simulateError: true }); - - await clickBanner(telemetryOptInProvider, optIn, { - _banners: banners, - _toastNotifications: toastNotifications, - }); - - expect(telemetryOptInProvider.getOptIn()).toBe(null); - expect(toastNotifications.addDanger.calledOnce).toBe(true); - expect(banners.remove.notCalled).toBe(true); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js deleted file mode 100644 index c03fdb85c4d1c..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js +++ /dev/null @@ -1,85 +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 { CONFIG_TELEMETRY } from '../../../common/constants'; - -/** - * Clean up any old, deprecated settings and determine if we should continue. - * - * This will update the latest telemetry setting if necessary. - * - * @param {Object} config The advanced settings config object. - * @return {Boolean} {@code true} if the banner should still be displayed. {@code false} if the banner should not be displayed. - */ -const CONFIG_ALLOW_REPORT = 'xPackMonitoring:allowReport'; - -export async function handleOldSettings(config, telemetryOptInProvider) { - const CONFIG_SHOW_BANNER = 'xPackMonitoring:showBanner'; - const oldAllowReportSetting = config.get(CONFIG_ALLOW_REPORT, null); - const oldTelemetrySetting = config.get(CONFIG_TELEMETRY, null); - - let legacyOptInValue = null; - - if (typeof oldTelemetrySetting === 'boolean') { - legacyOptInValue = oldTelemetrySetting; - } else if (typeof oldAllowReportSetting === 'boolean') { - legacyOptInValue = oldAllowReportSetting; - } - - if (legacyOptInValue !== null) { - try { - await telemetryOptInProvider.setOptIn(legacyOptInValue); - - // delete old keys once we've successfully changed the setting (if it fails, we just wait until next time) - config.remove(CONFIG_ALLOW_REPORT); - config.remove(CONFIG_SHOW_BANNER); - config.remove(CONFIG_TELEMETRY); - } finally { - return false; - } - } - - const oldShowSetting = config.get(CONFIG_SHOW_BANNER, null); - - if (oldShowSetting !== null) { - config.remove(CONFIG_SHOW_BANNER); - } - - return true; -} - -export async function isOptInHandleOldSettings(config, telemetryOptInProvider) { - const currentOptInSettting = telemetryOptInProvider.getOptIn(); - - if (typeof currentOptInSettting === 'boolean') { - return currentOptInSettting; - } - - const oldTelemetrySetting = config.get(CONFIG_TELEMETRY, null); - if (typeof oldTelemetrySetting === 'boolean') { - return oldTelemetrySetting; - } - - const oldAllowReportSetting = config.get(CONFIG_ALLOW_REPORT, null); - if (typeof oldAllowReportSetting === 'boolean') { - return oldAllowReportSetting; - } - - return null; -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js deleted file mode 100644 index 8f05675565a5e..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js +++ /dev/null @@ -1,208 +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 { mockInjectedMetadata } from '../../services/telemetry_opt_in.test.mocks'; - -import sinon from 'sinon'; - -import { CONFIG_TELEMETRY } from '../../../common/constants'; -import { handleOldSettings } from './handle_old_settings'; -import { TelemetryOptInProvider } from '../../services/telemetry_opt_in'; - -const getTelemetryOptInProvider = (enabled, { simulateFailure = false } = {}) => { - const $http = { - post: async () => { - if (simulateFailure) { - return Promise.reject(new Error('something happened')); - } - return {}; - }, - }; - - const chrome = { - addBasePath: url => url, - }; - mockInjectedMetadata({ telemetryOptedIn: enabled, allowChangingOptInStatus: true }); - - const $injector = { - get: key => { - if (key === '$http') { - return $http; - } - throw new Error(`unexpected mock injector usage for ${key}`); - }, - }; - - return new TelemetryOptInProvider($injector, chrome, false); -}; - -describe('handle_old_settings', () => { - it('re-uses old "allowReport" setting and stays opted in', async () => { - const config = { - get: sinon.stub(), - remove: sinon.spy(), - set: sinon.stub(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null); - expect(telemetryOptInProvider.getOptIn()).toBe(null); - - config.get.withArgs('xPackMonitoring:allowReport', null).returns(true); - config.set.withArgs(CONFIG_TELEMETRY, true).returns(Promise.resolve(true)); - - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false); - - expect(config.get.calledTwice).toBe(true); - expect(config.set.called).toBe(false); - - expect(config.remove.calledThrice).toBe(true); - expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:allowReport'); - expect(config.remove.getCall(1).args[0]).toBe('xPackMonitoring:showBanner'); - expect(config.remove.getCall(2).args[0]).toBe(CONFIG_TELEMETRY); - - expect(telemetryOptInProvider.getOptIn()).toBe(true); - }); - - it('re-uses old "telemetry:optIn" setting and stays opted in', async () => { - const config = { - get: sinon.stub(), - remove: sinon.spy(), - set: sinon.stub(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null); - expect(telemetryOptInProvider.getOptIn()).toBe(null); - - config.get.withArgs('xPackMonitoring:allowReport', null).returns(false); - config.get.withArgs(CONFIG_TELEMETRY, null).returns(true); - - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false); - - expect(config.get.calledTwice).toBe(true); - expect(config.set.called).toBe(false); - - expect(config.remove.calledThrice).toBe(true); - expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:allowReport'); - expect(config.remove.getCall(1).args[0]).toBe('xPackMonitoring:showBanner'); - expect(config.remove.getCall(2).args[0]).toBe(CONFIG_TELEMETRY); - - expect(telemetryOptInProvider.getOptIn()).toBe(true); - }); - - it('re-uses old "allowReport" setting and stays opted out', async () => { - const config = { - get: sinon.stub(), - remove: sinon.spy(), - set: sinon.stub(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null); - expect(telemetryOptInProvider.getOptIn()).toBe(null); - - config.get.withArgs('xPackMonitoring:allowReport', null).returns(false); - config.set.withArgs(CONFIG_TELEMETRY, false).returns(Promise.resolve(true)); - - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false); - - expect(config.get.calledTwice).toBe(true); - expect(config.set.called).toBe(false); - expect(config.remove.calledThrice).toBe(true); - expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:allowReport'); - expect(config.remove.getCall(1).args[0]).toBe('xPackMonitoring:showBanner'); - expect(config.remove.getCall(2).args[0]).toBe(CONFIG_TELEMETRY); - - expect(telemetryOptInProvider.getOptIn()).toBe(false); - }); - - it('re-uses old "telemetry:optIn" setting and stays opted out', async () => { - const config = { - get: sinon.stub(), - remove: sinon.spy(), - set: sinon.stub(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null); - - config.get.withArgs(CONFIG_TELEMETRY, null).returns(false); - config.get.withArgs('xPackMonitoring:allowReport', null).returns(true); - - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false); - - expect(config.get.calledTwice).toBe(true); - expect(config.set.called).toBe(false); - expect(config.remove.calledThrice).toBe(true); - expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:allowReport'); - expect(config.remove.getCall(1).args[0]).toBe('xPackMonitoring:showBanner'); - expect(config.remove.getCall(2).args[0]).toBe(CONFIG_TELEMETRY); - - expect(telemetryOptInProvider.getOptIn()).toBe(false); - }); - - it('acknowledges users old setting even if re-setting fails', async () => { - const config = { - get: sinon.stub(), - set: sinon.stub(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null, { simulateFailure: true }); - - config.get.withArgs('xPackMonitoring:allowReport', null).returns(false); - //todo: make the new version of this fail! - config.set.withArgs(CONFIG_TELEMETRY, false).returns(Promise.resolve(false)); - - // note: because it doesn't remove the old settings _and_ returns false, there's no risk of suddenly being opted in - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false); - - expect(config.get.calledTwice).toBe(true); - expect(config.set.called).toBe(false); - }); - - it('removes show banner setting and presents user with choice', async () => { - const config = { - get: sinon.stub(), - remove: sinon.spy(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null); - - config.get.withArgs('xPackMonitoring:allowReport', null).returns(null); - config.get.withArgs('xPackMonitoring:showBanner', null).returns(false); - - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(true); - - expect(config.get.calledThrice).toBe(true); - expect(config.remove.calledOnce).toBe(true); - expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:showBanner'); - }); - - it('is effectively ignored on fresh installs', async () => { - const config = { - get: sinon.stub(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null); - - config.get.withArgs('xPackMonitoring:allowReport', null).returns(null); - config.get.withArgs('xPackMonitoring:showBanner', null).returns(null); - - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(true); - - expect(config.get.calledThrice).toBe(true); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/inject_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/inject_banner.js deleted file mode 100644 index c4c5c3e9e0aa2..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/inject_banner.js +++ /dev/null @@ -1,76 +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 chrome from 'ui/chrome'; - -import { fetchTelemetry } from '../fetch_telemetry'; -import { renderBanner } from './render_banner'; -import { renderOptedInBanner } from './render_notice_banner'; -import { shouldShowBanner } from './should_show_banner'; -import { shouldShowOptInBanner } from './should_show_opt_in_banner'; -import { TelemetryOptInProvider, isUnauthenticated } from '../../services'; -import { npStart } from 'ui/new_platform'; - -/** - * Add the Telemetry opt-in banner if the user has not already made a decision. - * - * Note: this is an async function, but Angular fails to use it as one. Its usage does not need to be awaited, - * and thus it can be wrapped in the run method to just be a normal, non-async function. - * - * @param {Object} $injector The Angular injector - */ -async function asyncInjectBanner($injector) { - const Private = $injector.get('Private'); - const telemetryOptInProvider = Private(TelemetryOptInProvider); - const config = $injector.get('config'); - - // and no banner for non-logged in users - if (isUnauthenticated()) { - return; - } - - // and no banner on status page - if (chrome.getApp().id === 'status_page') { - return; - } - - const $http = $injector.get('$http'); - - // determine if the banner should be displayed - if (await shouldShowBanner(telemetryOptInProvider, config)) { - renderBanner(telemetryOptInProvider, () => fetchTelemetry($http, { unencrypted: true })); - } - - if (await shouldShowOptInBanner(telemetryOptInProvider, config)) { - renderOptedInBanner(telemetryOptInProvider, () => fetchTelemetry($http, { unencrypted: true })); - } -} - -/** - * Add the Telemetry opt-in banner when appropriate. - * - * @param {Object} $injector The Angular injector - */ -export function injectBanner($injector) { - const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled'); - const telemetryBanner = npStart.core.injectedMetadata.getInjectedVar('telemetryBanner'); - if (telemetryEnabled && telemetryBanner) { - asyncInjectBanner($injector); - } -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.js deleted file mode 100644 index 70b5030866620..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.js +++ /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 React from 'react'; - -import { banners } from 'ui/notify'; - -import { clickBanner } from './click_banner'; -import { OptInBanner } from '../../components/opt_in_banner_component'; - -/** - * Render the Telemetry Opt-in banner. - * - * @param {Object} telemetryOptInProvider The telemetry opt-in provider. - * @param {Function} fetchTelemetry Function to pull telemetry on demand. - * @param {Object} _banners Banners singleton, which can be overridden for tests. - */ -export function renderBanner(telemetryOptInProvider, fetchTelemetry, { _banners = banners } = {}) { - const bannerId = _banners.add({ - component: ( - clickBanner(telemetryOptInProvider, optIn)} - fetchTelemetry={fetchTelemetry} - /> - ), - priority: 10000, - }); - - telemetryOptInProvider.setBannerId(bannerId); -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.js deleted file mode 100644 index 2aa53db11c1d9..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.js +++ /dev/null @@ -1,38 +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 React from 'react'; - -import { banners } from 'ui/notify'; -import { OptedInBanner } from '../../components/opted_in_notice_banner'; - -/** - * Render the Telemetry Opt-in notice banner. - * - * @param {Object} telemetryOptInProvider The telemetry opt-in provider. - * @param {Object} _banners Banners singleton, which can be overridden for tests. - */ -export function renderOptedInBanner(telemetryOptInProvider, { _banners = banners } = {}) { - const bannerId = _banners.add({ - component: , - priority: 10000, - }); - - telemetryOptInProvider.setOptInBannerNoticeId(bannerId); -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js deleted file mode 100644 index ee55f6cc76266..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.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 { handleOldSettings } from './handle_old_settings'; - -/** - * Determine if the banner should be displayed. - * - * This method can have side-effects related to deprecated config settings. - * - * @param {Object} config The advanced settings config object. - * @param {Object} _handleOldSettings handleOldSettings function, but overridable for tests. - * @return {Boolean} {@code true} if the banner should be displayed. {@code false} otherwise. - */ -export async function shouldShowBanner( - telemetryOptInProvider, - config, - { _handleOldSettings = handleOldSettings } = {} -) { - return ( - telemetryOptInProvider.getOptIn() === null && - (await _handleOldSettings(config, telemetryOptInProvider)) - ); -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js deleted file mode 100644 index 9578d462bc85c..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js +++ /dev/null @@ -1,91 +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 { mockInjectedMetadata } from '../../services/telemetry_opt_in.test.mocks'; - -import sinon from 'sinon'; - -import { CONFIG_TELEMETRY } from '../../../common/constants'; -import { shouldShowBanner } from './should_show_banner'; -import { TelemetryOptInProvider } from '../../services'; - -const getMockInjector = () => { - const get = sinon.stub(); - - const mockHttp = { - post: sinon.stub(), - }; - - get.withArgs('$http').returns(mockHttp); - - return { get }; -}; - -const getTelemetryOptInProvider = ({ telemetryOptedIn = null } = {}) => { - mockInjectedMetadata({ telemetryOptedIn, allowChangingOptInStatus: true }); - const injector = getMockInjector(); - const chrome = { - addBasePath: url => url, - }; - - return new TelemetryOptInProvider(injector, chrome); -}; - -describe('should_show_banner', () => { - it('returns whatever handleOldSettings does when telemetry opt-in setting is unset', async () => { - const config = { get: sinon.stub() }; - const telemetryOptInProvider = getTelemetryOptInProvider(); - const handleOldSettingsTrue = sinon.stub(); - const handleOldSettingsFalse = sinon.stub(); - - config.get.withArgs(CONFIG_TELEMETRY, null).returns(null); - handleOldSettingsTrue.returns(Promise.resolve(true)); - handleOldSettingsFalse.returns(Promise.resolve(false)); - - const showBannerTrue = await shouldShowBanner(telemetryOptInProvider, config, { - _handleOldSettings: handleOldSettingsTrue, - }); - const showBannerFalse = await shouldShowBanner(telemetryOptInProvider, config, { - _handleOldSettings: handleOldSettingsFalse, - }); - - expect(showBannerTrue).toBe(true); - expect(showBannerFalse).toBe(false); - - expect(config.get.callCount).toBe(0); - expect(handleOldSettingsTrue.calledOnce).toBe(true); - expect(handleOldSettingsFalse.calledOnce).toBe(true); - }); - - it('returns false if telemetry opt-in setting is set to true', async () => { - const config = { get: sinon.stub() }; - - const telemetryOptInProvider = getTelemetryOptInProvider({ telemetryOptedIn: true }); - - expect(await shouldShowBanner(telemetryOptInProvider, config)).toBe(false); - }); - - it('returns false if telemetry opt-in setting is set to false', async () => { - const config = { get: sinon.stub() }; - - const telemetryOptInProvider = getTelemetryOptInProvider({ telemetryOptedIn: false }); - - expect(await shouldShowBanner(telemetryOptInProvider, config)).toBe(false); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js deleted file mode 100644 index 494ed24bcc1cb..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js +++ /dev/null @@ -1,148 +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 { mockInjectedMetadata } from './telemetry_opt_in.test.mocks'; -import { TelemetryOptInProvider } from './telemetry_opt_in'; - -describe('TelemetryOptInProvider', () => { - const setup = ({ optedIn, simulatePostError, simulatePutError }) => { - const mockHttp = { - post: jest.fn(async () => { - if (simulatePostError) { - return Promise.reject('Something happened'); - } - }), - put: jest.fn(async () => { - if (simulatePutError) { - return Promise.reject('Something happened'); - } - }), - }; - - const mockChrome = { - addBasePath: url => url, - }; - - mockInjectedMetadata({ - telemetryOptedIn: optedIn, - allowChangingOptInStatus: true, - telemetryNotifyUserAboutOptInDefault: true, - }); - - const mockInjector = { - get: key => { - switch (key) { - case '$http': { - return mockHttp; - } - default: - throw new Error('unexpected injector request: ' + key); - } - }, - }; - - const provider = new TelemetryOptInProvider(mockInjector, mockChrome, false); - return { - provider, - mockHttp, - }; - }; - - it('should return the current opt-in status', () => { - const { provider: optedInProvider } = setup({ optedIn: true }); - expect(optedInProvider.getOptIn()).toEqual(true); - - const { provider: optedOutProvider } = setup({ optedIn: false }); - expect(optedOutProvider.getOptIn()).toEqual(false); - }); - - it('should allow an opt-out to take place', async () => { - const { provider, mockHttp } = setup({ optedIn: true }); - await provider.setOptIn(false); - - expect(mockHttp.post).toHaveBeenCalledWith(`/api/telemetry/v2/optIn`, { enabled: false }); - - expect(provider.getOptIn()).toEqual(false); - }); - - it('should allow an opt-in to take place', async () => { - const { provider, mockHttp } = setup({ optedIn: false }); - await provider.setOptIn(true); - - expect(mockHttp.post).toHaveBeenCalledWith(`/api/telemetry/v2/optIn`, { enabled: true }); - - expect(provider.getOptIn()).toEqual(true); - }); - - it('should gracefully handle errors', async () => { - const { provider, mockHttp } = setup({ optedIn: false, simulatePostError: true }); - await provider.setOptIn(true); - - expect(mockHttp.post).toHaveBeenCalledWith(`/api/telemetry/v2/optIn`, { enabled: true }); - - // opt-in change should not be reflected - expect(provider.getOptIn()).toEqual(false); - }); - - it('should return the current bannerId', () => { - const { provider } = setup({}); - const bannerId = 'bruce-banner'; - provider.setBannerId(bannerId); - expect(provider.getBannerId()).toEqual(bannerId); - }); - - describe('Notice Banner', () => { - it('should return the current bannerId', () => { - const { provider } = setup({}); - const bannerId = 'bruce-wayne'; - provider.setOptInBannerNoticeId(bannerId); - - expect(provider.getOptInBannerNoticeId()).toEqual(bannerId); - expect(provider.getBannerId()).not.toEqual(bannerId); - }); - - it('should persist that a user has seen the notice', async () => { - const { provider, mockHttp } = setup({}); - await provider.setOptInNoticeSeen(); - - expect(mockHttp.put).toHaveBeenCalledWith(`/api/telemetry/v2/userHasSeenNotice`); - - expect(provider.notifyUserAboutOptInDefault()).toEqual(false); - }); - - it('should only call the API once', async () => { - const { provider, mockHttp } = setup({}); - await provider.setOptInNoticeSeen(); - await provider.setOptInNoticeSeen(); - - expect(mockHttp.put).toHaveBeenCalledTimes(1); - - expect(provider.notifyUserAboutOptInDefault()).toEqual(false); - }); - - it('should gracefully handle errors', async () => { - const { provider } = setup({ simulatePutError: true }); - - await provider.setOptInNoticeSeen(); - - // opt-in change should not be reflected - expect(provider.notifyUserAboutOptInDefault()).toEqual(true); - }); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js deleted file mode 100644 index 4543266be46df..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js +++ /dev/null @@ -1,60 +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 { - injectedMetadataServiceMock, - notificationServiceMock, - overlayServiceMock, -} from '../../../../../core/public/mocks'; -const injectedMetadataMock = injectedMetadataServiceMock.createStartContract(); - -export function mockInjectedMetadata({ - telemetryOptedIn, - allowChangingOptInStatus, - telemetryNotifyUserAboutOptInDefault, -}) { - const mockGetInjectedVar = jest.fn().mockImplementation(key => { - switch (key) { - case 'telemetryOptedIn': - return telemetryOptedIn; - case 'allowChangingOptInStatus': - return allowChangingOptInStatus; - case 'telemetryNotifyUserAboutOptInDefault': - return telemetryNotifyUserAboutOptInDefault; - default: - throw new Error(`unexpected injectedVar ${key}`); - } - }); - - injectedMetadataMock.getInjectedVar = mockGetInjectedVar; -} - -jest.doMock('ui/new_platform', () => ({ - npSetup: { - core: { - notifications: notificationServiceMock.createSetupContract(), - }, - }, - npStart: { - core: { - injectedMetadata: injectedMetadataMock, - overlays: overlayServiceMock.createStartContract(), - }, - }, -})); diff --git a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts deleted file mode 100644 index af908bea7f4b1..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts +++ /dev/null @@ -1,154 +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 moment from 'moment'; -// @ts-ignore -import { banners, toastNotifications } from 'ui/notify'; -import { npStart } from 'ui/new_platform'; -import { i18n } from '@kbn/i18n'; - -let bannerId: string | null = null; -let optInBannerNoticeId: string | null = null; -let currentOptInStatus = false; -let telemetryNotifyUserAboutOptInDefault = true; - -async function sendOptInStatus($injector: any, chrome: any, enabled: boolean) { - const telemetryOptInStatusUrl = npStart.core.injectedMetadata.getInjectedVar( - 'telemetryOptInStatusUrl' - ) as string; - const $http = $injector.get('$http'); - - try { - const optInStatus = await $http.post( - chrome.addBasePath('/api/telemetry/v2/clusters/_opt_in_stats'), - { - enabled, - unencrypted: false, - } - ); - - if (optInStatus.data && optInStatus.data.length) { - return await fetch(telemetryOptInStatusUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(optInStatus.data), - }); - } - } catch (err) { - // Sending the ping is best-effort. Telemetry tries to send the ping once and discards it immediately if sending fails. - // swallow any errors - } -} -export function TelemetryOptInProvider($injector: any, chrome: any, sendOptInStatusChange = true) { - currentOptInStatus = npStart.core.injectedMetadata.getInjectedVar('telemetryOptedIn') as boolean; - - const allowChangingOptInStatus = npStart.core.injectedMetadata.getInjectedVar( - 'allowChangingOptInStatus' - ) as boolean; - - telemetryNotifyUserAboutOptInDefault = npStart.core.injectedMetadata.getInjectedVar( - 'telemetryNotifyUserAboutOptInDefault' - ) as boolean; - - const provider = { - getBannerId: () => bannerId, - getOptInBannerNoticeId: () => optInBannerNoticeId, - getOptIn: () => currentOptInStatus, - canChangeOptInStatus: () => allowChangingOptInStatus, - notifyUserAboutOptInDefault: () => telemetryNotifyUserAboutOptInDefault, - setBannerId(id: string) { - bannerId = id; - }, - setOptInBannerNoticeId(id: string) { - optInBannerNoticeId = id; - }, - setOptInNoticeSeen: async () => { - const $http = $injector.get('$http'); - - // If they've seen the notice don't spam the API - if (!telemetryNotifyUserAboutOptInDefault) { - return telemetryNotifyUserAboutOptInDefault; - } - - if (optInBannerNoticeId) { - banners.remove(optInBannerNoticeId); - } - - try { - await $http.put(chrome.addBasePath('/api/telemetry/v2/userHasSeenNotice')); - telemetryNotifyUserAboutOptInDefault = false; - } catch (error) { - toastNotifications.addError(error, { - title: i18n.translate('telemetry.optInNoticeSeenErrorTitle', { - defaultMessage: 'Error', - }), - toastMessage: i18n.translate('telemetry.optInNoticeSeenErrorToastText', { - defaultMessage: 'An error occurred dismissing the notice', - }), - }); - telemetryNotifyUserAboutOptInDefault = true; - } - - return telemetryNotifyUserAboutOptInDefault; - }, - setOptIn: async (enabled: boolean) => { - if (!allowChangingOptInStatus) { - return; - } - const $http = $injector.get('$http'); - - try { - await $http.post(chrome.addBasePath('/api/telemetry/v2/optIn'), { enabled }); - if (sendOptInStatusChange) { - await sendOptInStatus($injector, chrome, enabled); - } - currentOptInStatus = enabled; - } catch (error) { - toastNotifications.addError(error, { - title: i18n.translate('telemetry.optInErrorToastTitle', { - defaultMessage: 'Error', - }), - toastMessage: i18n.translate('telemetry.optInErrorToastText', { - defaultMessage: - 'An error occurred while trying to set the usage statistics preference.', - }), - }); - return false; - } - - return true; - }, - fetchExample: async () => { - const $http = $injector.get('$http'); - return $http.post(chrome.addBasePath(`/api/telemetry/v2/clusters/_stats`), { - unencrypted: true, - timeRange: { - min: moment() - .subtract(20, 'minutes') - .toISOString(), - max: moment().toISOString(), - }, - }); - }, - }; - - return provider; -} diff --git a/src/legacy/core_plugins/telemetry/public/views/management/index.js b/src/legacy/core_plugins/telemetry/public/views/management/index.ts similarity index 100% rename from src/legacy/core_plugins/telemetry/public/views/management/index.js rename to src/legacy/core_plugins/telemetry/public/views/management/index.ts diff --git a/src/legacy/core_plugins/telemetry/public/views/management/management.js b/src/legacy/core_plugins/telemetry/public/views/management/management.tsx similarity index 52% rename from src/legacy/core_plugins/telemetry/public/views/management/management.js rename to src/legacy/core_plugins/telemetry/public/views/management/management.tsx index 7032775e391bb..c8ae410e0aa57 100644 --- a/src/legacy/core_plugins/telemetry/public/views/management/management.js +++ b/src/legacy/core_plugins/telemetry/public/views/management/management.tsx @@ -18,30 +18,32 @@ */ import React from 'react'; import routes from 'ui/routes'; - -import { npSetup } from 'ui/new_platform'; -import { TelemetryOptInProvider } from '../../services'; -import { TelemetryForm } from '../../components'; +import { npStart, npSetup } from 'ui/new_platform'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { TelemetryManagementSection } from '../../../../../../plugins/telemetry/public/components'; routes.defaults(/\/management/, { resolve: { - telemetryManagementSection: function(Private) { - const telemetryOptInProvider = Private(TelemetryOptInProvider); - const componentRegistry = npSetup.plugins.advancedSettings.component; + telemetryManagementSection() { + const { telemetry } = npStart.plugins as any; + const { advancedSettings } = npSetup.plugins as any; - const Component = props => ( - - ); + if (telemetry && advancedSettings) { + const componentRegistry = advancedSettings.component; + const Component = (props: any) => ( + + ); - componentRegistry.register( - componentRegistry.componentType.PAGE_FOOTER_COMPONENT, - Component, - true - ); + componentRegistry.register( + componentRegistry.componentType.PAGE_FOOTER_COMPONENT, + Component, + true + ); + } }, }, }); diff --git a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts index 99090cb2fb7ef..6919b6959aa8c 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts @@ -24,7 +24,6 @@ import { dirname, join } from 'path'; // look for telemetry.yml in the same places we expect kibana.yml import { ensureDeepObject } from './ensure_deep_object'; -import { getXpackConfigWithDeprecated } from '../../../common/get_xpack_config_with_deprecated'; import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; /** @@ -85,7 +84,7 @@ export function createTelemetryUsageCollector( isReady: () => true, fetch: async () => { const config = server.config(); - const configPath = getXpackConfigWithDeprecated(config, 'telemetry.config') as string; + const configPath = config.get('telemetry.config') as string; const telemetryPath = join(dirname(configPath), 'telemetry.yml'); return await readTelemetryFile(telemetryPath); }, diff --git a/src/legacy/core_plugins/telemetry/server/fetcher.ts b/src/legacy/core_plugins/telemetry/server/fetcher.ts index 9edd8457f2b89..6e16328c4abd8 100644 --- a/src/legacy/core_plugins/telemetry/server/fetcher.ts +++ b/src/legacy/core_plugins/telemetry/server/fetcher.ts @@ -24,7 +24,6 @@ import { telemetryCollectionManager } from './collection_manager'; import { getTelemetryOptIn, getTelemetrySendUsageFrom } from './telemetry_config'; import { getTelemetrySavedObject, updateTelemetrySavedObject } from './telemetry_repository'; import { REPORT_INTERVAL_MS } from '../common/constants'; -import { getXpackConfigWithDeprecated } from '../common/get_xpack_config_with_deprecated'; export class FetcherTask { private readonly checkDurationMs = 60 * 1000 * 5; @@ -52,7 +51,7 @@ export class FetcherTask { const configTelemetrySendUsageFrom = config.get('telemetry.sendUsageFrom'); const allowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus'); const configTelemetryOptIn = config.get('telemetry.optIn'); - const telemetryUrl = getXpackConfigWithDeprecated(config, 'telemetry.url') as string; + const telemetryUrl = config.get('telemetry.url') as string; return { telemetryOptIn: getTelemetryOptIn({ diff --git a/src/legacy/core_plugins/telemetry/server/handle_old_settings/handle_old_settings.ts b/src/legacy/core_plugins/telemetry/server/handle_old_settings/handle_old_settings.ts new file mode 100644 index 0000000000000..b28a01bffa44d --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/handle_old_settings/handle_old_settings.ts @@ -0,0 +1,59 @@ +/* + * 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. + */ + +/** + * Clean up any old, deprecated settings and determine if we should continue. + * + * This will update the latest telemetry setting if necessary. + * + * @param {Object} config The advanced settings config object. + * @return {Boolean} {@code true} if the banner should still be displayed. {@code false} if the banner should not be displayed. + */ + +import { Server } from 'hapi'; +import { CONFIG_TELEMETRY } from '../../common/constants'; +import { updateTelemetrySavedObject } from '../telemetry_repository'; + +const CONFIG_ALLOW_REPORT = 'xPackMonitoring:allowReport'; + +export async function handleOldSettings(server: Server) { + const { getSavedObjectsRepository } = server.savedObjects; + const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); + const savedObjectsClient = getSavedObjectsRepository(callWithInternalUser); + const uiSettings = server.uiSettingsServiceFactory({ savedObjectsClient }); + + const oldTelemetrySetting = await uiSettings.get(CONFIG_TELEMETRY); + const oldAllowReportSetting = await uiSettings.get(CONFIG_ALLOW_REPORT); + let legacyOptInValue = null; + + if (typeof oldTelemetrySetting === 'boolean') { + legacyOptInValue = oldTelemetrySetting; + } else if ( + typeof oldAllowReportSetting === 'boolean' && + uiSettings.isOverridden(CONFIG_ALLOW_REPORT) + ) { + legacyOptInValue = oldAllowReportSetting; + } + + if (legacyOptInValue !== null) { + await updateTelemetrySavedObject(savedObjectsClient, { + enabled: legacyOptInValue, + }); + } +} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/index.js b/src/legacy/core_plugins/telemetry/server/handle_old_settings/index.ts similarity index 93% rename from src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/index.js rename to src/legacy/core_plugins/telemetry/server/handle_old_settings/index.ts index ffb0e88c60a0d..77eae0d80db61 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/index.js +++ b/src/legacy/core_plugins/telemetry/server/handle_old_settings/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { injectBanner } from './inject_banner'; +export { handleOldSettings } from './handle_old_settings'; diff --git a/src/legacy/core_plugins/telemetry/server/index.ts b/src/legacy/core_plugins/telemetry/server/index.ts index 6c62d03adf25c..85d7d80234ffc 100644 --- a/src/legacy/core_plugins/telemetry/server/index.ts +++ b/src/legacy/core_plugins/telemetry/server/index.ts @@ -23,6 +23,7 @@ import * as constants from '../common/constants'; export { FetcherTask } from './fetcher'; export { replaceTelemetryInjectedVars } from './telemetry_config'; +export { handleOldSettings } from './handle_old_settings'; export { telemetryCollectionManager } from './collection_manager'; export { PluginsSetup } from './plugin'; export const telemetryPlugin = (initializerContext: PluginInitializerContext) => diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index e300ce4a0caf8..ff8fc9b07879c 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -39,6 +39,7 @@ import { import { ManagementSetup, ManagementStart } from '../../../../plugins/management/public'; import { BfetchPublicSetup, BfetchPublicStart } from '../../../../plugins/bfetch/public'; import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public'; +import { TelemetryPluginSetup, TelemetryPluginStart } from '../../../../plugins/telemetry/public'; import { NavigationPublicPluginSetup, NavigationPublicPluginStart, @@ -60,6 +61,7 @@ export interface PluginsSetup { usageCollection: UsageCollectionSetup; advancedSettings: AdvancedSettingsSetup; management: ManagementSetup; + telemetry?: TelemetryPluginSetup; } export interface PluginsStart { @@ -77,6 +79,7 @@ export interface PluginsStart { share: SharePluginStart; management: ManagementStart; advancedSettings: AdvancedSettingsStart; + telemetry?: TelemetryPluginStart; } export const npSetup = { diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap index 2f2332bb06e3c..eebbc63f6f1e4 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap @@ -97,6 +97,17 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "management": Object {}, "navLinks": Object {}, }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "registerMountContext": [MockFunction], @@ -738,6 +749,17 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "management": Object {}, "navLinks": Object {}, }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "registerMountContext": [MockFunction], @@ -1361,6 +1383,17 @@ exports[`QueryStringInput Should pass the query language to the language switche "management": Object {}, "navLinks": Object {}, }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "registerMountContext": [MockFunction], @@ -1999,6 +2032,17 @@ exports[`QueryStringInput Should pass the query language to the language switche "management": Object {}, "navLinks": Object {}, }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "registerMountContext": [MockFunction], @@ -2622,6 +2666,17 @@ exports[`QueryStringInput Should render the given query 1`] = ` "management": Object {}, "navLinks": Object {}, }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "registerMountContext": [MockFunction], @@ -3260,6 +3315,17 @@ exports[`QueryStringInput Should render the given query 1`] = ` "management": Object {}, "navLinks": Object {}, }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "registerMountContext": [MockFunction], diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_opt_in_banner.js b/src/plugins/telemetry/common/constants.ts similarity index 61% rename from src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_opt_in_banner.js rename to src/plugins/telemetry/common/constants.ts index 45539c4eea46c..7b7694ed9aed7 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_opt_in_banner.js +++ b/src/plugins/telemetry/common/constants.ts @@ -18,13 +18,22 @@ */ /** - * Determine if the notice banner should be displayed. - * - * This method can have side-effects related to deprecated config settings. - * - * @param {Object} telemetryOptInProvider The Telemetry opt-in provider singleton. - * @return {Boolean} {@code true} if the banner should be displayed. {@code false} otherwise. + * The amount of time, in milliseconds, to wait between reports when enabled. + * Currently 24 hours. + */ +export const REPORT_INTERVAL_MS = 86400000; + +/* + * Key for the localStorage service + */ +export const LOCALSTORAGE_KEY = 'telemetry.data'; + +/** + * Link to Advanced Settings. + */ +export const PATH_TO_ADVANCED_SETTINGS = 'kibana#/management/kibana/settings'; + +/** + * Link to the Elastic Telemetry privacy statement. */ -export async function shouldShowOptInBanner(telemetryOptInProvider) { - return telemetryOptInProvider.notifyUserAboutOptInDefault(); -} +export const PRIVACY_STATEMENT_URL = `https://www.elastic.co/legal/privacy-statement`; diff --git a/src/plugins/telemetry/kibana.json b/src/plugins/telemetry/kibana.json new file mode 100644 index 0000000000000..3a28149276c3e --- /dev/null +++ b/src/plugins/telemetry/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "telemetry", + "version": "kibana", + "server": false, + "ui": true +} diff --git a/src/plugins/telemetry/public/components/__snapshots__/opt_in_banner.test.tsx.snap b/src/plugins/telemetry/public/components/__snapshots__/opt_in_banner.test.tsx.snap new file mode 100644 index 0000000000000..87e60869f6c21 --- /dev/null +++ b/src/plugins/telemetry/public/components/__snapshots__/opt_in_banner.test.tsx.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OptInDetailsComponent renders as expected 1`] = ` + + } +> + + + + + + + + + + + + + + + +`; diff --git a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_details_component.test.tsx.snap b/src/plugins/telemetry/public/components/__snapshots__/opt_in_example_flyout.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_details_component.test.tsx.snap rename to src/plugins/telemetry/public/components/__snapshots__/opt_in_example_flyout.test.tsx.snap diff --git a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap b/src/plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap similarity index 97% rename from src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap rename to src/plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap index c80485332fa8a..7fa69a7409c6a 100644 --- a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap +++ b/src/plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap @@ -9,6 +9,7 @@ exports[`OptInMessage renders as expected 1`] = ` Object { "privacyStatementLink": { + it('renders as expected', () => { + expect(shallowWithIntl( {}} />)).toMatchSnapshot(); + }); + + it('fires the "onChangeOptInClick" prop with true when a enable is clicked', () => { + const onClick = jest.fn(); + const component = shallowWithIntl(); + + const enableButton = component.findWhere(n => { + const props = n.props(); + return n.type() === EuiButton && props['data-test-subj'] === 'enable'; + }); + + if (!enableButton) { + throw new Error(`Couldn't find any opt in enable button.`); + } + + enableButton.simulate('click'); + expect(onClick).toHaveBeenCalled(); + expect(onClick).toBeCalledWith(true); + }); + + it('fires the "onChangeOptInClick" with false when a disable is clicked', () => { + const onClick = jest.fn(); + const component = shallowWithIntl(); + + const disableButton = component.findWhere(n => { + const props = n.props(); + return n.type() === EuiButton && props['data-test-subj'] === 'disable'; + }); + + if (!disableButton) { + throw new Error(`Couldn't find any opt in disable button.`); + } + + disableButton.simulate('click'); + expect(onClick).toHaveBeenCalled(); + expect(onClick).toBeCalledWith(false); + }); +}); diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_banner_component.tsx b/src/plugins/telemetry/public/components/opt_in_banner.tsx similarity index 84% rename from src/legacy/core_plugins/telemetry/public/components/opt_in_banner_component.tsx rename to src/plugins/telemetry/public/components/opt_in_banner.tsx index 2813af9c499e7..adf7b8bc84719 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opt_in_banner_component.tsx +++ b/src/plugins/telemetry/public/components/opt_in_banner.tsx @@ -23,15 +23,12 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { OptInMessage } from './opt_in_message'; interface Props { - fetchTelemetry: () => Promise; - optInClick: (optIn: boolean) => void; + onChangeOptInClick: (isOptIn: boolean) => void; } -/** - * React component for displaying the Telemetry opt-in banner. - */ export class OptInBanner extends React.PureComponent { render() { + const { onChangeOptInClick } = this.props; const title = ( { ); return ( - + - this.props.optInClick(true)}> + onChangeOptInClick(true)}> { - this.props.optInClick(false)}> + onChangeOptInClick(false)}> { it('renders as expected', () => { expect( shallowWithIntl( - ({ data: [] }))} - onClose={jest.fn()} - /> + [])} onClose={jest.fn()} /> ) ).toMatchSnapshot(); }); diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.tsx b/src/plugins/telemetry/public/components/opt_in_example_flyout.tsx similarity index 91% rename from src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.tsx rename to src/plugins/telemetry/public/components/opt_in_example_flyout.tsx index 12ab780e75990..9ecbd4df20560 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.tsx +++ b/src/plugins/telemetry/public/components/opt_in_example_flyout.tsx @@ -37,7 +37,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; interface Props { - fetchTelemetry: () => Promise; + fetchExample: () => Promise; onClose: () => void; } @@ -57,22 +57,21 @@ export class OptInExampleFlyout extends React.PureComponent { hasPrivilegeToRead: false, }; - componentDidMount() { - this.props - .fetchTelemetry() - .then(response => - this.setState({ - data: Array.isArray(response.data) ? response.data : null, - isLoading: false, - hasPrivilegeToRead: true, - }) - ) - .catch(err => { - this.setState({ - isLoading: false, - hasPrivilegeToRead: err.status !== 403, - }); + async componentDidMount() { + try { + const { fetchExample } = this.props; + const clusters = await fetchExample(); + this.setState({ + data: Array.isArray(clusters) ? clusters : null, + isLoading: false, + hasPrivilegeToRead: true, }); + } catch (err) { + this.setState({ + isLoading: false, + hasPrivilegeToRead: err.status !== 403, + }); + } } renderBody({ data, isLoading, hasPrivilegeToRead }: State) { diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.test.tsx b/src/plugins/telemetry/public/components/opt_in_message.test.tsx similarity index 89% rename from src/legacy/core_plugins/telemetry/public/components/opt_in_message.test.tsx rename to src/plugins/telemetry/public/components/opt_in_message.test.tsx index 1a9fabceda907..dbe0941345a02 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.test.tsx +++ b/src/plugins/telemetry/public/components/opt_in_message.test.tsx @@ -22,8 +22,6 @@ import { OptInMessage } from './opt_in_message'; describe('OptInMessage', () => { it('renders as expected', () => { - expect( - shallowWithIntl( [])} />) - ).toMatchSnapshot(); + expect(shallowWithIntl()).toMatchSnapshot(); }); }); diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.tsx b/src/plugins/telemetry/public/components/opt_in_message.tsx similarity index 81% rename from src/legacy/core_plugins/telemetry/public/components/opt_in_message.tsx rename to src/plugins/telemetry/public/components/opt_in_message.tsx index 4221d78516e10..590a115b2bb6c 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.tsx +++ b/src/plugins/telemetry/public/components/opt_in_message.tsx @@ -20,30 +20,9 @@ import * as React from 'react'; import { EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; - import { PRIVACY_STATEMENT_URL } from '../../common/constants'; -interface Props { - fetchTelemetry: () => Promise; -} - -interface State { - showDetails: boolean; - showExample: boolean; -} - -export class OptInMessage extends React.PureComponent { - public readonly state: State = { - showDetails: false, - showExample: false, - }; - - toggleShowExample = () => { - this.setState(prevState => ({ - showExample: !prevState.showExample, - })); - }; - +export class OptInMessage extends React.PureComponent { render() { return ( @@ -52,7 +31,7 @@ export class OptInMessage extends React.PureComponent { defaultMessage="Want to help us improve the Elastic Stack? Data usage collection is currently disabled. Enabling data usage collection helps us manage and improve our products and services. See our {privacyStatementLink} for more details." values={{ privacyStatementLink: ( - + { it('renders as expected', () => { - expect(shallowWithIntl( {}} />)).toMatchSnapshot(); + expect(shallowWithIntl( {}} />)).toMatchSnapshot(); }); it('fires the "onSeenBanner" prop when a link is clicked', () => { const onLinkClick = jest.fn(); - const component = shallowWithIntl(); + const component = shallowWithIntl(); const button = component.findWhere(n => n.type() === EuiButton); diff --git a/src/legacy/core_plugins/telemetry/public/components/opted_in_notice_banner.tsx b/src/plugins/telemetry/public/components/opted_in_notice_banner.tsx similarity index 75% rename from src/legacy/core_plugins/telemetry/public/components/opted_in_notice_banner.tsx rename to src/plugins/telemetry/public/components/opted_in_notice_banner.tsx index e37fa73ebe7b8..090893964c881 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opted_in_notice_banner.tsx +++ b/src/plugins/telemetry/public/components/opted_in_notice_banner.tsx @@ -20,35 +20,32 @@ /* eslint @elastic/eui/href-or-on-click:0 */ import * as React from 'react'; -import chrome from 'ui/chrome'; import { EuiButton, EuiLink, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { PATH_TO_ADVANCED_SETTINGS } from '../../common/constants'; +import { i18n } from '@kbn/i18n'; +import { PATH_TO_ADVANCED_SETTINGS, PRIVACY_STATEMENT_URL } from '../../common/constants'; interface Props { onSeenBanner: () => any; } -/** - * React component for displaying the Telemetry opt-in notice. - */ -export class OptedInBanner extends React.PureComponent { - onLinkClick = () => { - this.props.onSeenBanner(); - return; - }; - +export class OptedInNoticeBanner extends React.PureComponent { render() { + const { onSeenBanner } = this.props; + const bannerTitle = i18n.translate('telemetry.telemetryOptedInNoticeTitle', { + defaultMessage: 'Help us improve the Elastic Stack', + }); + return ( - + @@ -59,10 +56,7 @@ export class OptedInBanner extends React.PureComponent { ), disableLink: ( - + { }} /> - + void; + showAppliesSettingMessage: boolean; + enableSaving: boolean; + query?: any; +} + +interface State { + processing: boolean; + showExample: boolean; + queryMatches: boolean | null; +} - state = { +export class TelemetryManagementSection extends Component { + state: State = { processing: false, showExample: false, queryMatches: null, }; - UNSAFE_componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps: Props) { const { query } = nextProps; const searchTerm = (query.text || '').toLowerCase(); @@ -71,11 +78,10 @@ export class TelemetryForm extends Component { } render() { - const { telemetryOptInProvider } = this.props; - + const { telemetryService } = this.props; const { showExample, queryMatches } = this.state; - if (!telemetryOptInProvider.canChangeOptInStatus()) { + if (!telemetryService.getCanChangeOptInStatus()) { return null; } @@ -87,7 +93,7 @@ export class TelemetryForm extends Component { {showExample && ( telemetryOptInProvider.fetchExample()} + fetchExample={telemetryService.fetchExample} onClose={this.toggleExample} /> )} @@ -106,15 +112,23 @@ export class TelemetryForm extends Component { {this.maybeGetAppliesSettingMessage()} ); - toggleOptIn = async () => { - const newOptInValue = !this.props.telemetryOptInProvider.getOptIn(); + toggleOptIn = async (): Promise => { + const { telemetryService } = this.props; + const newOptInValue = !telemetryService.getIsOptedIn(); return new Promise((resolve, reject) => { - this.setState( - { - enabled: newOptInValue, - processing: true, - }, - () => { - this.props.telemetryOptInProvider.setOptIn(newOptInValue).then( - () => { - this.setState({ processing: false }); - resolve(); - }, - e => { - // something went wrong - this.setState({ processing: false }); - reject(e); - } - ); + this.setState({ processing: true }, async () => { + try { + await telemetryService.setOptIn(newOptInValue); + this.setState({ processing: false }); + resolve(true); + } catch (err) { + this.setState({ processing: false }); + reject(err); } - ); + }); }); }; diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_opt_in.js b/src/plugins/telemetry/public/index.ts similarity index 81% rename from src/legacy/core_plugins/telemetry/public/hacks/telemetry_opt_in.js rename to src/plugins/telemetry/public/index.ts index 4e53c7ecd7030..2f86d7749bb9b 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_opt_in.js +++ b/src/plugins/telemetry/public/index.ts @@ -17,8 +17,9 @@ * under the License. */ -import { uiModules } from 'ui/modules'; +import { TelemetryPlugin } from './plugin'; +export { TelemetryPluginStart, TelemetryPluginSetup } from './plugin'; -import { injectBanner } from './welcome_banner'; - -uiModules.get('telemetry/hacks').run(injectBanner); +export function plugin() { + return new TelemetryPlugin(); +} diff --git a/src/plugins/telemetry/public/mocks.ts b/src/plugins/telemetry/public/mocks.ts new file mode 100644 index 0000000000000..93dc13c327509 --- /dev/null +++ b/src/plugins/telemetry/public/mocks.ts @@ -0,0 +1,85 @@ +/* + * 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. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { overlayServiceMock } from '../../../core/public/overlays/overlay_service.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { httpServiceMock } from '../../../core/public/http/http_service.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { notificationServiceMock } from '../../../core/public/notifications/notifications_service.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { injectedMetadataServiceMock } from '../../../core/public/injected_metadata/injected_metadata_service.mock'; +import { TelemetryService } from './services/telemetry_service'; +import { TelemetryNotifications } from './services/telemetry_notifications/telemetry_notifications'; +import { TelemetryPluginStart } from './plugin'; + +export function mockTelemetryService({ + reportOptInStatusChange, +}: { reportOptInStatusChange?: boolean } = {}) { + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); + injectedMetadata.getInjectedVar.mockImplementation((key: string) => { + switch (key) { + case 'telemetryNotifyUserAboutOptInDefault': + return true; + case 'allowChangingOptInStatus': + return true; + case 'telemetryOptedIn': + return true; + default: { + throw Error(`Unhandled getInjectedVar key "${key}".`); + } + } + }); + + return new TelemetryService({ + injectedMetadata, + http: httpServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + reportOptInStatusChange, + }); +} + +export function mockTelemetryNotifications({ + telemetryService, +}: { + telemetryService: TelemetryService; +}) { + return new TelemetryNotifications({ + overlays: overlayServiceMock.createStartContract(), + telemetryService, + }); +} + +export type Setup = jest.Mocked; + +export const telemetryPluginMock = { + createSetupContract, +}; + +function createSetupContract(): Setup { + const telemetryService = mockTelemetryService(); + const telemetryNotifications = mockTelemetryNotifications({ telemetryService }); + + const setupContract: Setup = { + telemetryService, + telemetryNotifications, + }; + + return setupContract; +} diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts new file mode 100644 index 0000000000000..7ba51cacd1949 --- /dev/null +++ b/src/plugins/telemetry/public/plugin.ts @@ -0,0 +1,118 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Plugin, CoreStart, CoreSetup, HttpStart } from '../../../core/public'; + +import { TelemetrySender, TelemetryService, TelemetryNotifications } from './services'; + +export interface TelemetryPluginSetup { + telemetryService: TelemetryService; +} + +export interface TelemetryPluginStart { + telemetryService: TelemetryService; + telemetryNotifications: TelemetryNotifications; +} + +export class TelemetryPlugin implements Plugin { + private telemetrySender?: TelemetrySender; + private telemetryNotifications?: TelemetryNotifications; + private telemetryService?: TelemetryService; + + public setup({ http, injectedMetadata, notifications }: CoreSetup): TelemetryPluginSetup { + this.telemetryService = new TelemetryService({ + http, + injectedMetadata, + notifications, + }); + + this.telemetrySender = new TelemetrySender(this.telemetryService); + + return { + telemetryService: this.telemetryService, + }; + } + + public start({ injectedMetadata, http, overlays, application }: CoreStart): TelemetryPluginStart { + if (!this.telemetryService) { + throw Error('Telemetry plugin failed to initialize properly.'); + } + + const telemetryBanner = injectedMetadata.getInjectedVar('telemetryBanner') as boolean; + const sendUsageFrom = injectedMetadata.getInjectedVar('telemetrySendUsageFrom') as + | 'browser' + | 'server'; + + this.telemetryNotifications = new TelemetryNotifications({ + overlays, + telemetryService: this.telemetryService, + }); + + application.currentAppId$.subscribe(appId => { + const isUnauthenticated = this.getIsUnauthenticated(http); + if (isUnauthenticated) { + return; + } + + this.maybeStartTelemetryPoller({ sendUsageFrom }); + if (telemetryBanner) { + this.maybeShowOptedInNotificationBanner(); + this.maybeShowOptInBanner(); + } + }); + + return { + telemetryService: this.telemetryService, + telemetryNotifications: this.telemetryNotifications, + }; + } + + private getIsUnauthenticated(http: HttpStart) { + const { anonymousPaths } = http; + return anonymousPaths.isAnonymous(window.location.pathname); + } + + private maybeStartTelemetryPoller({ sendUsageFrom }: { sendUsageFrom: string }) { + if (!this.telemetrySender) { + return; + } + if (sendUsageFrom === 'browser') { + this.telemetrySender.startChecking(); + } + } + + private maybeShowOptedInNotificationBanner() { + if (!this.telemetryNotifications) { + return; + } + const shouldShowBanner = this.telemetryNotifications.shouldShowOptedInNoticeBanner(); + if (shouldShowBanner) { + this.telemetryNotifications.renderOptedInNoticeBanner(); + } + } + + private maybeShowOptInBanner() { + if (!this.telemetryNotifications) { + return; + } + const shouldShowBanner = this.telemetryNotifications.shouldShowOptInBanner(); + if (shouldShowBanner) { + this.telemetryNotifications.renderOptInBanner(); + } + } +} diff --git a/src/plugins/telemetry/public/services/index.ts b/src/plugins/telemetry/public/services/index.ts new file mode 100644 index 0000000000000..ff4404c626fe0 --- /dev/null +++ b/src/plugins/telemetry/public/services/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { TelemetrySender } from './telemetry_sender'; +export { TelemetryService } from './telemetry_service'; +export { TelemetryNotifications } from './telemetry_notifications'; diff --git a/src/legacy/core_plugins/telemetry/public/services/index.ts b/src/plugins/telemetry/public/services/telemetry_notifications/index.ts similarity index 88% rename from src/legacy/core_plugins/telemetry/public/services/index.ts rename to src/plugins/telemetry/public/services/telemetry_notifications/index.ts index 8b02f8ce4c5b0..c6ba2cce1edb0 100644 --- a/src/legacy/core_plugins/telemetry/public/services/index.ts +++ b/src/plugins/telemetry/public/services/telemetry_notifications/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export { TelemetryOptInProvider } from './telemetry_opt_in'; -export { isUnauthenticated } from './path'; +export { TelemetryNotifications } from './telemetry_notifications'; diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.test.js b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.test.ts similarity index 56% rename from src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.test.js rename to src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.test.ts index f40e0b188c198..020d8023b6003 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.test.js +++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.test.ts @@ -17,24 +17,27 @@ * under the License. */ -import '../../services/telemetry_opt_in.test.mocks'; -import { renderOptedInBanner } from './render_notice_banner'; +import { renderOptInBanner } from './render_opt_in_banner'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { overlayServiceMock } from '../../../../../core/public/overlays/overlay_service.mock'; -describe('render_notice_banner', () => { +describe('renderOptInBanner', () => { it('adds a banner to banners with priority of 10000', () => { const bannerID = 'brucer-wayne'; + const overlays = overlayServiceMock.createStartContract(); + overlays.banners.add.mockReturnValue(bannerID); - const telemetryOptInProvider = { setOptInBannerNoticeId: jest.fn() }; - const banners = { add: jest.fn().mockReturnValue(bannerID) }; + const returnedBannerId = renderOptInBanner({ + setOptIn: jest.fn(), + overlays, + }); - renderOptedInBanner(telemetryOptInProvider, { _banners: banners }); + expect(overlays.banners.add).toBeCalledTimes(1); - expect(banners.add).toBeCalledTimes(1); - expect(telemetryOptInProvider.setOptInBannerNoticeId).toBeCalledWith(bannerID); + expect(returnedBannerId).toBe(bannerID); + const bannerConfig = overlays.banners.add.mock.calls[0]; - const bannerConfig = banners.add.mock.calls[0][0]; - - expect(bannerConfig.component).not.toBe(undefined); - expect(bannerConfig.priority).toBe(10000); + expect(bannerConfig[0]).not.toBe(undefined); + expect(bannerConfig[1]).toBe(10000); }); }); diff --git a/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx new file mode 100644 index 0000000000000..6e0164df6403a --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { CoreStart } from 'kibana/public'; +import { OptInBanner } from '../../components/opt_in_banner'; +import { toMountPoint } from '../../../../kibana_react/public'; + +interface RenderBannerConfig { + overlays: CoreStart['overlays']; + setOptIn: (isOptIn: boolean) => Promise; +} + +export function renderOptInBanner({ setOptIn, overlays }: RenderBannerConfig) { + const mount = toMountPoint(); + const bannerId = overlays.banners.add(mount, 10000); + + return bannerId; +} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js b/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.test.ts similarity index 52% rename from src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js rename to src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.test.ts index b4a86b36d922f..2d175024a74fb 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js +++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.test.ts @@ -17,26 +17,27 @@ * under the License. */ -import '../../services/telemetry_opt_in.test.mocks'; -import { renderBanner } from './render_banner'; +import { renderOptedInNoticeBanner } from './render_opted_in_notice_banner'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { overlayServiceMock } from '../../../../../core/public/overlays/overlay_service.mock'; -describe('render_banner', () => { +describe('renderOptedInNoticeBanner', () => { it('adds a banner to banners with priority of 10000', () => { - const bannerID = 'brucer-banner'; + const bannerID = 'brucer-wayne'; + const overlays = overlayServiceMock.createStartContract(); + overlays.banners.add.mockReturnValue(bannerID); - const telemetryOptInProvider = { setBannerId: jest.fn() }; - const banners = { add: jest.fn().mockReturnValue(bannerID) }; - const fetchTelemetry = jest.fn(); + const returnedBannerId = renderOptedInNoticeBanner({ + onSeen: jest.fn(), + overlays, + }); - renderBanner(telemetryOptInProvider, fetchTelemetry, { _banners: banners }); + expect(overlays.banners.add).toBeCalledTimes(1); - expect(banners.add).toBeCalledTimes(1); - expect(fetchTelemetry).toBeCalledTimes(0); - expect(telemetryOptInProvider.setBannerId).toBeCalledWith(bannerID); + expect(returnedBannerId).toBe(bannerID); + const bannerConfig = overlays.banners.add.mock.calls[0]; - const bannerConfig = banners.add.mock.calls[0][0]; - - expect(bannerConfig.component).not.toBe(undefined); - expect(bannerConfig.priority).toBe(10000); + expect(bannerConfig[0]).not.toBe(undefined); + expect(bannerConfig[1]).toBe(10000); }); }); diff --git a/src/legacy/core_plugins/telemetry/public/services/path.ts b/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.tsx similarity index 59% rename from src/legacy/core_plugins/telemetry/public/services/path.ts rename to src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.tsx index 4af545e982eaa..e63e46af6e8ca 100644 --- a/src/legacy/core_plugins/telemetry/public/services/path.ts +++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.tsx @@ -17,9 +17,18 @@ * under the License. */ -import chrome from 'ui/chrome'; +import React from 'react'; +import { CoreStart } from 'kibana/public'; +import { OptedInNoticeBanner } from '../../components/opted_in_notice_banner'; +import { toMountPoint } from '../../../../kibana_react/public'; -export function isUnauthenticated() { - const path = (chrome as any).removeBasePath(window.location.pathname); - return path === '/login' || path === '/logout' || path === '/logged_out' || path === '/status'; +interface RenderBannerConfig { + overlays: CoreStart['overlays']; + onSeen: () => void; +} +export function renderOptedInNoticeBanner({ onSeen, overlays }: RenderBannerConfig) { + const mount = toMountPoint(); + const bannerId = overlays.banners.add(mount, 10000); + + return bannerId; } diff --git a/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.test.ts b/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.test.ts new file mode 100644 index 0000000000000..f767615d25253 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.test.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable dot-notation */ +import { mockTelemetryNotifications, mockTelemetryService } from '../../mocks'; + +describe('onSetOptInClick', () => { + it('sets setting successfully and removes banner', async () => { + const optIn = true; + const bannerId = 'bruce-banner'; + + const telemetryService = mockTelemetryService(); + telemetryService.setOptIn = jest.fn(); + const telemetryNotifications = mockTelemetryNotifications({ telemetryService }); + telemetryNotifications['optInBannerId'] = bannerId; + + await telemetryNotifications['onSetOptInClick'](optIn); + expect(telemetryNotifications['overlays'].banners.remove).toBeCalledTimes(1); + expect(telemetryNotifications['overlays'].banners.remove).toBeCalledWith(bannerId); + expect(telemetryService.setOptIn).toBeCalledTimes(1); + expect(telemetryService.setOptIn).toBeCalledWith(optIn); + }); +}); + +describe('setOptedInNoticeSeen', () => { + it('sets setting successfully and removes banner', async () => { + const bannerId = 'bruce-banner'; + + const telemetryService = mockTelemetryService(); + telemetryService.setUserHasSeenNotice = jest.fn(); + const telemetryNotifications = mockTelemetryNotifications({ telemetryService }); + telemetryNotifications['optedInNoticeBannerId'] = bannerId; + await telemetryNotifications.setOptedInNoticeSeen(); + + expect(telemetryNotifications['overlays'].banners.remove).toBeCalledTimes(1); + expect(telemetryNotifications['overlays'].banners.remove).toBeCalledWith(bannerId); + expect(telemetryService.setUserHasSeenNotice).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts b/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts new file mode 100644 index 0000000000000..bf25bb592db82 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts @@ -0,0 +1,88 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreStart } from 'kibana/public'; +import { renderOptedInNoticeBanner } from './render_opted_in_notice_banner'; +import { renderOptInBanner } from './render_opt_in_banner'; +import { TelemetryService } from '../telemetry_service'; + +interface TelemetryNotificationsConstructor { + overlays: CoreStart['overlays']; + telemetryService: TelemetryService; +} + +export class TelemetryNotifications { + private readonly overlays: CoreStart['overlays']; + private readonly telemetryService: TelemetryService; + private optedInNoticeBannerId?: string; + private optInBannerId?: string; + + constructor({ overlays, telemetryService }: TelemetryNotificationsConstructor) { + this.telemetryService = telemetryService; + this.overlays = overlays; + } + + public shouldShowOptedInNoticeBanner = (): boolean => { + const userHasSeenOptedInNotice = this.telemetryService.getUserHasSeenOptedInNotice(); + const bannerOnScreen = typeof this.optedInNoticeBannerId !== 'undefined'; + return !bannerOnScreen && userHasSeenOptedInNotice; + }; + + public renderOptedInNoticeBanner = (): void => { + const bannerId = renderOptedInNoticeBanner({ + onSeen: this.setOptedInNoticeSeen, + overlays: this.overlays, + }); + + this.optedInNoticeBannerId = bannerId; + }; + + public shouldShowOptInBanner = (): boolean => { + const isOptedIn = this.telemetryService.getIsOptedIn(); + const bannerOnScreen = typeof this.optInBannerId !== 'undefined'; + return !bannerOnScreen && isOptedIn === null; + }; + + public renderOptInBanner = (): void => { + const bannerId = renderOptInBanner({ + setOptIn: this.onSetOptInClick, + overlays: this.overlays, + }); + + this.optInBannerId = bannerId; + }; + + private onSetOptInClick = async (isOptIn: boolean) => { + if (this.optInBannerId) { + this.overlays.banners.remove(this.optInBannerId); + this.optInBannerId = undefined; + } + + await this.telemetryService.setOptIn(isOptIn); + }; + + public setOptedInNoticeSeen = async (): Promise => { + if (this.optedInNoticeBannerId) { + this.overlays.banners.remove(this.optedInNoticeBannerId); + this.optedInNoticeBannerId = undefined; + } + + await this.telemetryService.setUserHasSeenNotice(); + }; +} diff --git a/src/plugins/telemetry/public/services/telemetry_sender.test.ts b/src/plugins/telemetry/public/services/telemetry_sender.test.ts new file mode 100644 index 0000000000000..e9f5765c10412 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_sender.test.ts @@ -0,0 +1,272 @@ +/* + * 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. + */ + +/* eslint-disable dot-notation */ +import { TelemetrySender } from './telemetry_sender'; +import { mockTelemetryService } from '../mocks'; +import { REPORT_INTERVAL_MS, LOCALSTORAGE_KEY } from '../../common/constants'; + +class LocalStorageMock implements Partial { + getItem = jest.fn(); + setItem = jest.fn(); +} + +describe('TelemetrySender', () => { + let originalLocalStorage: Storage; + let mockLocalStorage: LocalStorageMock; + beforeAll(() => { + originalLocalStorage = window.localStorage; + }); + + // @ts-ignore + beforeEach(() => (window.localStorage = mockLocalStorage = new LocalStorageMock())); + // @ts-ignore + afterAll(() => (window.localStorage = originalLocalStorage)); + + describe('constructor', () => { + it('defaults lastReport if unset', () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + expect(telemetrySender['lastReported']).toBeUndefined(); + expect(mockLocalStorage.getItem).toBeCalledTimes(1); + expect(mockLocalStorage.getItem).toHaveBeenCalledWith(LOCALSTORAGE_KEY); + }); + + it('uses lastReport if set', () => { + const lastReport = `${Date.now()}`; + mockLocalStorage.getItem.mockReturnValueOnce(JSON.stringify({ lastReport })); + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + expect(telemetrySender['lastReported']).toBe(lastReport); + }); + }); + + describe('saveToBrowser', () => { + it('uses lastReport', () => { + const lastReport = `${Date.now()}`; + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['lastReported'] = lastReport; + telemetrySender['saveToBrowser'](); + + expect(mockLocalStorage.setItem).toHaveBeenCalledTimes(1); + expect(mockLocalStorage.setItem).toHaveBeenCalledWith( + LOCALSTORAGE_KEY, + JSON.stringify({ lastReport }) + ); + }); + }); + + describe('shouldSendReport', () => { + it('returns false whenever optIn is false', () => { + const telemetryService = mockTelemetryService(); + telemetryService.getIsOptedIn = jest.fn().mockReturnValue(false); + const telemetrySender = new TelemetrySender(telemetryService); + const shouldSendRerpot = telemetrySender['shouldSendReport'](); + + expect(telemetryService.getIsOptedIn).toBeCalledTimes(1); + expect(shouldSendRerpot).toBe(false); + }); + + it('returns true if lastReported is undefined', () => { + const telemetryService = mockTelemetryService(); + telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); + const telemetrySender = new TelemetrySender(telemetryService); + const shouldSendRerpot = telemetrySender['shouldSendReport'](); + + expect(telemetrySender['lastReported']).toBeUndefined(); + expect(shouldSendRerpot).toBe(true); + }); + + it('returns true if lastReported passed REPORT_INTERVAL_MS', () => { + const lastReported = Date.now() - (REPORT_INTERVAL_MS + 1000); + + const telemetryService = mockTelemetryService(); + telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['lastReported'] = `${lastReported}`; + const shouldSendRerpot = telemetrySender['shouldSendReport'](); + expect(shouldSendRerpot).toBe(true); + }); + + it('returns false if lastReported is within REPORT_INTERVAL_MS', () => { + const lastReported = Date.now() + 1000; + + const telemetryService = mockTelemetryService(); + telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['lastReported'] = `${lastReported}`; + const shouldSendRerpot = telemetrySender['shouldSendReport'](); + expect(shouldSendRerpot).toBe(false); + }); + + it('returns true if lastReported is malformed', () => { + const telemetryService = mockTelemetryService(); + telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['lastReported'] = `random_malformed_string`; + const shouldSendRerpot = telemetrySender['shouldSendReport'](); + expect(shouldSendRerpot).toBe(true); + }); + + describe('sendIfDue', () => { + let originalFetch: typeof window['fetch']; + let mockFetch: jest.Mock; + + beforeAll(() => { + originalFetch = window.fetch; + }); + + // @ts-ignore + beforeEach(() => (window.fetch = mockFetch = jest.fn())); + // @ts-ignore + afterAll(() => (window.fetch = originalFetch)); + + it('does not send if already sending', async () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['shouldSendReport'] = jest.fn(); + telemetrySender['isSending'] = true; + await telemetrySender['sendIfDue'](); + + expect(telemetrySender['shouldSendReport']).toBeCalledTimes(0); + expect(mockFetch).toBeCalledTimes(0); + }); + + it('does not send if shouldSendReport returns false', async () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(false); + telemetrySender['isSending'] = false; + await telemetrySender['sendIfDue'](); + + expect(telemetrySender['shouldSendReport']).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(0); + }); + + it('sends report if due', async () => { + const mockTelemetryUrl = 'telemetry_cluster_url'; + const mockTelemetryPayload = ['hashed_cluster_usage_data1']; + + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + telemetrySender['isSending'] = false; + await telemetrySender['sendIfDue'](); + + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(1); + expect(mockFetch).toBeCalledWith(mockTelemetryUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: mockTelemetryPayload[0], + }); + }); + + it('sends report separately for every cluster', async () => { + const mockTelemetryUrl = 'telemetry_cluster_url'; + const mockTelemetryPayload = ['hashed_cluster_usage_data1', 'hashed_cluster_usage_data2']; + + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + telemetrySender['isSending'] = false; + await telemetrySender['sendIfDue'](); + + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(2); + }); + + it('updates last lastReported and calls saveToBrowser', async () => { + const mockTelemetryUrl = 'telemetry_cluster_url'; + const mockTelemetryPayload = ['hashed_cluster_usage_data1']; + + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + telemetrySender['saveToBrowser'] = jest.fn(); + + await telemetrySender['sendIfDue'](); + + expect(mockFetch).toBeCalledTimes(1); + expect(telemetrySender['lastReported']).toBeDefined(); + expect(telemetrySender['saveToBrowser']).toBeCalledTimes(1); + expect(telemetrySender['isSending']).toBe(false); + }); + + it('catches fetchTelemetry errors and sets isSending to false', async () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn(); + telemetryService.fetchTelemetry = jest.fn().mockImplementation(() => { + throw Error('Error fetching usage'); + }); + await telemetrySender['sendIfDue'](); + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(telemetrySender['lastReported']).toBeUndefined(); + expect(telemetrySender['isSending']).toBe(false); + }); + + it('catches fetch errors and sets isSending to false', async () => { + const mockTelemetryPayload = ['hashed_cluster_usage_data1', 'hashed_cluster_usage_data2']; + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn(); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + mockFetch.mockImplementation(() => { + throw Error('Error sending usage'); + }); + await telemetrySender['sendIfDue'](); + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(2); + expect(telemetrySender['lastReported']).toBeUndefined(); + expect(telemetrySender['isSending']).toBe(false); + }); + }); + }); + describe('startChecking', () => { + let originalSetInterval: typeof window['setInterval']; + let mockSetInterval: jest.Mock; + + beforeAll(() => { + originalSetInterval = window.setInterval; + }); + + // @ts-ignore + beforeEach(() => (window.setInterval = mockSetInterval = jest.fn())); + // @ts-ignore + afterAll(() => (window.setInterval = originalSetInterval)); + + it('calls sendIfDue every 60000 ms', () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender.startChecking(); + expect(mockSetInterval).toBeCalledTimes(1); + expect(mockSetInterval).toBeCalledWith(telemetrySender['sendIfDue'], 60000); + }); + }); +}); diff --git a/src/plugins/telemetry/public/services/telemetry_sender.ts b/src/plugins/telemetry/public/services/telemetry_sender.ts new file mode 100644 index 0000000000000..fec2db0506eb7 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_sender.ts @@ -0,0 +1,100 @@ +/* + * 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 { REPORT_INTERVAL_MS, LOCALSTORAGE_KEY } from '../../common/constants'; +import { TelemetryService } from './telemetry_service'; +import { Storage } from '../../../kibana_utils/public'; + +export class TelemetrySender { + private readonly telemetryService: TelemetryService; + private isSending: boolean = false; + private lastReported?: string; + private readonly storage: Storage; + private intervalId?: number; + + constructor(telemetryService: TelemetryService) { + this.telemetryService = telemetryService; + this.storage = new Storage(window.localStorage); + + const attributes = this.storage.get(LOCALSTORAGE_KEY); + if (attributes) { + this.lastReported = attributes.lastReport; + } + } + + private saveToBrowser = () => { + // we are the only code that manipulates this key, so it's safe to blindly overwrite the whole object + this.storage.set(LOCALSTORAGE_KEY, { lastReport: this.lastReported }); + }; + + private shouldSendReport = (): boolean => { + // check if opt-in for telemetry is enabled + if (this.telemetryService.getIsOptedIn()) { + if (!this.lastReported) { + return true; + } + // returns NaN for any malformed or unset (null/undefined) value + const lastReported = parseInt(this.lastReported, 10); + // If it's been a day since we last sent telemetry + if (isNaN(lastReported) || Date.now() - lastReported > REPORT_INTERVAL_MS) { + return true; + } + } + + return false; + }; + + private sendIfDue = async (): Promise => { + if (this.isSending || !this.shouldSendReport()) { + return; + } + + // mark that we are working so future requests are ignored until we're done + this.isSending = true; + try { + const telemetryUrl = this.telemetryService.getTelemetryUrl(); + const telemetryData: any | any[] = await this.telemetryService.fetchTelemetry(); + const clusters: string[] = [].concat(telemetryData); + await Promise.all( + clusters.map( + async cluster => + await fetch(telemetryUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: cluster, + }) + ) + ); + this.lastReported = `${Date.now()}`; + this.saveToBrowser(); + } catch (err) { + // ignore err + } finally { + this.isSending = false; + } + }; + + public startChecking = () => { + if (typeof this.intervalId === 'undefined') { + this.intervalId = window.setInterval(this.sendIfDue, 60000); + } + }; +} diff --git a/src/plugins/telemetry/public/services/telemetry_service.test.ts b/src/plugins/telemetry/public/services/telemetry_service.test.ts new file mode 100644 index 0000000000000..0ebcd52f1423c --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_service.test.ts @@ -0,0 +1,139 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable dot-notation */ +import { mockTelemetryService } from '../mocks'; + +const mockSubtract = jest.fn().mockImplementation(() => { + return { + toISOString: jest.fn(), + }; +}); + +jest.mock('moment', () => { + return jest.fn().mockImplementation(() => { + return { + subtract: mockSubtract, + toISOString: jest.fn(), + }; + }); +}); + +describe('TelemetryService', () => { + describe('fetchTelemetry', () => { + it('calls expected URL with 20 minutes - now', async () => { + const telemetryService = mockTelemetryService(); + await telemetryService.fetchTelemetry(); + expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/clusters/_stats', { + body: JSON.stringify({ unencrypted: false, timeRange: {} }), + }); + expect(mockSubtract).toBeCalledWith(20, 'minutes'); + }); + }); + + describe('fetchExample', () => { + it('calls fetchTelemetry with unencrupted: true', async () => { + const telemetryService = mockTelemetryService(); + telemetryService.fetchTelemetry = jest.fn(); + await telemetryService.fetchExample(); + expect(telemetryService.fetchTelemetry).toBeCalledWith({ unencrypted: true }); + }); + }); + + describe('setOptIn', () => { + it('calls api if canChangeOptInStatus', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + await telemetryService.setOptIn(true); + + expect(telemetryService['http'].post).toBeCalledTimes(1); + }); + + it('sends enabled true if optedIn: true', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + const optedIn = true; + await telemetryService.setOptIn(optedIn); + + expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/optIn', { + body: JSON.stringify({ enabled: optedIn }), + }); + }); + + it('sends enabled false if optedIn: false', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + const optedIn = false; + await telemetryService.setOptIn(optedIn); + + expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/optIn', { + body: JSON.stringify({ enabled: optedIn }), + }); + }); + + it('does not call reportOptInStatus if reportOptInStatusChange is false', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + telemetryService['reportOptInStatus'] = jest.fn(); + await telemetryService.setOptIn(true); + + expect(telemetryService['reportOptInStatus']).toBeCalledTimes(0); + expect(telemetryService['http'].post).toBeCalledTimes(1); + }); + + it('calls reportOptInStatus if reportOptInStatusChange is true', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: true }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + telemetryService['reportOptInStatus'] = jest.fn(); + await telemetryService.setOptIn(true); + + expect(telemetryService['reportOptInStatus']).toBeCalledTimes(1); + expect(telemetryService['http'].post).toBeCalledTimes(1); + }); + + it('adds an error toast on api error', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + telemetryService['reportOptInStatus'] = jest.fn(); + telemetryService['http'].post = jest.fn().mockImplementation((url: string) => { + if (url === '/api/telemetry/v2/optIn') { + throw Error('failed to update opt in.'); + } + }); + + await telemetryService.setOptIn(true); + expect(telemetryService['http'].post).toBeCalledTimes(1); + expect(telemetryService['reportOptInStatus']).toBeCalledTimes(0); + expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1); + }); + + it('adds an error toast on reportOptInStatus error', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: true }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + telemetryService['reportOptInStatus'] = jest.fn().mockImplementation(() => { + throw Error('failed to report OptIn Status.'); + }); + + await telemetryService.setOptIn(true); + expect(telemetryService['http'].post).toBeCalledTimes(1); + expect(telemetryService['reportOptInStatus']).toBeCalledTimes(1); + expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1); + }); + }); +}); diff --git a/src/plugins/telemetry/public/services/telemetry_service.ts b/src/plugins/telemetry/public/services/telemetry_service.ts new file mode 100644 index 0000000000000..073886e7d1327 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_service.ts @@ -0,0 +1,165 @@ +/* + * 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 moment from 'moment'; +import { i18n } from '@kbn/i18n'; +import { CoreStart } from 'kibana/public'; + +interface TelemetryServiceConstructor { + http: CoreStart['http']; + injectedMetadata: CoreStart['injectedMetadata']; + notifications: CoreStart['notifications']; + reportOptInStatusChange?: boolean; +} + +export class TelemetryService { + private readonly http: CoreStart['http']; + private readonly injectedMetadata: CoreStart['injectedMetadata']; + private readonly reportOptInStatusChange: boolean; + private readonly notifications: CoreStart['notifications']; + private isOptedIn: boolean | null; + private userHasSeenOptedInNotice: boolean; + + constructor({ + http, + injectedMetadata, + notifications, + reportOptInStatusChange = true, + }: TelemetryServiceConstructor) { + const isOptedIn = injectedMetadata.getInjectedVar('telemetryOptedIn') as boolean | null; + const userHasSeenOptedInNotice = injectedMetadata.getInjectedVar( + 'telemetryNotifyUserAboutOptInDefault' + ) as boolean; + this.reportOptInStatusChange = reportOptInStatusChange; + this.injectedMetadata = injectedMetadata; + this.notifications = notifications; + this.http = http; + + this.isOptedIn = isOptedIn; + this.userHasSeenOptedInNotice = userHasSeenOptedInNotice; + } + + public getCanChangeOptInStatus = () => { + const allowChangingOptInStatus = this.injectedMetadata.getInjectedVar( + 'allowChangingOptInStatus' + ) as boolean; + return allowChangingOptInStatus; + }; + + public getOptInStatusUrl = () => { + const telemetryOptInStatusUrl = this.injectedMetadata.getInjectedVar( + 'telemetryOptInStatusUrl' + ) as string; + return telemetryOptInStatusUrl; + }; + + public getTelemetryUrl = () => { + const telemetryUrl = this.injectedMetadata.getInjectedVar('telemetryUrl') as string; + return telemetryUrl; + }; + + public getUserHasSeenOptedInNotice = () => { + return this.userHasSeenOptedInNotice; + }; + + public getIsOptedIn = () => { + return this.isOptedIn; + }; + + public fetchExample = async () => { + return await this.fetchTelemetry({ unencrypted: true }); + }; + + public fetchTelemetry = async ({ unencrypted = false } = {}) => { + const now = moment(); + return this.http.post('/api/telemetry/v2/clusters/_stats', { + body: JSON.stringify({ + unencrypted, + timeRange: { + min: now.subtract(20, 'minutes').toISOString(), + max: now.toISOString(), + }, + }), + }); + }; + + public setOptIn = async (optedIn: boolean): Promise => { + const canChangeOptInStatus = this.getCanChangeOptInStatus(); + if (!canChangeOptInStatus) { + return false; + } + + try { + await this.http.post('/api/telemetry/v2/optIn', { + body: JSON.stringify({ enabled: optedIn }), + }); + if (this.reportOptInStatusChange) { + await this.reportOptInStatus(optedIn); + } + this.isOptedIn = optedIn; + } catch (err) { + this.notifications.toasts.addError(err, { + title: i18n.translate('telemetry.optInErrorToastTitle', { + defaultMessage: 'Error', + }), + toastMessage: i18n.translate('telemetry.optInErrorToastText', { + defaultMessage: 'An error occurred while trying to set the usage statistics preference.', + }), + }); + + return false; + } + + return true; + }; + + public setUserHasSeenNotice = async (): Promise => { + try { + await this.http.put('/api/telemetry/v2/userHasSeenNotice'); + this.userHasSeenOptedInNotice = true; + } catch (error) { + this.notifications.toasts.addError(error, { + title: i18n.translate('telemetry.optInNoticeSeenErrorTitle', { + defaultMessage: 'Error', + }), + toastMessage: i18n.translate('telemetry.optInNoticeSeenErrorToastText', { + defaultMessage: 'An error occurred dismissing the notice', + }), + }); + this.userHasSeenOptedInNotice = false; + } + }; + + private reportOptInStatus = async (OptInStatus: boolean): Promise => { + const telemetryOptInStatusUrl = this.getOptInStatusUrl(); + + try { + await fetch(telemetryOptInStatusUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ enabled: OptInStatus }), + }); + } catch (err) { + // Sending the ping is best-effort. Telemetry tries to send the ping once and discards it immediately if sending fails. + // swallow any errors + } + }; +} diff --git a/test/functional/config.ie.js b/test/functional/config.ie.js index 5e8ea56a848dc..2c32ccb69db03 100644 --- a/test/functional/config.ie.js +++ b/test/functional/config.ie.js @@ -35,7 +35,6 @@ export default async function({ readConfigFile }) { defaults: { 'accessibility:disableAnimations': true, 'dateFormat:tz': 'UTC', - 'telemetry:optIn': false, 'state:storeInSessionStorage': true, 'notifications:lifetime:info': 10000, }, @@ -43,7 +42,11 @@ export default async function({ readConfigFile }) { kbnTestServer: { ...defaultConfig.get('kbnTestServer'), - serverArgs: [...defaultConfig.get('kbnTestServer.serverArgs'), '--csp.strict=false'], + serverArgs: [ + ...defaultConfig.get('kbnTestServer.serverArgs'), + '--csp.strict=false', + '--telemetry.optIn=false', + ], }, }; } diff --git a/test/functional/config.js b/test/functional/config.js index 134ddf4e84b2d..155e844578c54 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -44,14 +44,17 @@ export default async function({ readConfigFile }) { kbnTestServer: { ...commonConfig.get('kbnTestServer'), - serverArgs: [...commonConfig.get('kbnTestServer.serverArgs'), '--oss'], + serverArgs: [ + ...commonConfig.get('kbnTestServer.serverArgs'), + '--oss', + '--telemetry.optIn=false', + ], }, uiSettings: { defaults: { 'accessibility:disableAnimations': true, 'dateFormat:tz': 'UTC', - 'telemetry:optIn': false, }, }, diff --git a/x-pack/.gitignore b/x-pack/.gitignore index 40a52f88dbbba..6bac5e181861d 100644 --- a/x-pack/.gitignore +++ b/x-pack/.gitignore @@ -4,6 +4,7 @@ /test/functional/failure_debug /test/functional/screenshots /test/functional/apps/reporting/reports/session +/test/reporting/configs/failure_debug/ /legacy/plugins/reporting/.chromium/ /legacy/plugins/reporting/.phantom/ /.aws-config.json diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/telemetry_opt_in.test.js.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/telemetry_opt_in.test.js.snap deleted file mode 100644 index 575c47205f9c0..0000000000000 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/telemetry_opt_in.test.js.snap +++ /dev/null @@ -1,576 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TelemetryOptIn should display when telemetry not opted in 1`] = ` - - -
- - -

- - Help Elastic support provide better service - -

-
- -
- - - - - - } - className="eui-AlignBaseline" - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="readMorePopover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - > - -

- - - , - "telemetryPrivacyStatementLink": - - , - } - } - /> -

-
- , - } - } - /> - - } - onChange={[Function]} - > -
- -
- -
- - -`; - -exports[`TelemetryOptIn should not display when telemetry is opted in 1`] = ` - -`; - -exports[`TelemetryOptIn shouldn't display when telemetry optIn status can't change 1`] = ` - -`; diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap index 353dc58e6d401..3bb8e4f8608a7 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -965,7 +965,6 @@ exports[`UploadLicense should display a modal when license requires acknowledgem className="euiSpacer euiSpacer--m" /> - @@ -1434,7 +1433,6 @@ exports[`UploadLicense should display an error when ES says license is expired 1 className="euiSpacer euiSpacer--m" /> - @@ -1903,7 +1901,6 @@ exports[`UploadLicense should display an error when ES says license is invalid 1 className="euiSpacer euiSpacer--m" /> - @@ -2368,7 +2365,6 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`] className="euiSpacer euiSpacer--m" /> - @@ -2837,7 +2833,6 @@ exports[`UploadLicense should display error when ES returns error 1`] = ` className="euiSpacer euiSpacer--m" /> - diff --git a/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js b/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js deleted file mode 100644 index 1b03ce869e52b..0000000000000 --- a/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js +++ /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 React from 'react'; -import { - setTelemetryEnabled, - setTelemetryOptInService, -} from '../public/np_ready/application/lib/telemetry'; -import { TelemetryOptIn } from '../public/np_ready/application/components/telemetry_opt_in'; -import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; - -jest.mock('ui/new_platform'); - -setTelemetryEnabled(true); - -describe('TelemetryOptIn', () => { - test('should display when telemetry not opted in', () => { - setTelemetryOptInService({ - getOptIn: () => false, - canChangeOptInStatus: () => true, - }); - const rendered = mountWithIntl(); - expect(rendered).toMatchSnapshot(); - }); - test('should not display when telemetry is opted in', () => { - setTelemetryOptInService({ - getOptIn: () => true, - canChangeOptInStatus: () => true, - }); - const rendered = mountWithIntl(); - expect(rendered).toMatchSnapshot(); - }); - test(`shouldn't display when telemetry optIn status can't change`, () => { - setTelemetryOptInService({ - getOptIn: () => false, - canChangeOptInStatus: () => false, - }); - const rendered = mountWithIntl(); - expect(rendered).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js index 7c497518b9df5..6a6c38fa6abb6 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js @@ -18,7 +18,7 @@ export class App extends Component { } render() { - const { hasPermission, permissionsLoading, permissionsError } = this.props; + const { hasPermission, permissionsLoading, permissionsError, telemetry } = this.props; if (permissionsLoading) { return ( @@ -85,11 +85,12 @@ export class App extends Component { ); } + const withTelemetry = Component => props => ; return ( - - + + ); diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx b/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx index 2780b54230eba..49bb4ce984e48 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx @@ -11,6 +11,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import * as history from 'history'; import { DocLinksStart, HttpSetup, ToastsSetup, ChromeStart } from 'src/core/public'; +import { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; // @ts-ignore import { App } from './app.container'; // @ts-ignore @@ -34,10 +35,11 @@ interface AppDependencies { toasts: ToastsSetup; docLinks: DocLinksStart; http: HttpSetup; + telemetry?: TelemetryPluginSetup; } export const boot = (deps: AppDependencies) => { - const { I18nContext, element, legacy, toasts, docLinks, http, chrome } = deps; + const { I18nContext, element, legacy, toasts, docLinks, http, chrome, telemetry } = deps; const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; const securityDocumentationLink = `${esBase}/security-settings.html`; @@ -56,15 +58,17 @@ export const boot = (deps: AppDependencies) => { toasts, http, chrome, + telemetry, MANAGEMENT_BREADCRUMB: legacy.MANAGEMENT_BREADCRUMB, }; const store = licenseManagementStore(initialState, services); + render( - + , diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.ts similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.ts diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.tsx similarity index 84% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.tsx index 5e570ae955dbf..eff5c6cc21c43 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.tsx @@ -6,26 +6,31 @@ import React, { Fragment } from 'react'; import { EuiLink, EuiCheckbox, EuiSpacer, EuiText, EuiTitle, EuiPopover } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { - shouldShowTelemetryOptIn, - getTelemetryFetcher, - PRIVACY_STATEMENT_URL, OptInExampleFlyout, + PRIVACY_STATEMENT_URL, + TelemetryPluginSetup, } from '../../lib/telemetry'; -import { FormattedMessage } from '@kbn/i18n/react'; -export class TelemetryOptIn extends React.Component { - constructor() { - super(); - this.state = { - showMoreTelemetryInfo: false, - isOptingInToTelemetry: false, - showExample: false, - }; - } - isOptingInToTelemetry = () => { - return this.state.isOptingInToTelemetry; +interface State { + showMoreTelemetryInfo: boolean; + showExample: boolean; +} + +interface Props { + onOptInChange: (isOptingInToTelemetry: boolean) => void; + isOptingInToTelemetry: boolean; + isStartTrial: boolean; + telemetry: TelemetryPluginSetup; +} + +export class TelemetryOptIn extends React.Component { + state: State = { + showMoreTelemetryInfo: false, + showExample: false, }; + closeReadMorePopover = () => { this.setState({ showMoreTelemetryInfo: false }); }; @@ -37,20 +42,22 @@ export class TelemetryOptIn extends React.Component { this.setState({ showExample: true }); this.closeReadMorePopover(); }; - onChangeOptIn = event => { + onChangeOptIn = (event: any) => { const isOptingInToTelemetry = event.target.checked; - this.setState({ isOptingInToTelemetry }); + const { onOptInChange } = this.props; + onOptInChange(isOptingInToTelemetry); }; + render() { - const { showMoreTelemetryInfo, isOptingInToTelemetry, showExample } = this.state; - const { isStartTrial } = this.props; + const { showMoreTelemetryInfo, showExample } = this.state; + const { isStartTrial, isOptingInToTelemetry, telemetry } = this.props; let example = null; if (showExample) { example = ( this.setState({ showExample: false })} - fetchTelemetry={getTelemetryFetcher} + fetchExample={telemetry.telemetryService.fetchExample} /> ); } @@ -123,7 +130,7 @@ export class TelemetryOptIn extends React.Component { ); - return shouldShowTelemetryOptIn() ? ( + return ( {example} {toCurrentCustomers} @@ -144,6 +151,6 @@ export class TelemetryOptIn extends React.Component { onChange={this.onChangeOptIn} /> - ) : null; + ); } } diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.js deleted file mode 100644 index 10da5d7705a8c..0000000000000 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.js +++ /dev/null @@ -1,36 +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 { fetchTelemetry } from '../../../../../../../../src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry'; -export { PRIVACY_STATEMENT_URL } from '../../../../../../../../src/legacy/core_plugins/telemetry/common/constants'; -export { TelemetryOptInProvider } from '../../../../../../../../src/legacy/core_plugins/telemetry/public/services'; -export { OptInExampleFlyout } from '../../../../../../../../src/legacy/core_plugins/telemetry/public/components'; - -let telemetryEnabled; -let httpClient; -let telemetryOptInService; -export const setTelemetryEnabled = isTelemetryEnabled => { - telemetryEnabled = isTelemetryEnabled; -}; -export const setHttpClient = anHttpClient => { - httpClient = anHttpClient; -}; -export const setTelemetryOptInService = aTelemetryOptInService => { - telemetryOptInService = aTelemetryOptInService; -}; -export const optInToTelemetry = async enableTelemetry => { - await telemetryOptInService.setOptIn(enableTelemetry); -}; -export const shouldShowTelemetryOptIn = () => { - return ( - telemetryEnabled && - !telemetryOptInService.getOptIn() && - telemetryOptInService.canChangeOptInStatus() - ); -}; -export const getTelemetryFetcher = () => { - return fetchTelemetry(httpClient, { unencrypted: true }); -}; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.ts b/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.ts new file mode 100644 index 0000000000000..9cc4ec5978fdc --- /dev/null +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.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 { TelemetryPluginSetup } from '../../../../../../../../src/plugins/telemetry/public'; + +export { OptInExampleFlyout } from '../../../../../../../../src/plugins/telemetry/public/components'; +export { PRIVACY_STATEMENT_URL } from '../../../../../../../../src/plugins/telemetry/common/constants'; +export { TelemetryPluginSetup, shouldShowTelemetryOptIn }; + +function shouldShowTelemetryOptIn( + telemetry?: TelemetryPluginSetup +): telemetry is TelemetryPluginSetup { + if (telemetry) { + const { telemetryService } = telemetry; + const isOptedIn = telemetryService.getIsOptedIn(); + const canChangeOptInStatus = telemetryService.getCanChangeOptInStatus(); + return canChangeOptInStatus && !isOptedIn; + } + + return false; +} diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js index e14d392fe6706..56c307a0d76e5 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js @@ -12,7 +12,7 @@ import { AddLicense } from './add_license'; import { RequestTrialExtension } from './request_trial_extension'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -export const LicenseDashboard = ({ setBreadcrumb } = { setBreadcrumb: () => {} }) => { +export const LicenseDashboard = ({ setBreadcrumb, telemetry } = { setBreadcrumb: () => {} }) => { useEffect(() => { setBreadcrumb('dashboard'); }); @@ -25,7 +25,7 @@ export const LicenseDashboard = ({ setBreadcrumb } = { setBreadcrumb: () => {} } - + diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.ts similarity index 95% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.ts index b9b33e7e3f2cb..1b3c956edc3ab 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ +// @ts-ignore export { StartTrial } from './start_trial.container'; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.tsx similarity index 87% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.tsx index 532c1d5e1a32f..e0f8ade8e45da 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Component } from 'react'; import { EuiButtonEmpty, @@ -22,32 +22,56 @@ import { EuiModalHeaderTitle, } from '@elastic/eui'; -import { TelemetryOptIn } from '../../../components/telemetry_opt_in'; -import { optInToTelemetry } from '../../../lib/telemetry'; import { FormattedMessage } from '@kbn/i18n/react'; +import { TelemetryOptIn } from '../../../components/telemetry_opt_in'; import { EXTERNAL_LINKS } from '../../../../../../common/constants'; import { getDocLinks } from '../../../lib/docs_links'; +import { TelemetryPluginSetup, shouldShowTelemetryOptIn } from '../../../lib/telemetry'; + +interface Props { + loadTrialStatus: () => void; + startLicenseTrial: () => void; + telemetry?: TelemetryPluginSetup; + shouldShowStartTrial: boolean; +} + +interface State { + showConfirmation: boolean; + isOptingInToTelemetry: boolean; +} + +export class StartTrial extends Component { + cancelRef: any; + confirmRef: any; + + state: State = { + showConfirmation: false, + isOptingInToTelemetry: false, + }; -export class StartTrial extends React.PureComponent { - constructor(props) { - super(props); - this.state = { showConfirmation: false }; - } UNSAFE_componentWillMount() { this.props.loadTrialStatus(); } - startLicenseTrial = () => { - const { startLicenseTrial } = this.props; - if (this.telemetryOptIn.isOptingInToTelemetry()) { - optInToTelemetry(true); + + onOptInChange = (isOptingInToTelemetry: boolean) => { + this.setState({ isOptingInToTelemetry }); + }; + + onStartLicenseTrial = () => { + const { telemetry, startLicenseTrial } = this.props; + if (this.state.isOptingInToTelemetry && telemetry) { + telemetry.telemetryService.setOptIn(true); } startLicenseTrial(); }; + cancel = () => { this.setState({ showConfirmation: false }); }; acknowledgeModal() { - const { showConfirmation } = this.state; + const { showConfirmation, isOptingInToTelemetry } = this.state; + const { telemetry } = this.props; + if (!showConfirmation) { return null; } @@ -158,12 +182,14 @@ export class StartTrial extends React.PureComponent { - { - this.telemetryOptIn = ref; - }} - /> + {shouldShowTelemetryOptIn(telemetry) && ( + + )} @@ -182,7 +208,7 @@ export class StartTrial extends React.PureComponent { { + this.setState({ isOptingInToTelemetry }); + }; send = acknowledge => { const file = this.file; const fr = new FileReader(); + fr.onload = ({ target: { result } }) => { - if (this.telemetryOptIn.isOptingInToTelemetry()) { - optInToTelemetry(true); + if (this.state.isOptingInToTelemetry) { + this.props.telemetry?.telemetryService.setOptIn(true); } this.props.uploadLicense(result, this.props.currentLicenseType, acknowledge); }; @@ -116,7 +124,8 @@ export class UploadLicense extends React.PureComponent { } }; render() { - const { currentLicenseType, applying } = this.props; + const { currentLicenseType, applying, telemetry } = this.props; + return ( @@ -170,11 +179,13 @@ export class UploadLicense extends React.PureComponent { - { - this.telemetryOptIn = ref; - }} - /> + {shouldShowTelemetryOptIn(telemetry) && ( + + )} diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts b/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts index 1da3c942830ca..60876c9b638d1 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts +++ b/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts @@ -5,11 +5,12 @@ */ import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; import { PLUGIN } from '../../common/constants'; import { Breadcrumb } from './application/breadcrumbs'; - export interface Plugins { + telemetry: TelemetryPluginSetup; __LEGACY: { xpackInfo: XPackMainPlugin; refreshXpack: () => void; @@ -18,7 +19,7 @@ export interface Plugins { } export class LicenseManagementUIPlugin implements Plugin { - setup({ application, notifications, http }: CoreSetup, { __LEGACY }: Plugins) { + setup({ application, notifications, http }: CoreSetup, { __LEGACY, telemetry }: Plugins) { application.register({ id: PLUGIN.ID, title: PLUGIN.TITLE, @@ -41,6 +42,7 @@ export class LicenseManagementUIPlugin implements Plugin { http, element, chrome, + telemetry, }); }, }); diff --git a/x-pack/legacy/plugins/license_management/public/register_route.ts b/x-pack/legacy/plugins/license_management/public/register_route.ts index fc1678a866ad3..a8f27a7236a47 100644 --- a/x-pack/legacy/plugins/license_management/public/register_route.ts +++ b/x-pack/legacy/plugins/license_management/public/register_route.ts @@ -15,15 +15,6 @@ import routes from 'ui/routes'; import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { plugin } from './np_ready'; - -import { - setTelemetryOptInService, - setTelemetryEnabled, - setHttpClient, - TelemetryOptInProvider, - // @ts-ignore -} from './np_ready/application/lib/telemetry'; - import { BASE_PATH } from '../common/constants'; const licenseManagementUiEnabled = chrome.getInjected('licenseManagementUiEnabled'); @@ -51,15 +42,6 @@ if (licenseManagementUiEnabled) { }); }; - const initializeTelemetry = ($injector: any) => { - const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled'); - const Private = $injector.get('Private'); - const telemetryOptInProvider = Private(TelemetryOptInProvider); - setTelemetryOptInService(telemetryOptInProvider); - setTelemetryEnabled(telemetryEnabled); - setHttpClient($injector.get('$http')); - }; - const template = `
`; @@ -69,8 +51,6 @@ if (licenseManagementUiEnabled) { controllerAs: 'licenseManagement', controller: class LicenseManagementController { constructor($injector: any, $rootScope: any, $scope: any, $route: any) { - initializeTelemetry($injector); - $scope.$$postDigest(() => { const element = document.getElementById('licenseReactRoot')!; @@ -94,6 +74,7 @@ if (licenseManagementUiEnabled) { }, }, { + telemetry: (npSetup.plugins as any).telemetry, __LEGACY: { xpackInfo, refreshXpack, MANAGEMENT_BREADCRUMB }, } ); diff --git a/x-pack/legacy/plugins/xpack_main/index.js b/x-pack/legacy/plugins/xpack_main/index.js index f3994f7ebcc34..809d90d58d796 100644 --- a/x-pack/legacy/plugins/xpack_main/index.js +++ b/x-pack/legacy/plugins/xpack_main/index.js @@ -11,8 +11,6 @@ import { replaceInjectedVars } from './server/lib/replace_injected_vars'; import { setupXPackMain } from './server/lib/setup_xpack_main'; import { xpackInfoRoute, settingsRoute } from './server/routes/api/v1'; -import { has } from 'lodash'; - export { callClusterFactory } from './server/lib/call_cluster_factory'; import { registerMonitoringCollection } from './server/telemetry_collection'; @@ -82,21 +80,5 @@ export const xpackMain = kibana => { xpackInfoRoute(server); settingsRoute(server, this.kbnServer); }, - deprecations: () => { - function movedToTelemetry(configPath) { - return (settings, log) => { - if (has(settings, configPath)) { - log( - `Config key "xpack.xpack_main.${configPath}" is deprecated. Use "telemetry.${configPath}" instead.` - ); - } - }; - } - return [ - movedToTelemetry('telemetry.config'), - movedToTelemetry('telemetry.url'), - movedToTelemetry('telemetry.enabled'), - ]; - }, }); }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index dbc6a015f9c97..6bcf61b53fd5f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2492,10 +2492,6 @@ "telemetry.seeExampleOfWhatWeCollectLinkText": "収集されるデータの例を見る", "telemetry.telemetryBannerDescription": "Elastic Stackの改善にご協力ください使用状況データの収集は現在無効です。使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細については、{privacyStatementLink}をご覧ください。", "telemetry.telemetryConfigDescription": "基本的な機能の利用状況に関する統計情報を提供して、Elastic Stack の改善にご協力ください。このデータは Elastic 社外と共有されません。", - "telemetry.telemetryConfigTitle": "遠隔測定オプトイン", - "telemetry.telemetryErrorNotificationMessageDescription.tryAgainText": "Kibana と Elasticsearch が現在も実行中であることを確認し、再試行してください。", - "telemetry.telemetryErrorNotificationMessageDescription.unableToSaveTelemetryPreferenceText": "遠隔測定設定を保存できません。", - "telemetry.telemetryErrorNotificationMessageTitle": "遠隔測定エラー", "telemetry.telemetryOptedInDisableUsage": "ここで使用状況データを無効にする", "telemetry.telemetryOptedInDismissMessage": "閉じる", "telemetry.telemetryOptedInNoticeDescription": "使用状況データがどのように製品とサービスの管理と改善につながるのかに関する詳細については、{privacyStatementLink}をご覧ください。収集を停止するには、{disableLink}。", @@ -13187,4 +13183,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4a2c33eba79da..25382221716dd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2492,10 +2492,6 @@ "telemetry.seeExampleOfWhatWeCollectLinkText": "查看我们收集的内容示例", "telemetry.telemetryBannerDescription": "想帮助我们改进 Elastic Stack?数据使用情况收集当前已禁用。启用数据使用情况收集可帮助我们管理并改善产品和服务。有关详情,请参阅我们的{privacyStatementLink}。", "telemetry.telemetryConfigDescription": "通过提供基本功能的使用情况统计信息,来帮助我们改进 Elastic Stack。我们不会在 Elastic 之外共享此数据。", - "telemetry.telemetryConfigTitle": "遥测选择加入", - "telemetry.telemetryErrorNotificationMessageDescription.tryAgainText": "确认 Kibana 和 Elasticsearch 仍在运行,然后重试。", - "telemetry.telemetryErrorNotificationMessageDescription.unableToSaveTelemetryPreferenceText": "无法保存遥测首选项。", - "telemetry.telemetryErrorNotificationMessageTitle": "遥测错误", "telemetry.telemetryOptedInDisableUsage": "请在此禁用使用情况数据", "telemetry.telemetryOptedInDismissMessage": "关闭", "telemetry.telemetryOptedInNoticeDescription": "要了解使用情况数据如何帮助我们管理和改善产品和服务,请参阅我们的{privacyStatementLink}。要停止收集,{disableLink}。", @@ -13186,4 +13182,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file diff --git a/x-pack/test/functional/config.ie.js b/x-pack/test/functional/config.ie.js index 081bab4b80457..bac4547b4aa5c 100644 --- a/x-pack/test/functional/config.ie.js +++ b/x-pack/test/functional/config.ie.js @@ -58,14 +58,17 @@ export default async function({ readConfigFile }) { defaults: { 'accessibility:disableAnimations': true, 'dateFormat:tz': 'UTC', - 'telemetry:optIn': false, 'state:storeInSessionStorage': true, }, }, kbnTestServer: { ...defaultConfig.get('kbnTestServer'), - serverArgs: [...defaultConfig.get('kbnTestServer.serverArgs'), '--csp.strict=false'], + serverArgs: [ + ...defaultConfig.get('kbnTestServer.serverArgs'), + '--csp.strict=false', + '--telemetry.optIn=false', + ], }, }; } diff --git a/x-pack/test/reporting/functional/reporting.js b/x-pack/test/reporting/functional/reporting.js index 0e1078a2a4c8b..012f0922c28cf 100644 --- a/x-pack/test/reporting/functional/reporting.js +++ b/x-pack/test/reporting/functional/reporting.js @@ -94,8 +94,8 @@ export default function({ getService, getPageObjects }) { // Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs // function is taking about 15 seconds per comparison in jenkins. this.timeout(300000); - - await PageObjects.dashboard.switchToEditMode(); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.gotoDashboardEditMode('My PDF Dashboard'); await PageObjects.reporting.setTimepickerInDataRange(); const visualizations = PageObjects.dashboard.getTestVisualizationNames(); @@ -135,7 +135,8 @@ export default function({ getService, getPageObjects }) { it('matches baseline report', async function() { this.timeout(300000); - await PageObjects.dashboard.switchToEditMode(); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.gotoDashboardEditMode('My PNG Dash'); await PageObjects.reporting.setTimepickerInDataRange(); const visualizations = PageObjects.dashboard.getTestVisualizationNames(); From 26ad75659680e738faad5f605dbac67fd139950d Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 13 Feb 2020 10:49:12 +0100 Subject: [PATCH 3/9] add `absolute` option to `getUrlForApp` (#57193) --- ...lugin-public.applicationstart.geturlforapp.md | 7 +++++-- .../kibana-plugin-public.applicationstart.md | 2 +- .../application/application_service.test.ts | 11 ++++++++++- .../public/application/application_service.tsx | 16 ++++++++++++++-- src/core/public/application/types.ts | 10 ++++++++-- src/core/public/public.api.md | 1 + 6 files changed, 39 insertions(+), 8 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md b/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md index 7eadd4d4e9d44..1ae368a11674f 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md @@ -4,13 +4,16 @@ ## ApplicationStart.getUrlForApp() method -Returns a relative URL to a given app, including the global base path. +Returns an URL to a given app, including the global base path. By default, the URL is relative (/basePath/app/my-app). Use the `absolute` option to generate an absolute url (http://host:port/basePath/app/my-app) + +Note that when generating absolute urls, the protocol, host and port are determined from the browser location. Signature: ```typescript getUrlForApp(appId: string, options?: { path?: string; + absolute?: boolean; }): string; ``` @@ -19,7 +22,7 @@ getUrlForApp(appId: string, options?: { | Parameter | Type | Description | | --- | --- | --- | | appId | string | | -| options | {
path?: string;
} | | +| options | {
path?: string;
absolute?: boolean;
} | | Returns: diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.md b/docs/development/core/public/kibana-plugin-public.applicationstart.md index 433ce87419ae8..d5a0bef9470f7 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.md @@ -22,7 +22,7 @@ export interface ApplicationStart | Method | Description | | --- | --- | -| [getUrlForApp(appId, options)](./kibana-plugin-public.applicationstart.geturlforapp.md) | Returns a relative URL to a given app, including the global base path. | +| [getUrlForApp(appId, options)](./kibana-plugin-public.applicationstart.geturlforapp.md) | Returns an URL to a given app, including the global base path. By default, the URL is relative (/basePath/app/my-app). Use the absolute option to generate an absolute url (http://host:port/basePath/app/my-app)Note that when generating absolute urls, the protocol, host and port are determined from the browser location. | | [navigateToApp(appId, options)](./kibana-plugin-public.applicationstart.navigatetoapp.md) | Navigate to a given app | | [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationstart.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). | diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index 18716bd872842..5487ca53170dd 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -580,7 +580,6 @@ describe('#start()', () => { it('creates URLs with path parameter', async () => { service.setup(setupDeps); - const { getUrlForApp } = await service.start(startDeps); expect(getUrlForApp('app1', { path: 'deep/link' })).toBe('/base-path/app/app1/deep/link'); @@ -588,6 +587,16 @@ describe('#start()', () => { expect(getUrlForApp('app1', { path: '//deep/link//' })).toBe('/base-path/app/app1/deep/link'); expect(getUrlForApp('app1', { path: 'deep/link///' })).toBe('/base-path/app/app1/deep/link'); }); + + it('creates absolute URLs when `absolute` parameter is true', async () => { + service.setup(setupDeps); + const { getUrlForApp } = await service.start(startDeps); + + expect(getUrlForApp('app1', { absolute: true })).toBe('http://localhost/base-path/app/app1'); + expect(getUrlForApp('app2', { path: 'deep/link', absolute: true })).toBe( + 'http://localhost/base-path/app/app2/deep/link' + ); + }); }); describe('navigateToApp', () => { diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index d100457f4027f..77f06e316c0aa 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -272,8 +272,13 @@ export class ApplicationService { takeUntil(this.stop$) ), registerMountContext: this.mountContext.registerContext, - getUrlForApp: (appId, { path }: { path?: string } = {}) => - http.basePath.prepend(getAppUrl(availableMounters, appId, path)), + getUrlForApp: ( + appId, + { path, absolute = false }: { path?: string; absolute?: boolean } = {} + ) => { + const relUrl = http.basePath.prepend(getAppUrl(availableMounters, appId, path)); + return absolute ? relativeToAbsolute(relUrl) : relUrl; + }, navigateToApp: async (appId, { path, state }: { path?: string; state?: any } = {}) => { if (await this.shouldNavigate(overlays)) { this.appLeaveHandlers.delete(this.currentAppId$.value!); @@ -364,3 +369,10 @@ const updateStatus = (app: T, statusUpdaters: AppUpdaterWrapp ...changes, }; }; + +function relativeToAbsolute(url: string) { + // convert all link urls to absolute urls + const a = document.createElement('a'); + a.setAttribute('href', url); + return a.href; +} diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 493afd1fec9db..977bb7a52da22 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -593,11 +593,17 @@ export interface ApplicationStart { navigateToApp(appId: string, options?: { path?: string; state?: any }): Promise; /** - * Returns a relative URL to a given app, including the global base path. + * Returns an URL to a given app, including the global base path. + * By default, the URL is relative (/basePath/app/my-app). + * Use the `absolute` option to generate an absolute url (http://host:port/basePath/app/my-app) + * + * Note that when generating absolute urls, the protocol, host and port are determined from the browser location. + * * @param appId * @param options.path - optional path inside application to deep link to + * @param options.absolute - if true, will returns an absolute url instead of a relative one */ - getUrlForApp(appId: string, options?: { path?: string }): string; + getUrlForApp(appId: string, options?: { path?: string; absolute?: boolean }): string; /** * Register a context provider for application mounting. Will only be available to applications that depend on the diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index aab88b0befba3..5e9b609bde916 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -101,6 +101,7 @@ export interface ApplicationStart { currentAppId$: Observable; getUrlForApp(appId: string, options?: { path?: string; + absolute?: boolean; }): string; navigateToApp(appId: string, options?: { path?: string; From 83cce379ad584ad11fda754bf6d411db79cd13bb Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Thu, 13 Feb 2020 12:31:40 +0100 Subject: [PATCH 4/9] refactors 'data-providers' tests (#57474) --- .../timeline/data_providers.spec.ts | 61 ++++++------------- .../siem/cypress/screens/hosts/all_hosts.ts | 9 +++ .../siem/cypress/screens/timeline/main.ts | 12 ++++ .../plugins/siem/cypress/tasks/common.ts | 50 +++++++++++++++ .../siem/cypress/tasks/hosts/all_hosts.ts | 40 ++++++++++++ .../siem/cypress/tasks/timeline/main.ts | 8 +++ .../plugins/siem/cypress/urls/navigation.ts | 1 + 7 files changed, 140 insertions(+), 41 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/cypress/screens/hosts/all_hosts.ts create mode 100644 x-pack/legacy/plugins/siem/cypress/tasks/common.ts create mode 100644 x-pack/legacy/plugins/siem/cypress/tasks/hosts/all_hosts.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts index 24c1974cf8343..3d251c1c6bcac 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts @@ -4,30 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HOSTS_PAGE } from '../../../urls/navigation'; import { + waitForAllHostsToBeLoaded, + dragAndDropFirstHostToTimeline, + dragFirstHostToTimeline, + dragFirstHostToEmptyTimelineDataProviders, +} from '../../../tasks/hosts/all_hosts'; +import { HOSTS_NAMES } from '../../../screens/hosts/all_hosts'; +import { DEFAULT_TIMEOUT, loginAndWaitForPage } from '../../../tasks/login'; +import { openTimeline, createNewTimeline } from '../../../tasks/timeline/main'; +import { + TIMELINE_DATA_PROVIDERS_EMPTY, TIMELINE_DATA_PROVIDERS, TIMELINE_DROPPED_DATA_PROVIDERS, - TIMELINE_DATA_PROVIDERS_EMPTY, -} from '../../lib/timeline/selectors'; -import { - createNewTimeline, - dragFromAllHostsToTimeline, - toggleTimelineVisibility, -} from '../../lib/timeline/helpers'; -import { ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS } from '../../lib/hosts/selectors'; -import { HOSTS_PAGE } from '../../lib/urls'; -import { waitForAllHostsWidget } from '../../lib/hosts/helpers'; -import { DEFAULT_TIMEOUT, loginAndWaitForPage } from '../../lib/util/helpers'; -import { drag, dragWithoutDrop } from '../../lib/drag_n_drop/helpers'; +} from '../../../screens/timeline/main'; describe('timeline data providers', () => { before(() => { loginAndWaitForPage(HOSTS_PAGE); - waitForAllHostsWidget(); + waitForAllHostsToBeLoaded(); }); beforeEach(() => { - toggleTimelineVisibility(); + openTimeline(); }); afterEach(() => { @@ -35,16 +35,13 @@ describe('timeline data providers', () => { }); it('renders the data provider of a host dragged from the All Hosts widget on the hosts page', () => { - dragFromAllHostsToTimeline(); + dragAndDropFirstHostToTimeline(); - cy.get(TIMELINE_DROPPED_DATA_PROVIDERS, { - timeout: DEFAULT_TIMEOUT + 10 * 1000, - }) + cy.get(TIMELINE_DROPPED_DATA_PROVIDERS, { timeout: DEFAULT_TIMEOUT }) .first() .invoke('text') .then(dataProviderText => { - // verify the data provider displays the same `host.name` as the host dragged from the `All Hosts` widget - cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS) + cy.get(HOSTS_NAMES) .first() .invoke('text') .should(hostname => { @@ -54,9 +51,7 @@ describe('timeline data providers', () => { }); it('sets the background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the data providers', () => { - cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS) - .first() - .then(host => drag(host)); + dragFirstHostToTimeline(); cy.get(TIMELINE_DATA_PROVIDERS).should( 'have.css', @@ -65,30 +60,14 @@ describe('timeline data providers', () => { ); }); - it('sets the background to euiColorSuccess with a 20% alpha channel when the user starts dragging a host AND is hovering over the data providers', () => { - cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS) - .first() - .then(host => drag(host)); - - cy.get(TIMELINE_DATA_PROVIDERS_EMPTY).then(dataProvidersDropArea => - dragWithoutDrop(dataProvidersDropArea) - ); + it('sets the background to euiColorSuccess with a 20% alpha channel and renders the dashed border color as euiColorSuccess when the user starts dragging a host AND is hovering over the data providers', () => { + dragFirstHostToEmptyTimelineDataProviders(); cy.get(TIMELINE_DATA_PROVIDERS_EMPTY).should( 'have.css', 'background', 'rgba(1, 125, 115, 0.2) none repeat scroll 0% 0% / auto padding-box border-box' ); - }); - - it('renders the dashed border color as euiColorSuccess when hovering over the data providers', () => { - cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS) - .first() - .then(host => drag(host)); - - cy.get(TIMELINE_DATA_PROVIDERS_EMPTY).then(dataProvidersDropArea => - dragWithoutDrop(dataProvidersDropArea) - ); cy.get(TIMELINE_DATA_PROVIDERS).should( 'have.css', diff --git a/x-pack/legacy/plugins/siem/cypress/screens/hosts/all_hosts.ts b/x-pack/legacy/plugins/siem/cypress/screens/hosts/all_hosts.ts new file mode 100644 index 0000000000000..f316356580814 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/screens/hosts/all_hosts.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const ALL_HOSTS_TABLE = '[data-test-subj="table-allHosts-loading-false"]'; + +export const HOSTS_NAMES = '[data-test-subj="draggable-content-host.name"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts b/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts index ca11f48932263..60c9c2ab44372 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts @@ -20,3 +20,15 @@ export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-gear"]'; export const TIMELINE_INSPECT_BUTTON = '[data-test-subj="inspect-empty-button"]'; + +export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]'; + +export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]'; + +export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; + +export const TIMELINE_DATA_PROVIDERS_EMPTY = + '[data-test-subj="dataProviders"] [data-test-subj="empty"]'; + +export const TIMELINE_DROPPED_DATA_PROVIDERS = + '[data-test-subj="dataProviders"] [data-test-subj="providerContainer"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/common.ts b/x-pack/legacy/plugins/siem/cypress/tasks/common.ts new file mode 100644 index 0000000000000..39a61401c15b3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/tasks/common.ts @@ -0,0 +1,50 @@ +/* + * 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. + */ + +const primaryButton = 0; + +/** + * To overcome the React Beautiful DND sloppy click detection threshold: + * https://github.com/atlassian/react-beautiful-dnd/blob/67b96c8d04f64af6b63ae1315f74fc02b5db032b/docs/sensors/mouse.md#sloppy-clicks-and-click-prevention- + */ +const dndSloppyClickDetectionThreshold = 5; + +/** Starts dragging the subject */ +export const drag = (subject: JQuery) => { + const subjectLocation = subject[0].getBoundingClientRect(); + + cy.wrap(subject) + .trigger('mousedown', { + button: primaryButton, + clientX: subjectLocation.left, + clientY: subjectLocation.top, + force: true, + }) + .wait(1) + .trigger('mousemove', { + button: primaryButton, + clientX: subjectLocation.left + dndSloppyClickDetectionThreshold, + clientY: subjectLocation.top, + force: true, + }) + .wait(1); +}; + +/** "Drops" the subject being dragged on the specified drop target */ +export const drop = (dropTarget: JQuery) => { + cy.wrap(dropTarget) + .trigger('mousemove', { button: primaryButton, force: true }) + .wait(1) + .trigger('mouseup', { force: true }) + .wait(1); +}; + +/** Drags the subject being dragged on the specified drop target, but does not drop it */ +export const dragWithoutDrop = (dropTarget: JQuery) => { + cy.wrap(dropTarget).trigger('mousemove', 'center', { + button: primaryButton, + }); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/all_hosts.ts b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/all_hosts.ts new file mode 100644 index 0000000000000..43e2a7e1bef11 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/all_hosts.ts @@ -0,0 +1,40 @@ +/* + * 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 { ALL_HOSTS_TABLE, HOSTS_NAMES } from '../../screens/hosts/all_hosts'; +import { + TIMELINE_DATA_PROVIDERS, + TIMELINE_DATA_PROVIDERS_EMPTY, +} from '../../screens/timeline/main'; +import { DEFAULT_TIMEOUT } from '../../tasks/login'; +import { drag, drop, dragWithoutDrop } from '../../tasks/common'; + +export const waitForAllHostsToBeLoaded = () => { + cy.get(ALL_HOSTS_TABLE, { timeout: DEFAULT_TIMEOUT }).should('exist'); +}; + +export const dragAndDropFirstHostToTimeline = () => { + cy.get(HOSTS_NAMES) + .first() + .then(firstHost => drag(firstHost)); + cy.get(TIMELINE_DATA_PROVIDERS).then(dataProvidersDropArea => drop(dataProvidersDropArea)); +}; + +export const dragFirstHostToTimeline = () => { + cy.get(HOSTS_NAMES) + .first() + .then(host => drag(host)); +}; + +export const dragFirstHostToEmptyTimelineDataProviders = () => { + cy.get(HOSTS_NAMES) + .first() + .then(host => drag(host)); + + cy.get(TIMELINE_DATA_PROVIDERS_EMPTY).then(dataProvidersDropArea => + dragWithoutDrop(dataProvidersDropArea) + ); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts b/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts index ae2a863092907..068b6dd9f8bd4 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts @@ -13,6 +13,8 @@ import { SERVER_SIDE_EVENT_COUNT, TIMELINE_SETTINGS_ICON, TIMELINE_INSPECT_BUTTON, + CREATE_NEW_TIMELINE, + CLOSE_TIMELINE_BTN, } from '../../screens/timeline/main'; export const hostExistsQuery = 'host.name: *'; @@ -44,3 +46,9 @@ export const openTimelineInspectButton = () => { cy.get(TIMELINE_INSPECT_BUTTON, { timeout: DEFAULT_TIMEOUT }).should('not.be.disabled'); cy.get(TIMELINE_INSPECT_BUTTON).trigger('click', { force: true }); }; + +export const createNewTimeline = () => { + cy.get(TIMELINE_SETTINGS_ICON).click({ force: true }); + cy.get(CREATE_NEW_TIMELINE).click(); + cy.get(CLOSE_TIMELINE_BTN).click({ force: true }); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts b/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts index 0437693e87e5e..164a117b82475 100644 --- a/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts +++ b/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts @@ -6,6 +6,7 @@ export const TIMELINES_PAGE = '/app/siem#/timelines'; export const OVERVIEW_PAGE = '/app/siem#/overview'; +export const HOSTS_PAGE = '/app/siem#/hosts/allHosts'; export const HOSTS_PAGE_TAB_URLS = { allHosts: '/app/siem#/hosts/allHosts', anomalies: '/app/siem#/hosts/anomalies', From 717d471a2348496994989fe9cb7dbdda55f93c2c Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 13 Feb 2020 05:52:02 -0700 Subject: [PATCH 5/9] [Maps] do not show border color for icon in legend when border width is zero (#57501) * [Maps] do not show border color for icon in legend when border width is zero * fix jest tests * fix jest tests Co-authored-by: Elastic Machine --- .../__snapshots__/vector_icon.test.js.snap | 2 +- .../vector/components/legend/symbol_icon.js | 50 +++---------------- .../vector/components/legend/vector_icon.js | 8 +-- .../__snapshots__/icon_select.test.js.snap | 6 --- .../vector/components/symbol/icon_select.js | 6 +-- .../vector/components/vector_style_editor.js | 7 +-- .../layers/styles/vector/symbol_utils.js | 8 +-- .../layers/styles/vector/symbol_utils.test.js | 13 +---- .../layers/styles/vector/vector_style.js | 25 +++++++--- 9 files changed, 39 insertions(+), 86 deletions(-) diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap index 5837a80ec3083..f7dea92a8a0b7 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap @@ -38,8 +38,8 @@ exports[`Renders PolygonIcon 1`] = ` exports[`Renders SymbolIcon 1`] = ` `; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/symbol_icon.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/symbol_icon.js index 301d64e676703..ea3886c600be9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/symbol_icon.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/symbol_icon.js @@ -12,62 +12,30 @@ import { getMakiSymbolSvg, styleSvg, buildSrcUrl } from '../../symbol_utils'; export class SymbolIcon extends Component { state = { imgDataUrl: undefined, - prevSymbolId: undefined, - prevFill: undefined, - prevStroke: undefined, - prevStrokeWidth: undefined, }; componentDidMount() { this._isMounted = true; - this._loadSymbol( - this.props.symbolId, - this.props.fill, - this.props.stroke, - this.props.strokeWidth - ); - } - - componentDidUpdate() { - this._loadSymbol( - this.props.symbolId, - this.props.fill, - this.props.stroke, - this.props.strokeWidth - ); + this._loadSymbol(); } componentWillUnmount() { this._isMounted = false; } - async _loadSymbol(nextSymbolId, nextFill, nextStroke, nextStrokeWidth) { - if ( - nextSymbolId === this.state.prevSymbolId && - nextFill === this.state.prevFill && - nextStroke === this.state.prevStroke && - nextStrokeWidth === this.state.prevStrokeWidth - ) { - return; - } - + async _loadSymbol() { let imgDataUrl; try { - const svg = getMakiSymbolSvg(nextSymbolId); - const styledSvg = await styleSvg(svg, nextFill, nextStroke, nextStrokeWidth); + const svg = getMakiSymbolSvg(this.props.symbolId); + const styledSvg = await styleSvg(svg, this.props.fill, this.props.stroke); imgDataUrl = buildSrcUrl(styledSvg); } catch (error) { // ignore failures - component will just not display an icon + return; } if (this._isMounted) { - this.setState({ - imgDataUrl, - prevSymbolId: nextSymbolId, - prevFill: nextFill, - prevStroke: nextStroke, - prevStrokeWidth: nextStrokeWidth, - }); + this.setState({ imgDataUrl }); } } @@ -80,7 +48,6 @@ export class SymbolIcon extends Component { symbolId, // eslint-disable-line no-unused-vars fill, // eslint-disable-line no-unused-vars stroke, // eslint-disable-line no-unused-vars - strokeWidth, // eslint-disable-line no-unused-vars ...rest } = this.props; @@ -98,7 +65,6 @@ export class SymbolIcon extends Component { SymbolIcon.propTypes = { symbolId: PropTypes.string.isRequired, - fill: PropTypes.string.isRequired, - stroke: PropTypes.string.isRequired, - strokeWidth: PropTypes.string.isRequired, + fill: PropTypes.string, + stroke: PropTypes.string, }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_icon.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_icon.js index 29429b5b29aff..e255dceda856e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_icon.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_icon.js @@ -37,10 +37,10 @@ export function VectorIcon({ fillColor, isPointsOnly, isLinesOnly, strokeColor, return ( ); } @@ -49,6 +49,6 @@ VectorIcon.propTypes = { fillColor: PropTypes.string, isPointsOnly: PropTypes.bool.isRequired, isLinesOnly: PropTypes.bool.isRequired, - strokeColor: PropTypes.string.isRequired, + strokeColor: PropTypes.string, symbolId: PropTypes.string, }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap index b4b7a3fcf28fa..706dc0763b7ca 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap @@ -25,8 +25,6 @@ exports[`Should render icon select 1`] = ` } @@ -53,8 +51,6 @@ exports[`Should render icon select 1`] = ` "label": "symbol1", "prepend": , "value": "symbol1", @@ -63,8 +59,6 @@ exports[`Should render icon select 1`] = ` "label": "symbol2", "prepend": , "value": "symbol2", diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_select.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_select.js index 03cd1ac14a013..68f7a30b22862 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_select.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_select.js @@ -80,11 +80,10 @@ export class IconSelect extends Component { fullWidth prepend={ } /> @@ -100,10 +99,9 @@ export class IconSelect extends Component { label, prepend: ( ), }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index 9636dab406a44..7daf85b68dd8e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -146,11 +146,6 @@ export class VectorStyleEditor extends Component { this.props.handlePropertyChange(propertyName, styleDescriptor); }; - _hasBorder() { - const width = this.props.styleProperties[VECTOR_STYLES.LINE_WIDTH]; - return width.isDynamic() ? width.isComplete() : width.getOptions().size !== 0; - } - _hasMarkerOrIcon() { const iconSize = this.props.styleProperties[VECTOR_STYLES.ICON_SIZE]; return iconSize.isDynamic() || iconSize.getOptions().size > 0; @@ -192,7 +187,7 @@ export class VectorStyleEditor extends Component { const disabledByIconSize = isPointBorderColor && !this._hasMarkerOrIcon(); return (
); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.test.js index ed59b1d5513a0..1d3b3608cb2d9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.test.js @@ -32,21 +32,12 @@ describe('styleSvg', () => { ); }); - it('Should add stroke style property to svg element', async () => { + it('Should add stroke and stroke-wdth style properties to svg element', async () => { const unstyledSvgString = ''; const styledSvg = await styleSvg(unstyledSvgString, 'red', 'white'); expect(styledSvg.split('\n')[1]).toBe( - '' - ); - }); - - it('Should add stroke-width style property to svg element', async () => { - const unstyledSvgString = - ''; - const styledSvg = await styleSvg(unstyledSvgString, 'red', 'white', '2px'); - expect(styledSvg.split('\n')[1]).toBe( - '' + '' ); }); }); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index 1f96c37c9d286..62651fdd702d6 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -138,6 +138,12 @@ export class VectorStyle extends AbstractStyle { ]; } + _hasBorder() { + return this._lineWidthStyleProperty.isDynamic() + ? this._lineWidthStyleProperty.isComplete() + : this._lineWidthStyleProperty.getOptions().size !== 0; + } + renderEditor({ layer, onStyleDescriptorChange }) { const rawProperties = this.getRawProperties(); const handlePropertyChange = (propertyName, settings) => { @@ -170,6 +176,7 @@ export class VectorStyle extends AbstractStyle { onIsTimeAwareChange={onIsTimeAwareChange} isTimeAware={this.isTimeAware()} showIsTimeAware={propertiesWithFieldMeta.length > 0} + hasBorder={this._hasBorder()} /> ); } @@ -423,12 +430,18 @@ export class VectorStyle extends AbstractStyle { getIcon = () => { const isLinesOnly = this._getIsLinesOnly(); - const strokeColor = isLinesOnly - ? extractColorFromStyleProperty(this._descriptor.properties[VECTOR_STYLES.LINE_COLOR], 'grey') - : extractColorFromStyleProperty( - this._descriptor.properties[VECTOR_STYLES.LINE_COLOR], - 'none' - ); + let strokeColor; + if (isLinesOnly) { + strokeColor = extractColorFromStyleProperty( + this._descriptor.properties[VECTOR_STYLES.LINE_COLOR], + 'grey' + ); + } else if (this._hasBorder()) { + strokeColor = extractColorFromStyleProperty( + this._descriptor.properties[VECTOR_STYLES.LINE_COLOR], + 'none' + ); + } const fillColor = isLinesOnly ? null : extractColorFromStyleProperty( From 4437858fe33da7b35a072fca1d942c3640ef3d32 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 13 Feb 2020 13:54:51 +0100 Subject: [PATCH 6/9] Lens client side shim cleanup (#56976) --- .../plugins/lens/public/app_plugin/index.ts | 2 +- .../_index.scss | 0 .../_visualization.scss | 0 .../expression.tsx | 0 .../public/datatable_visualization/index.ts | 32 + .../visualization.test.tsx | 2 +- .../visualization.tsx | 0 .../datatable_visualization_plugin/plugin.tsx | 49 - .../lens/public/editor_frame_plugin/index.ts | 7 - .../_index.scss | 0 .../__mocks__/suggestion_helpers.ts | 0 .../editor_frame/_chart_switch.scss | 0 .../editor_frame/_data_panel_wrapper.scss | 0 .../editor_frame/_expression_renderer.scss | 0 .../editor_frame/_frame_layout.scss | 0 .../editor_frame/_suggestion_panel.scss | 0 .../_workspace_panel_wrapper.scss | 0 .../editor_frame/chart_switch.test.tsx | 0 .../editor_frame/chart_switch.tsx | 0 .../editor_frame/config_panel_wrapper.tsx | 0 .../editor_frame/data_panel_wrapper.tsx | 0 .../editor_frame/editor_frame.test.tsx | 0 .../editor_frame/editor_frame.tsx | 0 .../editor_frame/expression_helpers.ts | 0 .../editor_frame/frame_layout.tsx | 0 .../editor_frame/index.scss | 0 .../editor_frame/index.ts | 0 .../editor_frame/layer_actions.test.ts | 0 .../editor_frame/layer_actions.ts | 0 .../editor_frame/save.test.ts | 0 .../editor_frame/save.ts | 0 .../editor_frame/state_management.test.ts | 0 .../editor_frame/state_management.ts | 0 .../editor_frame/suggestion_helpers.test.ts | 0 .../editor_frame/suggestion_helpers.ts | 0 .../editor_frame/suggestion_panel.test.tsx | 0 .../editor_frame/suggestion_panel.tsx | 0 .../editor_frame/workspace_panel.test.tsx | 0 .../editor_frame/workspace_panel.tsx | 0 .../editor_frame/workspace_panel_wrapper.tsx | 0 .../embeddable/embeddable.test.tsx | 0 .../embeddable/embeddable.tsx | 0 .../embeddable/embeddable_factory.ts | 31 +- .../embeddable/expression_wrapper.tsx | 0 .../index.ts | 2 +- .../merge_tables.test.ts | 0 .../merge_tables.ts | 2 +- .../mocks.tsx | 7 +- .../service.test.tsx} | 42 +- .../service.tsx} | 67 +- x-pack/legacy/plugins/lens/public/index.scss | 10 +- x-pack/legacy/plugins/lens/public/index.ts | 4 + .../__mocks__/loader.ts | 0 .../__mocks__/state_helpers.ts | 0 .../lens_field_icon.test.tsx.snap | 0 .../_datapanel.scss | 0 .../_field_item.scss | 0 .../_index.scss | 0 .../auto_date.test.ts | 0 .../auto_date.ts | 0 .../change_indexpattern.tsx | 0 .../datapanel.test.tsx | 0 .../datapanel.tsx | 0 .../dimension_panel/_dimension_panel.scss | 0 .../dimension_panel/_field_select.scss | 0 .../dimension_panel/_index.scss | 0 .../dimension_panel/_popover.scss | 0 .../bucket_nesting_editor.test.tsx | 0 .../dimension_panel/bucket_nesting_editor.tsx | 0 .../dimension_panel/dimension_panel.test.tsx | 0 .../dimension_panel/dimension_panel.tsx | 0 .../dimension_panel/field_select.tsx | 0 .../dimension_panel/index.ts | 0 .../dimension_panel/popover_editor.tsx | 0 .../document_field.ts | 0 .../field_icon.test.tsx | 0 .../field_icon.tsx | 0 .../field_item.test.tsx | 0 .../field_item.tsx | 0 .../public/indexpattern_datasource/index.ts | 49 + .../indexpattern.test.ts | 6 - .../indexpattern.tsx | 15 +- .../indexpattern_suggestions.test.tsx | 0 .../indexpattern_suggestions.ts | 0 .../layerpanel.test.tsx | 0 .../layerpanel.tsx | 0 .../lens_field_icon.test.tsx | 0 .../lens_field_icon.tsx | 0 .../loader.test.ts | 0 .../loader.ts | 1 - .../mocks.ts | 0 .../operations/__mocks__/index.ts | 0 .../operations/definitions/cardinality.tsx | 0 .../operations/definitions/column_types.ts | 0 .../operations/definitions/count.tsx | 0 .../definitions/date_histogram.test.tsx | 0 .../operations/definitions/date_histogram.tsx | 0 .../operations/definitions/index.ts | 0 .../operations/definitions/metrics.tsx | 0 .../operations/definitions/terms.test.tsx | 0 .../operations/definitions/terms.tsx | 0 .../operations/index.ts | 0 .../operations/operations.test.ts | 0 .../operations/operations.ts | 0 .../pure_helpers.test.ts | 0 .../pure_helpers.ts | 0 .../rename_columns.test.ts | 0 .../rename_columns.ts | 0 .../state_helpers.test.ts | 0 .../state_helpers.ts | 0 .../to_expression.ts | 0 .../types.ts | 0 .../utils.ts | 0 .../public/indexpattern_plugin/plugin.tsx | 53 - x-pack/legacy/plugins/lens/public/legacy.ts | 13 +- .../index.ts => legacy_imports.ts} | 2 +- .../auto_scale.test.tsx | 0 .../auto_scale.tsx | 0 .../index.scss | 0 .../lens/public/metric_visualization/index.ts | 33 + .../metric_config_panel.test.tsx | 2 +- .../metric_config_panel.tsx | 0 .../metric_expression.test.tsx | 0 .../metric_expression.tsx | 0 .../metric_suggestions.test.ts | 0 .../metric_suggestions.ts | 0 .../metric_visualization.test.ts | 2 +- .../metric_visualization.tsx | 0 .../types.ts | 0 .../metric_visualization_plugin/index.ts | 7 - .../metric_visualization_plugin/plugin.tsx | 50 - .../multi_column_editor.test.tsx | 2 +- .../lens/public/{app_plugin => }/plugin.tsx | 159 +- x-pack/legacy/plugins/lens/public/types.ts | 8 +- .../__snapshots__/xy_expression.test.tsx.snap | 482 ++++++ .../xy_visualization.test.ts.snap | 0 .../_index.scss | 0 .../_xy_expression.scss | 0 .../plugin.tsx => xy_visualization/index.ts} | 45 +- .../state_helpers.ts | 0 .../to_expression.ts | 0 .../types.ts | 0 .../xy_config_panel.test.tsx | 2 +- .../xy_config_panel.tsx | 0 .../xy_expression.test.tsx | 42 +- .../xy_expression.tsx | 12 +- .../xy_suggestions.test.ts | 0 .../xy_suggestions.ts | 0 .../xy_visualization.test.ts | 2 +- .../xy_visualization.tsx | 0 .../__snapshots__/xy_expression.test.tsx.snap | 1315 ----------------- .../public/xy_visualization_plugin/index.ts | 7 - 152 files changed, 835 insertions(+), 1731 deletions(-) rename x-pack/legacy/plugins/lens/public/{datatable_visualization_plugin => datatable_visualization}/_index.scss (100%) rename x-pack/legacy/plugins/lens/public/{datatable_visualization_plugin => datatable_visualization}/_visualization.scss (100%) rename x-pack/legacy/plugins/lens/public/{datatable_visualization_plugin => datatable_visualization}/expression.tsx (100%) create mode 100644 x-pack/legacy/plugins/lens/public/datatable_visualization/index.ts rename x-pack/legacy/plugins/lens/public/{datatable_visualization_plugin => datatable_visualization}/visualization.test.tsx (99%) rename x-pack/legacy/plugins/lens/public/{datatable_visualization_plugin => datatable_visualization}/visualization.tsx (100%) delete mode 100644 x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/plugin.tsx delete mode 100644 x-pack/legacy/plugins/lens/public/editor_frame_plugin/index.ts rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/_index.scss (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/__mocks__/suggestion_helpers.ts (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/_chart_switch.scss (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/_data_panel_wrapper.scss (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/_expression_renderer.scss (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/_frame_layout.scss (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/_suggestion_panel.scss (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/_workspace_panel_wrapper.scss (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/chart_switch.test.tsx (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/chart_switch.tsx (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/config_panel_wrapper.tsx (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/data_panel_wrapper.tsx (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/editor_frame.test.tsx (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/editor_frame.tsx (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/expression_helpers.ts (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/frame_layout.tsx (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/index.scss (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/index.ts (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/layer_actions.test.ts (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/layer_actions.ts (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/save.test.ts (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/save.ts (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/state_management.test.ts (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/state_management.ts (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/suggestion_helpers.test.ts (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/suggestion_helpers.ts (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/suggestion_panel.test.tsx (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/suggestion_panel.tsx (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/workspace_panel.test.tsx (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/workspace_panel.tsx (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/editor_frame/workspace_panel_wrapper.tsx (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/embeddable/embeddable.test.tsx (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/embeddable/embeddable.tsx (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/embeddable/embeddable_factory.ts (78%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/embeddable/expression_wrapper.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => editor_frame_service}/index.ts (90%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/merge_tables.test.ts (100%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/merge_tables.ts (96%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin => editor_frame_service}/mocks.tsx (95%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin/plugin.test.tsx => editor_frame_service/service.test.tsx} (66%) rename x-pack/legacy/plugins/lens/public/{editor_frame_plugin/plugin.tsx => editor_frame_service/service.tsx} (70%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/__mocks__/loader.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/__mocks__/state_helpers.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/__snapshots__/lens_field_icon.test.tsx.snap (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/_datapanel.scss (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/_field_item.scss (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/_index.scss (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/auto_date.test.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/auto_date.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/change_indexpattern.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/datapanel.test.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/datapanel.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/dimension_panel/_dimension_panel.scss (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/dimension_panel/_field_select.scss (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/dimension_panel/_index.scss (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/dimension_panel/_popover.scss (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/dimension_panel/bucket_nesting_editor.test.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/dimension_panel/bucket_nesting_editor.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/dimension_panel/dimension_panel.test.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/dimension_panel/dimension_panel.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/dimension_panel/field_select.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/dimension_panel/index.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/dimension_panel/popover_editor.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/document_field.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/field_icon.test.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/field_icon.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/field_item.test.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/field_item.tsx (100%) create mode 100644 x-pack/legacy/plugins/lens/public/indexpattern_datasource/index.ts rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/indexpattern.test.ts (97%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/indexpattern.tsx (94%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/indexpattern_suggestions.test.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/indexpattern_suggestions.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/layerpanel.test.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/layerpanel.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/lens_field_icon.test.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/lens_field_icon.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/loader.test.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/loader.ts (99%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/mocks.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/operations/__mocks__/index.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/operations/definitions/cardinality.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/operations/definitions/column_types.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/operations/definitions/count.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/operations/definitions/date_histogram.test.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/operations/definitions/date_histogram.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/operations/definitions/index.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/operations/definitions/metrics.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/operations/definitions/terms.test.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/operations/definitions/terms.tsx (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/operations/index.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/operations/operations.test.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/operations/operations.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/pure_helpers.test.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/pure_helpers.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/rename_columns.test.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/rename_columns.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/state_helpers.test.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/state_helpers.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/to_expression.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/types.ts (100%) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => indexpattern_datasource}/utils.ts (100%) delete mode 100644 x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx rename x-pack/legacy/plugins/lens/public/{datatable_visualization_plugin/index.ts => legacy_imports.ts} (72%) rename x-pack/legacy/plugins/lens/public/{metric_visualization_plugin => metric_visualization}/auto_scale.test.tsx (100%) rename x-pack/legacy/plugins/lens/public/{metric_visualization_plugin => metric_visualization}/auto_scale.tsx (100%) rename x-pack/legacy/plugins/lens/public/{metric_visualization_plugin => metric_visualization}/index.scss (100%) create mode 100644 x-pack/legacy/plugins/lens/public/metric_visualization/index.ts rename x-pack/legacy/plugins/lens/public/{metric_visualization_plugin => metric_visualization}/metric_config_panel.test.tsx (98%) rename x-pack/legacy/plugins/lens/public/{metric_visualization_plugin => metric_visualization}/metric_config_panel.tsx (100%) rename x-pack/legacy/plugins/lens/public/{metric_visualization_plugin => metric_visualization}/metric_expression.test.tsx (100%) rename x-pack/legacy/plugins/lens/public/{metric_visualization_plugin => metric_visualization}/metric_expression.tsx (100%) rename x-pack/legacy/plugins/lens/public/{metric_visualization_plugin => metric_visualization}/metric_suggestions.test.ts (100%) rename x-pack/legacy/plugins/lens/public/{metric_visualization_plugin => metric_visualization}/metric_suggestions.ts (100%) rename x-pack/legacy/plugins/lens/public/{metric_visualization_plugin => metric_visualization}/metric_visualization.test.ts (99%) rename x-pack/legacy/plugins/lens/public/{metric_visualization_plugin => metric_visualization}/metric_visualization.tsx (100%) rename x-pack/legacy/plugins/lens/public/{metric_visualization_plugin => metric_visualization}/types.ts (100%) delete mode 100644 x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.ts delete mode 100644 x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx rename x-pack/legacy/plugins/lens/public/{app_plugin => }/plugin.tsx (55%) create mode 100644 x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap rename x-pack/legacy/plugins/lens/public/{xy_visualization_plugin => xy_visualization}/__snapshots__/xy_visualization.test.ts.snap (100%) rename x-pack/legacy/plugins/lens/public/{xy_visualization_plugin => xy_visualization}/_index.scss (100%) rename x-pack/legacy/plugins/lens/public/{xy_visualization_plugin => xy_visualization}/_xy_expression.scss (100%) rename x-pack/legacy/plugins/lens/public/{xy_visualization_plugin/plugin.tsx => xy_visualization/index.ts} (57%) rename x-pack/legacy/plugins/lens/public/{xy_visualization_plugin => xy_visualization}/state_helpers.ts (100%) rename x-pack/legacy/plugins/lens/public/{xy_visualization_plugin => xy_visualization}/to_expression.ts (100%) rename x-pack/legacy/plugins/lens/public/{xy_visualization_plugin => xy_visualization}/types.ts (100%) rename x-pack/legacy/plugins/lens/public/{xy_visualization_plugin => xy_visualization}/xy_config_panel.test.tsx (99%) rename x-pack/legacy/plugins/lens/public/{xy_visualization_plugin => xy_visualization}/xy_config_panel.tsx (100%) rename x-pack/legacy/plugins/lens/public/{xy_visualization_plugin => xy_visualization}/xy_expression.test.tsx (93%) rename x-pack/legacy/plugins/lens/public/{xy_visualization_plugin => xy_visualization}/xy_expression.tsx (94%) rename x-pack/legacy/plugins/lens/public/{xy_visualization_plugin => xy_visualization}/xy_suggestions.test.ts (100%) rename x-pack/legacy/plugins/lens/public/{xy_visualization_plugin => xy_visualization}/xy_suggestions.ts (100%) rename x-pack/legacy/plugins/lens/public/{xy_visualization_plugin => xy_visualization}/xy_visualization.test.ts (99%) rename x-pack/legacy/plugins/lens/public/{xy_visualization_plugin => xy_visualization}/xy_visualization.tsx (100%) delete mode 100644 x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap delete mode 100644 x-pack/legacy/plugins/lens/public/xy_visualization_plugin/index.ts diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/index.ts b/x-pack/legacy/plugins/lens/public/app_plugin/index.ts index f75dce9b7507f..1460fdfef37e6 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/index.ts +++ b/x-pack/legacy/plugins/lens/public/app_plugin/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './plugin'; +export * from './app'; diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/_index.scss b/x-pack/legacy/plugins/lens/public/datatable_visualization/_index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/_index.scss rename to x-pack/legacy/plugins/lens/public/datatable_visualization/_index.scss diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/_visualization.scss b/x-pack/legacy/plugins/lens/public/datatable_visualization/_visualization.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/_visualization.scss rename to x-pack/legacy/plugins/lens/public/datatable_visualization/_visualization.scss diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/expression.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx rename to x-pack/legacy/plugins/lens/public/datatable_visualization/expression.tsx diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/index.ts b/x-pack/legacy/plugins/lens/public/datatable_visualization/index.ts new file mode 100644 index 0000000000000..6dee47cc632c2 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/index.ts @@ -0,0 +1,32 @@ +/* + * 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 { CoreSetup } from 'src/core/public'; +import { datatableVisualization } from './visualization'; +import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; +import { datatable, datatableColumns, getDatatableRenderer } from './expression'; +import { FormatFactory } from '../legacy_imports'; +import { EditorFrameSetup } from '../types'; + +export interface DatatableVisualizationPluginSetupPlugins { + expressions: ExpressionsSetup; + formatFactory: FormatFactory; + editorFrame: EditorFrameSetup; +} + +export class DatatableVisualization { + constructor() {} + + setup( + _core: CoreSetup | null, + { expressions, formatFactory, editorFrame }: DatatableVisualizationPluginSetupPlugins + ) { + expressions.registerFunction(() => datatableColumns); + expressions.registerFunction(() => datatable); + expressions.registerRenderer(() => getDatatableRenderer(formatFactory)); + editorFrame.registerVisualization(datatableVisualization); + } +} diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx rename to x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx index cb9350226575c..0cba22170df1f 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { createMockDatasource } from '../editor_frame_plugin/mocks'; +import { createMockDatasource } from '../editor_frame_service/mocks'; import { DatatableVisualizationState, datatableVisualization, diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx rename to x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/plugin.tsx deleted file mode 100644 index ed047f52ecc0f..0000000000000 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/plugin.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { npSetup } from 'ui/new_platform'; -import { CoreSetup } from 'src/core/public'; -import { getFormat, FormatFactory } from 'ui/visualize/loader/pipeline_helpers/utilities'; -import { datatableVisualization } from './visualization'; -import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; -import { datatable, datatableColumns, getDatatableRenderer } from './expression'; - -export interface DatatableVisualizationPluginSetupPlugins { - expressions: ExpressionsSetup; - // TODO this is a simulated NP plugin. - // Once field formatters are actually migrated, the actual shim can be used - fieldFormat: { - formatFactory: FormatFactory; - }; -} - -class DatatableVisualizationPlugin { - constructor() {} - - setup( - _core: CoreSetup | null, - { expressions, fieldFormat }: DatatableVisualizationPluginSetupPlugins - ) { - expressions.registerFunction(() => datatableColumns); - expressions.registerFunction(() => datatable); - expressions.registerRenderer(() => getDatatableRenderer(fieldFormat.formatFactory)); - - return datatableVisualization; - } - - stop() {} -} - -const plugin = new DatatableVisualizationPlugin(); - -export const datatableVisualizationSetup = () => - plugin.setup(npSetup.core, { - expressions: npSetup.plugins.expressions, - fieldFormat: { - formatFactory: getFormat, - }, - }); -export const datatableVisualizationStop = () => plugin.stop(); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/index.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/index.ts deleted file mode 100644 index f75dce9b7507f..0000000000000 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export * from './plugin'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/_index.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/_index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/_index.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/_index.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/__mocks__/suggestion_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/suggestion_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/__mocks__/suggestion_helpers.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/suggestion_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_chart_switch.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_chart_switch.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_data_panel_wrapper.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_data_panel_wrapper.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_data_panel_wrapper.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_data_panel_wrapper.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_expression_renderer.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_expression_renderer.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_expression_renderer.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_expression_renderer.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_frame_layout.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_frame_layout.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_frame_layout.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_frame_layout.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_suggestion_panel.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_suggestion_panel.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_suggestion_panel.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_suggestion_panel.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_workspace_panel_wrapper.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_workspace_panel_wrapper.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.test.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/frame_layout.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/frame_layout.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/layer_actions.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/layer_actions.test.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/layer_actions.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/layer_actions.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel_wrapper.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable_factory.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts similarity index 78% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable_factory.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index 00cde2ee3e04c..e8bb8914fa292 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable_factory.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; -import { Chrome } from 'ui/chrome'; - -import { capabilities } from 'ui/capabilities'; +import { + Capabilities, + HttpSetup, + RecursiveReadonly, + SavedObjectsClientContract, +} from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { IndexPatternsContract, IndexPattern } from '../../../../../../../src/plugins/data/public'; import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; @@ -24,14 +26,12 @@ import { getEditPath } from '../../../../../../plugins/lens/common'; export class EmbeddableFactory extends AbstractEmbeddableFactory { type = DOC_TYPE; - private chrome: Chrome; - private indexPatternService: IndexPatternsContract; - private expressionRenderer: ReactExpressionRendererType; - constructor( - chrome: Chrome, - expressionRenderer: ReactExpressionRendererType, - indexPatternService: IndexPatternsContract + private coreHttp: HttpSetup, + private capabilities: RecursiveReadonly, + private savedObjectsClient: SavedObjectsClientContract, + private expressionRenderer: ReactExpressionRendererType, + private indexPatternService: IndexPatternsContract ) { super({ savedObjectMetaData: { @@ -42,13 +42,10 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { getIconForSavedObject: () => 'lensApp', }, }); - this.chrome = chrome; - this.expressionRenderer = expressionRenderer; - this.indexPatternService = indexPatternService; } public isEditable() { - return capabilities.get().visualize.save as boolean; + return this.capabilities.visualize.save as boolean; } canCreateNew() { @@ -66,7 +63,7 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { input: Partial & { id: string }, parent?: IContainer ) { - const store = new SavedObjectIndexStore(this.chrome.getSavedObjectsClient()); + const store = new SavedObjectIndexStore(this.savedObjectsClient); const savedVis = await store.load(savedObjectId); const promises = savedVis.state.datasourceMetaData.filterableIndexPatterns.map( @@ -91,7 +88,7 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { this.expressionRenderer, { savedVis, - editUrl: this.chrome.addBasePath(getEditPath(savedObjectId)), + editUrl: this.coreHttp.basePath.prepend(getEditPath(savedObjectId)), editable: this.isEditable(), indexPatterns, }, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/index.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/index.ts similarity index 90% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/index.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/index.ts index f75dce9b7507f..d6e96d74b766c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/index.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './plugin'; +export * from './service'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.ts similarity index 96% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.ts index 3c466522e1ebe..c5be5f524755d 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.ts @@ -11,7 +11,7 @@ import { KibanaDatatable, } from 'src/plugins/expressions/public'; import { LensMultiTable } from '../types'; -import { toAbsoluteDates } from '../indexpattern_plugin/auto_date'; +import { toAbsoluteDates } from '../indexpattern_datasource/auto_date'; interface MergeTables { layerIds: string[]; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx similarity index 95% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx index b4fc88cb074c7..cd121a1f96a2b 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx @@ -10,12 +10,10 @@ import { ExpressionsSetup, ExpressionsStart, } from '../../../../../../src/plugins/expressions/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { embeddablePluginMock } from '../../../../../../src/plugins/embeddable/public/mocks'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks'; import { DatasourcePublicAPI, FramePublicAPI, Datasource, Visualization } from '../types'; -import { EditorFrameSetupPlugins, EditorFrameStartPlugins } from './plugin'; +import { EditorFrameSetupPlugins, EditorFrameStartPlugins } from './service'; export function createMockVisualization(): jest.Mocked { return { @@ -108,9 +106,6 @@ export function createMockSetupDependencies() { data: {}, embeddable: embeddablePluginMock.createSetupContract(), expressions: expressionsPluginMock.createSetupContract(), - chrome: { - getSavedObjectsClient: () => {}, - }, } as unknown) as MockedSetupDependencies; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx similarity index 66% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx index 7a6067dd5f23c..ef4b5f6d7b834 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EditorFramePlugin } from './plugin'; +import { EditorFrameService } from './service'; import { coreMock } from 'src/core/public/mocks'; import { MockedSetupDependencies, @@ -25,14 +25,14 @@ jest.mock('./embeddable/embeddable_factory', () => ({ EmbeddableFactory: class Mock {}, })); -describe('editor_frame plugin', () => { - let pluginInstance: EditorFramePlugin; +describe('editor_frame service', () => { + let pluginInstance: EditorFrameService; let mountpoint: Element; let pluginSetupDependencies: MockedSetupDependencies; let pluginStartDependencies: MockedStartDependencies; beforeEach(() => { - pluginInstance = new EditorFramePlugin(); + pluginInstance = new EditorFrameService(); mountpoint = document.createElement('div'); pluginSetupDependencies = createMockSetupDependencies(); pluginStartDependencies = createMockStartDependencies(); @@ -42,26 +42,28 @@ describe('editor_frame plugin', () => { mountpoint.remove(); }); - it('should create an editor frame instance which mounts and unmounts', () => { - expect(() => { - pluginInstance.setup(coreMock.createSetup(), pluginSetupDependencies); - const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); - const instance = publicAPI.createInstance({}); - instance.mount(mountpoint, { - onError: jest.fn(), - onChange: jest.fn(), - dateRange: { fromDate: '', toDate: '' }, - query: { query: '', language: 'lucene' }, - filters: [], - }); - instance.unmount(); - }).not.toThrowError(); + it('should create an editor frame instance which mounts and unmounts', async () => { + await expect( + (async () => { + pluginInstance.setup(coreMock.createSetup(), pluginSetupDependencies); + const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); + const instance = await publicAPI.createInstance({}); + instance.mount(mountpoint, { + onError: jest.fn(), + onChange: jest.fn(), + dateRange: { fromDate: '', toDate: '' }, + query: { query: '', language: 'lucene' }, + filters: [], + }); + instance.unmount(); + })() + ).resolves.toBeUndefined(); }); - it('should not have child nodes after unmount', () => { + it('should not have child nodes after unmount', async () => { pluginInstance.setup(coreMock.createSetup(), pluginSetupDependencies); const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); - const instance = publicAPI.createInstance({}); + const instance = await publicAPI.createInstance({}); instance.mount(mountpoint, { onError: jest.fn(), onChange: jest.fn(), diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx similarity index 70% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx index e914eb7d7784b..9a3d724705a1a 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx @@ -8,8 +8,6 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { CoreSetup, CoreStart } from 'src/core/public'; -import chrome, { Chrome } from 'ui/chrome'; -import { npSetup, npStart } from 'ui/new_platform'; import { ExpressionsSetup, ExpressionsStart, @@ -44,24 +42,35 @@ export interface EditorFrameStartPlugins { data: DataPublicPluginStart; embeddable: IEmbeddableStart; expressions: ExpressionsStart; - chrome: Chrome; } -export class EditorFramePlugin { +async function collectAsyncDefinitions( + definitions: Array> +) { + const resolvedDefinitions = await Promise.all(definitions); + const definitionMap: Record = {}; + resolvedDefinitions.forEach(definition => { + definitionMap[definition.id] = definition; + }); + + return definitionMap; +} + +export class EditorFrameService { constructor() {} - private readonly datasources: Record = {}; - private readonly visualizations: Record = {}; + private readonly datasources: Array> = []; + private readonly visualizations: Array> = []; public setup(core: CoreSetup, plugins: EditorFrameSetupPlugins): EditorFrameSetup { plugins.expressions.registerFunction(() => mergeTables); return { registerDatasource: datasource => { - this.datasources[datasource.id] = datasource as Datasource; + this.datasources.push(datasource as Datasource); }, registerVisualization: visualization => { - this.visualizations[visualization.id] = visualization as Visualization; + this.visualizations.push(visualization as Visualization); }, }; } @@ -70,27 +79,34 @@ export class EditorFramePlugin { plugins.embeddable.registerEmbeddableFactory( 'lens', new EmbeddableFactory( - plugins.chrome, + core.http, + core.application.capabilities, + core.savedObjects.client, plugins.expressions.ReactExpressionRenderer, plugins.data.indexPatterns ) ); - const createInstance = (): EditorFrameInstance => { + const createInstance = async (): Promise => { let domElement: Element; + const [resolvedDatasources, resolvedVisualizations] = await Promise.all([ + collectAsyncDefinitions(this.datasources), + collectAsyncDefinitions(this.visualizations), + ]); + return { mount: (element, { doc, onError, dateRange, query, filters, savedQuery, onChange }) => { domElement = element; - const firstDatasourceId = Object.keys(this.datasources)[0]; - const firstVisualizationId = Object.keys(this.visualizations)[0]; + const firstDatasourceId = Object.keys(resolvedDatasources)[0]; + const firstVisualizationId = Object.keys(resolvedVisualizations)[0]; render( - editorFrame.setup(npSetup.core, { - data: npSetup.plugins.data, - embeddable: npSetup.plugins.embeddable, - expressions: npSetup.plugins.expressions, - }); - -export const editorFrameStart = () => - editorFrame.start(npStart.core, { - data: npStart.plugins.data, - embeddable: npStart.plugins.embeddable, - expressions: npStart.plugins.expressions, - chrome, - }); - -export const editorFrameStop = () => editorFrame.stop(); diff --git a/x-pack/legacy/plugins/lens/public/index.scss b/x-pack/legacy/plugins/lens/public/index.scss index f646b1ed0a9ae..496573f6a1c9a 100644 --- a/x-pack/legacy/plugins/lens/public/index.scss +++ b/x-pack/legacy/plugins/lens/public/index.scss @@ -7,9 +7,9 @@ @import './config_panel'; @import './app_plugin/index'; -@import './datatable_visualization_plugin/index'; +@import 'datatable_visualization/index'; @import './drag_drop/index'; -@import './editor_frame_plugin/index'; -@import './indexpattern_plugin/index'; -@import './xy_visualization_plugin/index'; -@import './metric_visualization_plugin/index'; +@import 'editor_frame_service/index'; +@import 'indexpattern_datasource/index'; +@import 'xy_visualization/index'; +@import 'metric_visualization/index'; diff --git a/x-pack/legacy/plugins/lens/public/index.ts b/x-pack/legacy/plugins/lens/public/index.ts index 9f4141dbcae7d..e49f648906af0 100644 --- a/x-pack/legacy/plugins/lens/public/index.ts +++ b/x-pack/legacy/plugins/lens/public/index.ts @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LensPlugin } from './plugin'; + export * from './types'; + +export const plugin = () => new LensPlugin(); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/loader.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/loader.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/state_helpers.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/state_helpers.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/__snapshots__/lens_field_icon.test.tsx.snap b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/__snapshots__/lens_field_icon.test.tsx.snap rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_datapanel.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/_datapanel.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_field_item.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/_field_item.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_index.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/_index.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/_index.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_dimension_panel.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_dimension_panel.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_field_select.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_field_select.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_field_select.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_field_select.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_index.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_index.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_popover.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_popover.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/index.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/document_field.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/document_field.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/document_field.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/document_field.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/index.ts new file mode 100644 index 0000000000000..3ca6e3e1ef56e --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/index.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'src/core/public'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; +import { getIndexPatternDatasource } from './indexpattern'; +import { renameColumns } from './rename_columns'; +import { autoDate } from './auto_date'; +import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; +import { + DataPublicPluginSetup, + DataPublicPluginStart, +} from '../../../../../../src/plugins/data/public'; +import { Datasource, EditorFrameSetup } from '../types'; + +export interface IndexPatternDatasourceSetupPlugins { + expressions: ExpressionsSetup; + data: DataPublicPluginSetup; + editorFrame: EditorFrameSetup; +} + +export interface IndexPatternDatasourceStartPlugins { + data: DataPublicPluginStart; +} + +export class IndexPatternDatasource { + constructor() {} + + setup( + core: CoreSetup, + { expressions, editorFrame }: IndexPatternDatasourceSetupPlugins + ) { + expressions.registerFunction(renameColumns); + expressions.registerFunction(autoDate); + + editorFrame.registerDatasource( + core.getStartServices().then(([coreStart, { data }]) => + getIndexPatternDatasource({ + core: coreStart, + storage: new Storage(localStorage), + data, + }) + ) as Promise + ); + } +} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts similarity index 97% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index e7def3b9dbf2c..41be22f2c72ed 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import chromeMock from 'ui/chrome'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { SavedObjectsClientContract } from 'kibana/public'; import { getIndexPatternDatasource, IndexPatternColumn, uniqueLabels } from './indexpattern'; import { DatasourcePublicAPI, Operation, Datasource } from '../types'; import { coreMock } from 'src/core/public/mocks'; @@ -15,8 +13,6 @@ import { IndexPatternPersistedState, IndexPatternPrivateState } from './types'; jest.mock('./loader'); jest.mock('../id_generator'); -// chrome, notify, storage are used by ./plugin -jest.mock('ui/chrome'); // Contains old and new platform data plugins, used for interpreter and filter ratio jest.mock('ui/new_platform'); @@ -142,10 +138,8 @@ describe('IndexPattern Data Source', () => { beforeEach(() => { indexPatternDatasource = getIndexPatternDatasource({ - chrome: chromeMock, storage: {} as IStorageWrapper, core: coreMock.createStart(), - savedObjectsClient: {} as SavedObjectsClientContract, data: pluginsMock.createStart().data, }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx similarity index 94% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 2426d7fc14b5d..afb88d1af7951 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -8,7 +8,7 @@ import _ from 'lodash'; import React from 'react'; import { render } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { CoreStart, SavedObjectsClientContract } from 'src/core/public'; +import { CoreStart } from 'src/core/public'; import { i18n } from '@kbn/i18n'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { @@ -21,7 +21,6 @@ import { import { loadInitialState, changeIndexPattern, changeLayerIndexPattern } from './loader'; import { toExpression } from './to_expression'; import { IndexPatternDimensionPanel } from './dimension_panel'; -import { IndexPatternDatasourceSetupPlugins } from './plugin'; import { IndexPatternDataPanel } from './datapanel'; import { getDatasourceSuggestionsForField, @@ -90,20 +89,16 @@ export function uniqueLabels(layers: Record) { } export function getIndexPatternDatasource({ - chrome, core, storage, - savedObjectsClient, data, -}: Pick & { - // Core start is being required here because it contains the savedObject client - // In the new platform, this plugin wouldn't be initialized until after setup +}: { core: CoreStart; storage: IStorageWrapper; - savedObjectsClient: SavedObjectsClientContract; data: ReturnType; }) { - const uiSettings = chrome.getUiSettingsClient(); + const savedObjectsClient = core.savedObjects.client; + const uiSettings = core.uiSettings; const onIndexPatternLoadError = (err: Error) => core.notifications.toasts.addError(err, { title: i18n.translate('xpack.lens.indexPattern.indexPatternLoadError', { @@ -118,7 +113,7 @@ export function getIndexPatternDatasource({ async initialize(state?: IndexPatternPersistedState) { return loadInitialState({ state, - savedObjectsClient, + savedObjectsClient: await savedObjectsClient, defaultIndexPatternId: core.uiSettings.get('defaultIndex'), }); }, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts index 3ec4b4f4df2ce..ed3d8a91b366d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts @@ -5,7 +5,6 @@ */ import _ from 'lodash'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { SavedObjectsClientContract, SavedObjectAttributes, HttpSetup } from 'src/core/public'; import { SimpleSavedObject } from 'src/core/public'; import { StateSetter } from '../types'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/mocks.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/mocks.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/mocks.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/mocks.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/__mocks__/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/__mocks__/index.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/cardinality.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/cardinality.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/column_types.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/column_types.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/count.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/count.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/metrics.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/metrics.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/index.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/pure_helpers.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/pure_helpers.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/pure_helpers.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/pure_helpers.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/to_expression.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/to_expression.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/types.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/types.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/types.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/types.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/utils.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/utils.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/utils.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/utils.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx deleted file mode 100644 index 11bc52fc48378..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx +++ /dev/null @@ -1,53 +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 { CoreSetup } from 'src/core/public'; -// The following dependencies on ui/* and src/legacy/core_plugins must be mocked when testing -import chrome, { Chrome } from 'ui/chrome'; -import { npSetup, npStart } from 'ui/new_platform'; -import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; -import { getIndexPatternDatasource } from './indexpattern'; -import { renameColumns } from './rename_columns'; -import { autoDate } from './auto_date'; -import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; - -// TODO these are intermediary types because interpreter is not typed yet -// They can get replaced by references to the real interfaces as soon as they -// are available - -export interface IndexPatternDatasourceSetupPlugins { - chrome: Chrome; - expressions: ExpressionsSetup; -} - -class IndexPatternDatasourcePlugin { - constructor() {} - - setup(core: CoreSetup, { expressions }: IndexPatternDatasourceSetupPlugins) { - expressions.registerFunction(renameColumns); - expressions.registerFunction(autoDate); - } - - stop() {} -} - -const plugin = new IndexPatternDatasourcePlugin(); - -export const indexPatternDatasourceSetup = () => { - plugin.setup(npSetup.core, { - chrome, - expressions: npSetup.plugins.expressions, - }); - - return getIndexPatternDatasource({ - core: npStart.core, - chrome, - storage: new Storage(localStorage), - savedObjectsClient: chrome.getSavedObjectsClient(), - data: npStart.plugins.data, - }); -}; -export const indexPatternDatasourceStop = () => plugin.stop(); diff --git a/x-pack/legacy/plugins/lens/public/legacy.ts b/x-pack/legacy/plugins/lens/public/legacy.ts index a39d73f187ece..8023bad34de66 100644 --- a/x-pack/legacy/plugins/lens/public/legacy.ts +++ b/x-pack/legacy/plugins/lens/public/legacy.ts @@ -5,15 +5,12 @@ */ import { npSetup, npStart } from 'ui/new_platform'; -import { start as dataShimStart } from '../../../../../src/legacy/core_plugins/data/public/legacy'; +import { getFormat } from './legacy_imports'; export * from './types'; -import { AppPlugin } from './app_plugin'; +import { plugin } from './index'; -const app = new AppPlugin(); -app.setup(npSetup.core, npSetup.plugins); -app.start(npStart.core, { - ...npStart.plugins, - dataShim: dataShimStart, -}); +const pluginInstance = plugin(); +pluginInstance.setup(npSetup.core, { ...npSetup.plugins, __LEGACY: { formatFactory: getFormat } }); +pluginInstance.start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/index.ts b/x-pack/legacy/plugins/lens/public/legacy_imports.ts similarity index 72% rename from x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/index.ts rename to x-pack/legacy/plugins/lens/public/legacy_imports.ts index f75dce9b7507f..9dcc22ddb1bb7 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/index.ts +++ b/x-pack/legacy/plugins/lens/public/legacy_imports.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './plugin'; +export { getFormat, FormatFactory } from 'ui/visualize/loader/pipeline_helpers/utilities'; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.scss b/x-pack/legacy/plugins/lens/public/metric_visualization/index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.scss rename to x-pack/legacy/plugins/lens/public/metric_visualization/index.scss diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/index.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/index.ts new file mode 100644 index 0000000000000..217cc6902fc99 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'src/core/public'; +import { FormatFactory } from '../legacy_imports'; +import { metricVisualization } from './metric_visualization'; +import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; +import { metricChart, getMetricChartRenderer } from './metric_expression'; +import { EditorFrameSetup } from '../types'; + +export interface MetricVisualizationPluginSetupPlugins { + expressions: ExpressionsSetup; + formatFactory: FormatFactory; + editorFrame: EditorFrameSetup; +} + +export class MetricVisualization { + constructor() {} + + setup( + _core: CoreSetup | null, + { expressions, formatFactory, editorFrame }: MetricVisualizationPluginSetupPlugins + ) { + expressions.registerFunction(() => metricChart); + + expressions.registerRenderer(() => getMetricChartRenderer(formatFactory)); + + editorFrame.registerVisualization(metricVisualization); + } +} diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx index a66239e5d30f6..eac35f82a50fa 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx @@ -11,7 +11,7 @@ import { MetricConfigPanel } from './metric_config_panel'; import { DatasourceDimensionPanelProps, Operation, DatasourcePublicAPI } from '../types'; import { State } from './types'; import { NativeRendererProps } from '../native_renderer'; -import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_plugin/mocks'; +import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks'; describe('MetricConfigPanel', () => { const dragDropContext = { dragging: undefined, setDragging: jest.fn() }; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.test.ts diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.ts diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts index c131612399cca..88964b95c2ac7 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts @@ -6,7 +6,7 @@ import { metricVisualization } from './metric_visualization'; import { State } from './types'; -import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_plugin/mocks'; +import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; import { generateId } from '../id_generator'; import { DatasourcePublicAPI, FramePublicAPI } from '../types'; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/types.ts rename to x-pack/legacy/plugins/lens/public/metric_visualization/types.ts diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.ts deleted file mode 100644 index f75dce9b7507f..0000000000000 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export * from './plugin'; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx deleted file mode 100644 index 219ef533a4ba3..0000000000000 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx +++ /dev/null @@ -1,50 +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 { npSetup } from 'ui/new_platform'; -import { CoreSetup } from 'src/core/public'; -import { FormatFactory, getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; -import { metricVisualization } from './metric_visualization'; -import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; -import { metricChart, getMetricChartRenderer } from './metric_expression'; - -export interface MetricVisualizationPluginSetupPlugins { - expressions: ExpressionsSetup; - // TODO this is a simulated NP plugin. - // Once field formatters are actually migrated, the actual shim can be used - fieldFormat: { - formatFactory: FormatFactory; - }; -} - -class MetricVisualizationPlugin { - constructor() {} - - setup( - _core: CoreSetup | null, - { expressions, fieldFormat }: MetricVisualizationPluginSetupPlugins - ) { - expressions.registerFunction(() => metricChart); - - expressions.registerRenderer(() => getMetricChartRenderer(fieldFormat.formatFactory)); - - return metricVisualization; - } - - stop() {} -} - -const plugin = new MetricVisualizationPlugin(); - -export const metricVisualizationSetup = () => - plugin.setup(null, { - expressions: npSetup.plugins.expressions, - fieldFormat: { - formatFactory: getFormat, - }, - }); - -export const metricVisualizationStop = () => plugin.stop(); diff --git a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx index 012c27d3ce3ff..38f48c9cdaf72 100644 --- a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx +++ b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { createMockDatasource } from '../editor_frame_plugin/mocks'; +import { createMockDatasource } from '../editor_frame_service/mocks'; import { MultiColumnEditor } from './multi_column_editor'; import { mount } from 'enzyme'; diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/plugin.tsx similarity index 55% rename from x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx rename to x-pack/legacy/plugins/lens/public/plugin.tsx index 283f4d2a0689d..634d227559835 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/plugin.tsx @@ -4,97 +4,112 @@ * you may not use this file except in compliance with the Elastic License. */ -import 'ui/autoload/all'; -// Used to run esaggs queries -import 'uiExports/fieldFormats'; -import 'uiExports/search'; -import 'uiExports/visRequestHandlers'; -import 'uiExports/visResponseHandlers'; -// Used for kibana_context function -import 'uiExports/savedObjectTypes'; - import React from 'react'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; import { render, unmountComponentAtNode } from 'react-dom'; -import { CoreSetup, CoreStart, SavedObjectsClientContract } from 'src/core/public'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { AppMountParameters, CoreSetup, CoreStart } from 'src/core/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; import rison, { RisonObject, RisonValue } from 'rison-node'; import { isObject } from 'lodash'; -import { DataStart } from '../../../../../../src/legacy/core_plugins/data/public'; -import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; -import { editorFrameSetup, editorFrameStart, editorFrameStop } from '../editor_frame_plugin'; -import { indexPatternDatasourceSetup, indexPatternDatasourceStop } from '../indexpattern_plugin'; -import { addHelpMenuToAppChrome } from '../help_menu_util'; -import { SavedObjectIndexStore } from '../persistence'; -import { xyVisualizationSetup, xyVisualizationStop } from '../xy_visualization_plugin'; -import { metricVisualizationSetup, metricVisualizationStop } from '../metric_visualization_plugin'; -import { - datatableVisualizationSetup, - datatableVisualizationStop, -} from '../datatable_visualization_plugin'; -import { App } from './app'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { EditorFrameService } from './editor_frame_service'; +import { IndexPatternDatasource } from './indexpattern_datasource'; +import { addHelpMenuToAppChrome } from './help_menu_util'; +import { SavedObjectIndexStore } from './persistence'; +import { XyVisualization } from './xy_visualization'; +import { MetricVisualization } from './metric_visualization'; +import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public'; +import { DatatableVisualization } from './datatable_visualization'; +import { App } from './app_plugin'; import { LensReportManager, setReportManager, stopReportManager, trackUiEvent, -} from '../lens_ui_telemetry'; -import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../../../../../plugins/lens/common'; -import { KibanaLegacySetup } from '../../../../../../src/plugins/kibana_legacy/public'; -import { EditorFrameStart } from '../types'; +} from './lens_ui_telemetry'; +import { KibanaLegacySetup } from '../../../../../src/plugins/kibana_legacy/public'; +import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../../../../plugins/lens/common'; import { addEmbeddableToDashboardUrl, getUrlVars, getLensUrlFromDashboardAbsoluteUrl, -} from '../../../../../../src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper'; +} from '../../../../../src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper'; +import { FormatFactory } from './legacy_imports'; +import { IEmbeddableSetup, IEmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { EditorFrameStart } from './types'; export interface LensPluginSetupDependencies { kibanaLegacy: KibanaLegacySetup; + expressions: ExpressionsSetup; + data: DataPublicPluginSetup; + embeddable: IEmbeddableSetup; + __LEGACY: { + formatFactory: FormatFactory; + }; } export interface LensPluginStartDependencies { data: DataPublicPluginStart; - dataShim: DataStart; + embeddable: IEmbeddableStart; + expressions: ExpressionsStart; } export const isRisonObject = (value: RisonValue): value is RisonObject => { return isObject(value); }; -export class AppPlugin { - private startDependencies: { - data: DataPublicPluginStart; - dataShim: DataStart; - savedObjectsClient: SavedObjectsClientContract; - editorFrame: EditorFrameStart; - } | null = null; - - constructor() {} - - setup(core: CoreSetup, { kibanaLegacy }: LensPluginSetupDependencies) { - // TODO: These plugins should not be called from the top level, but since this is the - // entry point to the app we have no choice until the new platform is ready - const indexPattern = indexPatternDatasourceSetup(); - const datatableVisualization = datatableVisualizationSetup(); - const xyVisualization = xyVisualizationSetup(); - const metricVisualization = metricVisualizationSetup(); - const editorFrameSetupInterface = editorFrameSetup(); +export class LensPlugin { + private datatableVisualization: DatatableVisualization; + private editorFrameService: EditorFrameService; + private createEditorFrame: EditorFrameStart['createInstance'] | null = null; + private indexpatternDatasource: IndexPatternDatasource; + private xyVisualization: XyVisualization; + private metricVisualization: MetricVisualization; + + constructor() { + this.datatableVisualization = new DatatableVisualization(); + this.editorFrameService = new EditorFrameService(); + this.indexpatternDatasource = new IndexPatternDatasource(); + this.xyVisualization = new XyVisualization(); + this.metricVisualization = new MetricVisualization(); + } - editorFrameSetupInterface.registerVisualization(xyVisualization); - editorFrameSetupInterface.registerVisualization(datatableVisualization); - editorFrameSetupInterface.registerVisualization(metricVisualization); - editorFrameSetupInterface.registerDatasource(indexPattern); + setup( + core: CoreSetup, + { + kibanaLegacy, + expressions, + data, + embeddable, + __LEGACY: { formatFactory }, + }: LensPluginSetupDependencies + ) { + const editorFrameSetupInterface = this.editorFrameService.setup(core, { + data, + embeddable, + expressions, + }); + const dependencies = { + expressions, + data, + editorFrame: editorFrameSetupInterface, + formatFactory, + }; + this.indexpatternDatasource.setup(core, dependencies); + this.xyVisualization.setup(core, dependencies); + this.datatableVisualization.setup(core, dependencies); + this.metricVisualization.setup(core, dependencies); kibanaLegacy.registerLegacyApp({ id: 'lens', title: NOT_INTERNATIONALIZED_PRODUCT_NAME, - mount: async (context, params) => { - if (this.startDependencies === null) { - throw new Error('mounted before start phase'); - } - const { data, savedObjectsClient, editorFrame } = this.startDependencies; - addHelpMenuToAppChrome(context.core.chrome); - const instance = editorFrame.createInstance({}); + mount: async (params: AppMountParameters) => { + const [coreStart, startDependencies] = await core.getStartServices(); + const dataStart = startDependencies.data; + const savedObjectsClient = coreStart.savedObjects.client; + addHelpMenuToAppChrome(coreStart.chrome); + + const instance = await this.createEditorFrame!({}); setReportManager( new LensReportManager({ @@ -108,7 +123,7 @@ export class AppPlugin { return; } // @ts-ignore - decoded.time = data.query.timefilter.timefilter.getTime(); + decoded.time = dataStart.query.timefilter.timefilter.getTime(); urlVars._g = rison.encode(decoded); }; const redirectTo = ( @@ -122,12 +137,12 @@ export class AppPlugin { routeProps.history.push(`/lens/edit/${id}`); } else if (addToDashboardMode && id) { routeProps.history.push(`/lens/edit/${id}`); - const url = context.core.chrome.navLinks.get('kibana:dashboard'); + const url = coreStart.chrome.navLinks.get('kibana:dashboard'); if (!url) { throw new Error('Cannot get last dashboard url'); } const lastDashboardAbsoluteUrl = url.url; - const basePath = context.core.http.basePath.get(); + const basePath = coreStart.http.basePath.get(); const lensUrl = getLensUrlFromDashboardAbsoluteUrl( lastDashboardAbsoluteUrl, basePath, @@ -158,8 +173,8 @@ export class AppPlugin { !!routeProps.location.search && routeProps.location.search.includes('addToDashboard'); return ( (datasource: Datasource) => void; - registerVisualization: (visualization: Visualization) => void; + registerDatasource: (datasource: Datasource | Promise>) => void; + registerVisualization: ( + visualization: Visualization | Promise> + ) => void; } export interface EditorFrameStart { - createInstance: (options: EditorFrameOptions) => EditorFrameInstance; + createInstance: (options: EditorFrameOptions) => Promise; } // Hints the default nesting to the data source. 0 is the highest priority diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap b/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap new file mode 100644 index 0000000000000..fd0c4b8212fc6 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap @@ -0,0 +1,482 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`xy_expression XYChart component it renders area 1`] = ` + + + + + + +`; + +exports[`xy_expression XYChart component it renders bar 1`] = ` + + + + + + +`; + +exports[`xy_expression XYChart component it renders horizontal bar 1`] = ` + + + + + + +`; + +exports[`xy_expression XYChart component it renders line 1`] = ` + + + + + + +`; + +exports[`xy_expression XYChart component it renders stacked area 1`] = ` + + + + + + +`; + +exports[`xy_expression XYChart component it renders stacked bar 1`] = ` + + + + + + +`; + +exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] = ` + + + + + + +`; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap b/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_visualization.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap rename to x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_visualization.test.ts.snap diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_index.scss b/x-pack/legacy/plugins/lens/public/xy_visualization/_index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_index.scss rename to x-pack/legacy/plugins/lens/public/xy_visualization/_index.scss diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_xy_expression.scss b/x-pack/legacy/plugins/lens/public/xy_visualization/_xy_expression.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_xy_expression.scss rename to x-pack/legacy/plugins/lens/public/xy_visualization/_xy_expression.scss diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/index.ts similarity index 57% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx rename to x-pack/legacy/plugins/lens/public/xy_visualization/index.ts index 6feece99370ef..86c52e0577616 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/index.ts @@ -4,24 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { npSetup } from 'ui/new_platform'; +import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; import { CoreSetup, IUiSettingsClient } from 'src/core/public'; -import chrome, { Chrome } from 'ui/chrome'; import moment from 'moment-timezone'; -import { getFormat, FormatFactory } from 'ui/visualize/loader/pipeline_helpers/utilities'; +import { FormatFactory } from '../legacy_imports'; import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; import { xyVisualization } from './xy_visualization'; import { xyChart, getXyChartRenderer } from './xy_expression'; import { legendConfig, xConfig, layerConfig } from './types'; +import { EditorFrameSetup } from '../types'; export interface XyVisualizationPluginSetupPlugins { expressions: ExpressionsSetup; - chrome: Chrome; - // TODO this is a simulated NP plugin. - // Once field formatters are actually migrated, the actual shim can be used - fieldFormat: { - formatFactory: FormatFactory; - }; + formatFactory: FormatFactory; + editorFrame: EditorFrameSetup; } function getTimeZone(uiSettings: IUiSettingsClient) { @@ -33,16 +29,12 @@ function getTimeZone(uiSettings: IUiSettingsClient) { return configuredTimeZone; } -class XyVisualizationPlugin { +export class XyVisualization { constructor() {} setup( - _core: CoreSetup | null, - { - expressions, - fieldFormat: { formatFactory }, - chrome: { getUiSettingsClient }, - }: XyVisualizationPluginSetupPlugins + core: CoreSetup, + { expressions, formatFactory, editorFrame }: XyVisualizationPluginSetupPlugins ) { expressions.registerFunction(() => legendConfig); expressions.registerFunction(() => xConfig); @@ -52,24 +44,13 @@ class XyVisualizationPlugin { expressions.registerRenderer( getXyChartRenderer({ formatFactory, - timeZone: getTimeZone(getUiSettingsClient()), + chartTheme: core.uiSettings.get('theme:darkMode') + ? EUI_CHARTS_THEME_DARK.theme + : EUI_CHARTS_THEME_LIGHT.theme, + timeZone: getTimeZone(core.uiSettings), }) ); - return xyVisualization; + editorFrame.registerVisualization(xyVisualization); } - - stop() {} } - -const plugin = new XyVisualizationPlugin(); - -export const xyVisualizationSetup = () => - plugin.setup(null, { - expressions: npSetup.plugins.expressions, - fieldFormat: { - formatFactory: getFormat, - }, - chrome, - }); -export const xyVisualizationStop = () => plugin.stop(); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/state_helpers.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/state_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/state_helpers.ts rename to x-pack/legacy/plugins/lens/public/xy_visualization/state_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts rename to x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts rename to x-pack/legacy/plugins/lens/public/xy_visualization/types.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index 6ed827bc71c68..301c4a58a0ffd 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -14,7 +14,7 @@ import { State } from './types'; import { Position } from '@elastic/charts'; import { NativeRendererProps } from '../native_renderer'; import { generateId } from '../id_generator'; -import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_plugin/mocks'; +import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks'; jest.mock('../id_generator'); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.test.tsx similarity index 93% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.test.tsx index daedb30db3f3e..04e0b80faa200 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.test.tsx @@ -132,6 +132,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'line' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -156,6 +157,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` @@ -184,6 +186,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(Settings).prop('xDomain')).toBeUndefined(); @@ -197,6 +200,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -211,6 +215,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -225,6 +230,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_horizontal' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -240,6 +246,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_stacked' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -255,6 +262,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area_stacked' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -273,6 +281,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -284,7 +293,13 @@ describe('xy_expression', () => { test('it passes time zone to the series', () => { const { data, args } = sampleArgs(); const component = shallow( - + ); expect(component.find(LineSeries).prop('timeZone')).toEqual('CEST'); }); @@ -299,6 +314,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [firstLayer] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(true); @@ -321,6 +337,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(true); @@ -337,6 +354,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(false); @@ -346,7 +364,13 @@ describe('xy_expression', () => { const { data, args } = sampleArgs(); const component = shallow( - + ); expect(component.find(LineSeries).prop('data')).toEqual([ { 'Label A': 1, 'Label B': 2, c: 'I', 'Label D': 'Foo', d: 'Foo' }, @@ -358,7 +382,13 @@ describe('xy_expression', () => { const { data, args } = sampleArgs(); const component = shallow( - + ); expect(component.find(LineSeries).prop('yAccessors')).toEqual(['Label A', 'Label B']); }); @@ -372,6 +402,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], xScaleType: 'ordinal' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(LineSeries).prop('xScaleType')).toEqual(ScaleType.Ordinal); @@ -386,6 +417,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], yScaleType: 'sqrt' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(LineSeries).prop('yScaleType')).toEqual(ScaleType.Sqrt); @@ -400,6 +432,7 @@ describe('xy_expression', () => { args={{ ...args }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); @@ -415,6 +448,7 @@ describe('xy_expression', () => { args={{ ...args }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); @@ -429,6 +463,7 @@ describe('xy_expression', () => { data={{ ...data }} args={{ ...args, layers: [{ ...args.layers[0], accessors: ['a'] }] }} formatFactory={getFormatSpy} + chartTheme={{}} timeZone="UTC" /> ); @@ -447,6 +482,7 @@ describe('xy_expression', () => { args={{ ...args }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx similarity index 94% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx index c62a8288d6655..27fd6e7064042 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -6,7 +6,6 @@ import React, { useState, useEffect } from 'react'; import ReactDOM from 'react-dom'; -import chrome from 'ui/chrome'; import { Chart, Settings, @@ -15,6 +14,7 @@ import { AreaSeries, BarSeries, Position, + PartialTheme, } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { @@ -27,16 +27,12 @@ import { import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; -import { FormatFactory } from '../../../../../../src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities'; +import { FormatFactory } from '../legacy_imports'; import { LensMultiTable } from '../types'; import { XYArgs, SeriesType, visualizationTypes } from './types'; import { VisualizationContainer } from '../visualization_container'; import { isHorizontalChart } from './state_helpers'; -const IS_DARK_THEME = chrome.getUiSettingsClient().get('theme:darkMode'); -const chartTheme = IS_DARK_THEME ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; - export interface XYChartProps { data: LensMultiTable; args: XYArgs; @@ -49,6 +45,7 @@ export interface XYRender { } type XYChartRenderProps = XYChartProps & { + chartTheme: PartialTheme; formatFactory: FormatFactory; timeZone: string; }; @@ -101,6 +98,7 @@ export const xyChart: ExpressionFunctionDefinition< export const getXyChartRenderer = (dependencies: { formatFactory: FormatFactory; + chartTheme: PartialTheme; timeZone: string; }): ExpressionRenderDefinition => ({ name: 'lens_xy_chart_renderer', @@ -146,7 +144,7 @@ export function XYChartReportable(props: XYChartRenderProps) { ); } -export function XYChart({ data, args, formatFactory, timeZone }: XYChartRenderProps) { +export function XYChart({ data, args, formatFactory, timeZone, chartTheme }: XYChartRenderProps) { const { legend, layers } = args; if (Object.values(data.tables).every(table => table.rows.length === 0)) { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts index 89794ec74eaec..a27a8e7754b86 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts @@ -8,7 +8,7 @@ import { xyVisualization } from './xy_visualization'; import { Position } from '@elastic/charts'; import { Operation } from '../types'; import { State, SeriesType } from './types'; -import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_plugin/mocks'; +import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; import { generateId } from '../id_generator'; import { Ast } from '@kbn/interpreter/target/common'; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap deleted file mode 100644 index 495d7a7bcd77e..0000000000000 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap +++ /dev/null @@ -1,1315 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`xy_expression XYChart component it renders area 1`] = ` - - - - - - -`; - -exports[`xy_expression XYChart component it renders bar 1`] = ` - - - - - - -`; - -exports[`xy_expression XYChart component it renders horizontal bar 1`] = ` - - - - - - -`; - -exports[`xy_expression XYChart component it renders line 1`] = ` - - - - - - -`; - -exports[`xy_expression XYChart component it renders stacked area 1`] = ` - - - - - - -`; - -exports[`xy_expression XYChart component it renders stacked bar 1`] = ` - - - - - - -`; - -exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] = ` - - - - - - -`; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/index.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/index.ts deleted file mode 100644 index f75dce9b7507f..0000000000000 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export * from './plugin'; From c333576a13b416d6fa64b6fc982084fe4414bae1 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Thu, 13 Feb 2020 13:59:27 +0100 Subject: [PATCH 7/9] [Lens] Filter out pinned filters from saved object of Lens (#57197) --- .../lens/public/app_plugin/app.test.tsx | 131 +++++++++++++----- .../plugins/lens/public/app_plugin/app.tsx | 17 ++- 2 files changed, 110 insertions(+), 38 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx index 99926c646da22..374e3270b3d45 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx @@ -38,7 +38,10 @@ jest const { TopNavMenu } = npStart.plugins.navigation.ui; -const waitForPromises = () => new Promise(resolve => setTimeout(resolve)); +const waitForPromises = async () => + act(async () => { + await new Promise(resolve => setTimeout(resolve)); + }); function createMockFrame(): jest.Mocked { return { @@ -220,6 +223,7 @@ describe('Lens App', () => { }); instance.setProps({ docId: '1234' }); + await waitForPromises(); expect(defaultArgs.core.chrome.setBreadcrumbs).toHaveBeenCalledWith([ @@ -373,8 +377,10 @@ describe('Lens App', () => { async function save({ initialDocId, addToDashboardMode, + lastKnownDoc = { expression: 'kibana 3' }, ...saveProps }: SaveProps & { + lastKnownDoc?: object; initialDocId?: string; addToDashboardMode?: boolean; }) { @@ -392,6 +398,7 @@ describe('Lens App', () => { state: { query: 'fake query', datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, + filters: [], }, }); (args.docStorage.save as jest.Mock).mockImplementation(async ({ id }) => ({ @@ -410,10 +417,12 @@ describe('Lens App', () => { } const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ - filterableIndexPatterns: [], - doc: ({ id: initialDocId, expression: 'kibana 3' } as unknown) as Document, - }); + act(() => + onChange({ + filterableIndexPatterns: [], + doc: { id: initialDocId, ...lastKnownDoc } as Document, + }) + ); instance.update(); @@ -441,10 +450,12 @@ describe('Lens App', () => { expect(getButton(instance).disableButton).toEqual(true); const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ - filterableIndexPatterns: [], - doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document, - }); + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document, + }) + ); instance.update(); expect(getButton(instance).disableButton).toEqual(true); }); @@ -482,10 +493,12 @@ describe('Lens App', () => { expect(getButton(instance).disableButton).toEqual(true); const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ - filterableIndexPatterns: [], - doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document, - }); + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document, + }) + ); instance.update(); expect(getButton(instance).disableButton).toEqual(false); @@ -559,10 +572,12 @@ describe('Lens App', () => { const instance = mount(); const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ - filterableIndexPatterns: [], - doc: ({ id: undefined, expression: 'new expression' } as unknown) as Document, - }); + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: undefined, expression: 'new expression' } as unknown) as Document, + }) + ); instance.update(); @@ -593,6 +608,38 @@ describe('Lens App', () => { expect(args.redirectTo).toHaveBeenCalledWith('aaa'); }); + + it('saves app filters and does not save pinned filters', async () => { + const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const field = ({ name: 'myfield' } as unknown) as IFieldType; + const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; + + const unpinned = esFilters.buildExistsFilter(field, indexPattern); + const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); + FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); + await waitForPromises(); + + const { args } = await save({ + initialDocId: '1234', + newCopyOnSave: false, + newTitle: 'hello there2', + lastKnownDoc: { + expression: 'kibana 3', + state: { + filters: [pinned, unpinned], + }, + }, + }); + + expect(args.docStorage.save).toHaveBeenCalledWith({ + id: '1234', + title: 'hello there2', + expression: 'kibana 3', + state: { + filters: [unpinned], + }, + }); + }); }); }); @@ -658,10 +705,12 @@ describe('Lens App', () => { ); const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ - filterableIndexPatterns: [{ id: '1', title: 'newIndex' }], - doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, - }); + act(() => + onChange({ + filterableIndexPatterns: [{ id: '1', title: 'newIndex' }], + doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + }) + ); await waitForPromises(); instance.update(); @@ -674,12 +723,15 @@ describe('Lens App', () => { ); // Do it again to verify that the dirty checking is done right - onChange({ - filterableIndexPatterns: [{ id: '2', title: 'second index' }], - doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, - }); + act(() => + onChange({ + filterableIndexPatterns: [{ id: '2', title: 'second index' }], + doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + }) + ); await waitForPromises(); + instance.update(); expect(TopNavMenu).toHaveBeenLastCalledWith( @@ -689,17 +741,18 @@ describe('Lens App', () => { {} ); }); - it('updates the editor frame when the user changes query or time in the search bar', () => { const args = defaultArgs; args.editorFrame = frame; const instance = mount(); - instance.find(TopNavMenu).prop('onQuerySubmit')!({ - dateRange: { from: 'now-14d', to: 'now-7d' }, - query: { query: 'new', language: 'lucene' }, - }); + act(() => + instance.find(TopNavMenu).prop('onQuerySubmit')!({ + dateRange: { from: 'now-14d', to: 'now-7d' }, + query: { query: 'new', language: 'lucene' }, + }) + ); instance.update(); @@ -728,7 +781,9 @@ describe('Lens App', () => { const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; - args.data.query.filterManager.setFilters([esFilters.buildExistsFilter(field, indexPattern)]); + act(() => + args.data.query.filterManager.setFilters([esFilters.buildExistsFilter(field, indexPattern)]) + ); instance.update(); @@ -852,10 +907,12 @@ describe('Lens App', () => { const instance = mount(); - instance.find(TopNavMenu).prop('onQuerySubmit')!({ - dateRange: { from: 'now-14d', to: 'now-7d' }, - query: { query: 'new', language: 'lucene' }, - }); + act(() => + instance.find(TopNavMenu).prop('onQuerySubmit')!({ + dateRange: { from: 'now-14d', to: 'now-7d' }, + query: { query: 'new', language: 'lucene' }, + }) + ); const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; @@ -865,10 +922,10 @@ describe('Lens App', () => { const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); - args.data.query.filterManager.setFilters([pinned, unpinned]); + act(() => args.data.query.filterManager.setFilters([pinned, unpinned])); instance.update(); - instance.find(TopNavMenu).prop('onClearSavedQuery')!(); + act(() => instance.find(TopNavMenu).prop('onClearSavedQuery')!()); instance.update(); expect(frame.mount).toHaveBeenLastCalledWith( diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index c901d4c0c1497..a212cb0a1a879 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -20,6 +20,7 @@ import { EditorFrameInstance } from '../types'; import { NativeRenderer } from '../native_renderer'; import { trackUiEvent } from '../lens_ui_telemetry'; import { + esFilters, Filter, IndexPattern as IndexPatternInstance, IndexPatternsContract, @@ -320,8 +321,22 @@ export function App({ {lastKnownDoc && state.isSaveModalVisible && ( { + const [pinnedFilters, appFilters] = _.partition( + lastKnownDoc.state?.filters, + esFilters.isFilterPinned + ); + const lastDocWithoutPinned = pinnedFilters?.length + ? { + ...lastKnownDoc, + state: { + ...lastKnownDoc.state, + filters: appFilters, + }, + } + : lastKnownDoc; + const doc = { - ...lastKnownDoc, + ...lastDocWithoutPinned, id: props.newCopyOnSave ? undefined : lastKnownDoc.id, title: props.newTitle, }; From 02319b7da8b7ab77e929a05d51f9574879671b6c Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 13 Feb 2020 13:11:31 +0000 Subject: [PATCH 8/9] [ML] Categorization field example endpoint tests (#57471) * [ML] Categorization example endpoint tests * adding data * removing debug code * adding endpoint error test * updating version in archive --- .../apis/ml/categorization_field_examples.ts | 305 ++++++ x-pack/test/api_integration/apis/ml/index.ts | 1 + .../ml/categorization/data.json.gz | Bin 0 -> 331971 bytes .../ml/categorization/mappings.json | 873 ++++++++++++++++++ 4 files changed, 1179 insertions(+) create mode 100644 x-pack/test/api_integration/apis/ml/categorization_field_examples.ts create mode 100644 x-pack/test/functional/es_archives/ml/categorization/data.json.gz create mode 100644 x-pack/test/functional/es_archives/ml/categorization/mappings.json diff --git a/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts b/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts new file mode 100644 index 0000000000000..89dbd73f3fb64 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts @@ -0,0 +1,305 @@ +/* + * 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'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +const start = 1554463535770; +const end = 1574316073914; +const analyzer = { + tokenizer: 'ml_classic', + filter: [ + { + type: 'stop', + stopwords: [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', + 'Mon', + 'Tue', + 'Wed', + 'Thu', + 'Fri', + 'Sat', + 'Sun', + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + 'GMT', + 'UTC', + ], + }, + ], +}; +const defaultRequestBody = { + indexPatternTitle: 'categorization_functional_test', + query: { bool: { must: [{ match_all: {} }] } }, + size: 5, + timeField: '@timestamp', + start, + end, + analyzer, +}; + +const testDataList = [ + { + title: 'valid with good number of tokens', + requestBody: { + ...defaultRequestBody, + field: 'field1', + }, + expected: { + responseCode: 200, + overallValidStatus: 'valid', + sampleSize: 1000, + exampleLength: 5, + validationChecks: [ + { + id: 0, + valid: 'valid', + message: '1000 field values analyzed, 95% contain 3 or more tokens.', + }, + ], + }, + }, + { + title: 'invalid, too many tokens.', + requestBody: { + ...defaultRequestBody, + field: 'field2', + }, + expected: { + responseCode: 200, + overallValidStatus: 'invalid', + sampleSize: 500, + exampleLength: 5, + validationChecks: [ + { + id: 1, + valid: 'partially_valid', + message: 'The median length for the field values analyzed is over 400 characters.', + }, + { + id: 4, + valid: 'invalid', + message: + 'Tokenization of field value examples has failed due to more than 10000 tokens being found in a sample of 50 values.', + }, + ], + }, + }, + { + title: 'partially valid, more than 75% are null', + requestBody: { + ...defaultRequestBody, + field: 'field3', + }, + expected: { + responseCode: 200, + overallValidStatus: 'partially_valid', + sampleSize: 250, + exampleLength: 5, + validationChecks: [ + { + id: 0, + valid: 'valid', + message: '250 field values analyzed, 95% contain 3 or more tokens.', + }, + { + id: 2, + valid: 'partially_valid', + message: 'More than 75% of field values are null.', + }, + ], + }, + }, + { + title: 'partially valid, median length is over 400 characters', + requestBody: { + ...defaultRequestBody, + field: 'field4', + }, + expected: { + responseCode: 200, + overallValidStatus: 'partially_valid', + sampleSize: 500, + exampleLength: 5, + validationChecks: [ + { + id: 0, + valid: 'valid', + message: '500 field values analyzed, 100% contain 3 or more tokens.', + }, + { + id: 1, + valid: 'partially_valid', + message: 'The median length for the field values analyzed is over 400 characters.', + }, + ], + }, + }, + { + title: 'invalid, no values in any doc', + requestBody: { + ...defaultRequestBody, + field: 'field5', + }, + expected: { + responseCode: 200, + overallValidStatus: 'invalid', + sampleSize: 0, + exampleLength: 0, + validationChecks: [ + { + id: 3, + valid: 'invalid', + message: + 'No examples for this field could be found. Please ensure the selected date range contains data.', + }, + ], + }, + }, + { + title: 'invalid, mostly made up of stop words, so no matched tokens', + requestBody: { + ...defaultRequestBody, + field: 'field6', + }, + expected: { + responseCode: 200, + overallValidStatus: 'invalid', + sampleSize: 1000, + exampleLength: 5, + validationChecks: [ + { + id: 0, + valid: 'invalid', + message: '1000 field values analyzed, 0% contain 3 or more tokens.', + }, + ], + }, + }, + { + title: 'valid, mostly made up of stop words, but analyser has no stop words. so it is ok.', + requestBody: { + ...defaultRequestBody, + field: 'field6', + analyzer: { + tokenizer: 'ml_classic', + }, + }, + expected: { + responseCode: 200, + overallValidStatus: 'valid', + sampleSize: 1000, + exampleLength: 5, + validationChecks: [ + { + id: 0, + valid: 'valid', + message: '1000 field values analyzed, 100% contain 3 or more tokens.', + }, + ], + }, + }, + { + title: 'partially valid, half the docs are stop words.', + requestBody: { + ...defaultRequestBody, + field: 'field7', + }, + expected: { + responseCode: 200, + overallValidStatus: 'partially_valid', + sampleSize: 1000, + exampleLength: 5, + validationChecks: [ + { + id: 0, + valid: 'partially_valid', + message: '1000 field values analyzed, 50% contain 3 or more tokens.', + }, + ], + }, + }, + { + title: "endpoint error, index doesn't exist", + requestBody: { + ...defaultRequestBody, + indexPatternTitle: 'does_not_exist', + field: 'field1', + }, + expected: { + responseCode: 404, + overallValidStatus: undefined, + sampleSize: undefined, + validationChecks: undefined, + }, + }, +]; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + + describe('Categorization example endpoint - ', function() { + before(async () => { + await esArchiver.load('ml/categorization'); + }); + + after(async () => { + await esArchiver.unload('ml/categorization'); + }); + + for (const testData of testDataList) { + it(testData.title, async () => { + const { body } = await supertest + .post('/api/ml/jobs/categorization_field_examples') + .set(COMMON_HEADERS) + .send(testData.requestBody) + .expect(testData.expected.responseCode); + + expect(body.overallValidStatus).to.eql(testData.expected.overallValidStatus); + expect(body.sampleSize).to.eql(testData.expected.sampleSize); + expect(body.validationChecks).to.eql(testData.expected.validationChecks); + if (body.statusCode === 200) { + expect(body.examples.length).to.eql(testData.expected.exampleLength); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 9fff4ca8436b0..1df5dfe2941ce 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -12,5 +12,6 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./bucket_span_estimator')); loadTestFile(require.resolve('./calculate_model_memory_limit')); + loadTestFile(require.resolve('./categorization_field_examples')); }); } diff --git a/x-pack/test/functional/es_archives/ml/categorization/data.json.gz b/x-pack/test/functional/es_archives/ml/categorization/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..a66b68d8159437c21c0d4d9a50164c6259f79134 GIT binary patch literal 331971 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

)*)Q}w2c|gpdaHSxxCQY0+gB?A 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

    IzEjLuraq(yOa^PfdMcl$ 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 _N(cl2+rkV4MYT!|l*HiAiSshl3+8N;C@aP- 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 Date: Thu, 13 Feb 2020 14:28:06 +0100 Subject: [PATCH 9/9] Use log4j pattern syntax (#57433) * address comments * use log4j-like syntax in layout pattern * %timestamp --> %date to match log4j conversion pattern * %context --> %logger to match log4j pattern * remove file from pre-defined appenders. file name is required. let users to setup everything * matchAll is not polyfilled in runtime * document available patterns and migration path * document BWC requirements * Revert "matchAll is not polyfilled in runtime" This reverts commit 9f491d4f535aa25e327961ffca4755039cf18040. * address comments Co-authored-by: Elastic Machine --- src/core/MIGRATION.md | 6 +- src/core/server/logging/README.md | 148 +++++++++++++++++- .../logging/integration_tests/logging.test.ts | 2 +- .../__snapshots__/json_layout.test.ts.snap | 4 - .../conversions/{timestamp.ts => date.ts} | 19 +-- .../logging/layouts/conversions/index.ts | 26 +++ .../logging/layouts/conversions/level.ts | 4 +- .../conversions/{context.ts => logger.ts} | 6 +- .../logging/layouts/conversions/message.ts | 4 +- .../logging/layouts/conversions/meta.ts | 6 +- .../server/logging/layouts/conversions/pid.ts | 4 +- .../logging/layouts/conversions/type.ts | 2 +- .../logging/layouts/json_layout.test.ts | 82 +++++++--- .../server/logging/layouts/layouts.test.ts | 6 +- .../logging/layouts/pattern_layout.test.ts | 79 +++++----- .../server/logging/layouts/pattern_layout.ts | 27 ++-- .../server/logging/logging_config.test.ts | 19 +-- src/core/server/logging/logging_config.ts | 7 - 18 files changed, 314 insertions(+), 137 deletions(-) rename src/core/server/logging/layouts/conversions/{timestamp.ts => date.ts} (83%) create mode 100644 src/core/server/logging/layouts/conversions/index.ts rename src/core/server/logging/layouts/conversions/{context.ts => logger.ts} (89%) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index c942bddc9fd57..fa0edd8faadd7 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -56,6 +56,7 @@ - [On the server side](#on-the-server-side) - [On the client side](#on-the-client-side) - [Updates an application navlink at runtime](#updates-an-app-navlink-at-runtime) + - [Logging config migration](#logging-config-migration) Make no mistake, it is going to take a lot of work to move certain plugins to the new platform. Our target is to migrate the entire repo over to the new platform throughout 7.x and to remove the legacy plugin system no later than 8.0, and this is only possible if teams start on the effort now. @@ -1655,4 +1656,7 @@ export class MyPlugin implements Plugin { tooltip: 'Application disabled', }) } -``` \ No newline at end of file +``` + +### Logging config migration +[Read](./server/logging/README.md#logging-config-migration) \ No newline at end of file diff --git a/src/core/server/logging/README.md b/src/core/server/logging/README.md index 65fe64b045801..3fbec7a45148d 100644 --- a/src/core/server/logging/README.md +++ b/src/core/server/logging/README.md @@ -1,4 +1,12 @@ # Logging +- [Loggers, Appenders and Layouts](#loggers-appenders-and-layouts) +- [Logger hierarchy](#logger-hierarchy) +- [Log level](#log-level) +- [Layouts](#layouts) + - [Pattern layout](#pattern-layout) + - [JSON layout](#json-layout) +- [Configuration](#configuration) +- [Usage](#usage) The way logging works in Kibana is inspired by `log4j 2` logging framework used by [Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html#logging). The main idea is to have consistent logging behaviour (configuration, log format etc.) across the entire Elastic Stack @@ -52,12 +60,68 @@ custom appenders, so one should always make the choice explicitly. There are two types of layout supported at the moment: `pattern` and `json`. -With `pattern` layout it's possible to define a string pattern with special placeholders wrapped into curly braces that +### Pattern layout +With `pattern` layout it's possible to define a string pattern with special placeholders `%conversion_pattern` (see the table below) that will be replaced with data from the actual log message. By default the following pattern is used: -`[{timestamp}][{level}][{context}] {message}`. Also `highlight` option can be enabled for `pattern` layout so that +`[%date][%level][%logger]%meta %message`. Also `highlight` option can be enabled for `pattern` layout so that some parts of the log message are highlighted with different colors that may be quite handy if log messages are forwarded to the terminal with color support. +`pattern` layout uses a sub-set of [log4j2 pattern syntax](https://logging.apache.org/log4j/2.x/manual/layouts.html#PatternLayout) +and **doesn't implement** all `log4j2` capabilities. The conversions that are provided out of the box are: +#### level +Outputs the [level](#log-level) of the logging event. +Example of `%level` output: +```bash +TRACE +DEBUG +INFO +``` + +##### logger +Outputs the name of the logger that published the logging event. +Example of `%logger` output: +```bash +server +server.http +server.http.Kibana +``` + +#### message +Outputs the application supplied message associated with the logging event. + +#### meta +Outputs the entries of `meta` object data in **json** format, if one is present in the event. +Example of `%meta` output: +```bash +// Meta{from: 'v7', to: 'v8'} +'{"from":"v7","to":"v8"}' +// Meta empty object +'{}' +// no Meta provided +'' +``` + +##### date +Outputs the date of the logging event. The date conversion specifier may be followed by a set of braces containing a name of predefined date format and canonical timezone name. +Timezone name is expected to be one from [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) +Example of `%date` output: + +| Conversion pattern | Example | +| ---------------------------------------- | ---------------------------------------------------------------- | +| `%date` | `2012-02-01T14:30:22.011Z` uses `ISO8601` format by default | +| `%date{ISO8601}` | `2012-02-01T14:30:22.011Z` | +| `%date{ISO8601_TZ}` | `2012-02-01T09:30:22.011-05:00` `ISO8601` with timezone | +| `%date{ISO8601_TZ}{America/Los_Angeles}` | `2012-02-01T06:30:22.011-08:00` | +| `%date{ABSOLUTE}` | `09:30:22.011` | +| `%date{ABSOLUTE}{America/Los_Angeles}` | `06:30:22.011` | +| `%date{UNIX}` | `1328106622` | +| `%date{UNIX_MILLIS}` | `1328106622011` | + +#### pid +Outputs the process ID. + +### JSON layout With `json` layout log messages will be formatted as JSON strings that include timestamp, log level, context, message text and any other metadata that may be associated with the log message itself. @@ -88,7 +152,7 @@ logging: kind: console layout: kind: pattern - pattern: [{timestamp}][{level}] {message} + pattern: "[%date][%level] %message" json-file-appender: kind: file path: /var/log/kibana-json.log @@ -179,3 +243,81 @@ The log will be less verbose with `warn` level for the `server` context: [2017-07-25T18:54:41.639Z][ERROR][server] Message with `error` log level. [2017-07-25T18:54:41.639Z][FATAL][server] Message with `fatal` log level. ``` + +### Logging config migration +Compatibility with the legacy logging system is assured until the end of the `v7` version. +All log messages handled by `root` context are forwarded to the legacy logging service. If you re-write +root appenders, make sure that it contains `default` appender to provide backward compatibility. +**Note**: If you define an appender for a context, the log messages aren't handled by the +`root` context anymore and not forwarded to the legacy logging service. + +#### logging.dest +By default logs in *stdout*. With new Kibana logging you can use pre-existing `console` appender or +define a custom one. +```yaml +logging: + loggers: + - context: your-plugin + appenders: [console] +``` +Logs in a *file* if given file path. You should define a custom appender with `kind: file` +```yaml + +logging: + appenders: + file: + kind: file + path: /var/log/kibana.log + layout: + kind: pattern + loggers: + - context: your-plugin + appenders: [file] +``` +#### logging.json +Defines the format of log output. Logs in JSON if `true`. With new logging config you can adjust +the output format with [layouts](#layouts). + +#### logging.quiet +Suppresses all logging output other than error messages. With new logging, config can be achieved +with adjusting minimum required [logging level](#log-level) +```yaml + loggers: + - context: my-plugin + appenders: [console] + level: error +# or for all output +logging.root.level: error +``` + +#### logging.silent: +Suppresses all logging output. +```yaml +logging.root.level: off +``` + +#### logging.verbose: +Logs all events +```yaml +logging.root.level: all +``` + +#### logging.timezone +Set to the canonical timezone id to log events using that timezone. New logging config allows +to [specify timezone](#date) for `layout: pattern`. +```yaml +logging: + appenders: + custom-console: + kind: console + layout: + kind: pattern + highlight: true + pattern: "[%level] [%date{ISO8601_TZ}{America/Los_Angeles}][%logger] %message" +``` + +#### logging.events +Define a custom logger for a specific context. + +#### logging.filter +TBD diff --git a/src/core/server/logging/integration_tests/logging.test.ts b/src/core/server/logging/integration_tests/logging.test.ts index 7142f91300f12..b88f5ba2c2b60 100644 --- a/src/core/server/logging/integration_tests/logging.test.ts +++ b/src/core/server/logging/integration_tests/logging.test.ts @@ -29,7 +29,7 @@ function createRoot() { layout: { highlight: false, kind: 'pattern', - pattern: '{level}|{context}|{message}', + pattern: '%level|%logger|%message', }, }, }, diff --git a/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap b/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap index da57023c94286..14c071b40ad7a 100644 --- a/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap +++ b/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap @@ -1,9 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`\`format()\` correctly formats error record with meta-data 1`] = `"{\\"@timestamp\\":\\"2012-02-01T09:30:22.011-05:00\\",\\"context\\":\\"context-with-meta\\",\\"level\\":\\"DEBUG\\",\\"message\\":\\"message-with-meta\\",\\"meta\\":{\\"from\\":\\"v7\\",\\"to\\":\\"v8\\"},\\"pid\\":5355}"`; - -exports[`\`format()\` correctly formats record with meta-data 1`] = `"{\\"@timestamp\\":\\"2012-02-01T09:30:22.011-05:00\\",\\"context\\":\\"context-with-meta\\",\\"level\\":\\"DEBUG\\",\\"message\\":\\"message-with-meta\\",\\"meta\\":{\\"from\\":\\"v7\\",\\"to\\":\\"v8\\"},\\"pid\\":5355}"`; - exports[`\`format()\` correctly formats record. 1`] = `"{\\"@timestamp\\":\\"2012-02-01T09:30:22.011-05:00\\",\\"context\\":\\"context-1\\",\\"error\\":{\\"message\\":\\"Some error message\\",\\"name\\":\\"Some error name\\",\\"stack\\":\\"Some error stack\\"},\\"level\\":\\"FATAL\\",\\"message\\":\\"message-1\\",\\"pid\\":5355}"`; exports[`\`format()\` correctly formats record. 2`] = `"{\\"@timestamp\\":\\"2012-02-01T09:30:22.011-05:00\\",\\"context\\":\\"context-2\\",\\"level\\":\\"ERROR\\",\\"message\\":\\"message-2\\",\\"pid\\":5355}"`; diff --git a/src/core/server/logging/layouts/conversions/timestamp.ts b/src/core/server/logging/layouts/conversions/date.ts similarity index 83% rename from src/core/server/logging/layouts/conversions/timestamp.ts rename to src/core/server/logging/layouts/conversions/date.ts index 6db6fc6eeb6bf..d3ed54fb98240 100644 --- a/src/core/server/logging/layouts/conversions/timestamp.ts +++ b/src/core/server/logging/layouts/conversions/date.ts @@ -22,7 +22,7 @@ import { last } from 'lodash'; import { Conversion } from './type'; import { LogRecord } from '../../log_record'; -const timestampRegExp = /{timestamp({(?[^}]+)})?({(?[^}]+)})?}/gi; +const dateRegExp = /%date({(?[^}]+)})?({(?[^}]+)})?/g; const formats = { ISO8601: 'ISO8601', @@ -54,10 +54,11 @@ function formatDate(date: Date, dateFormat: string = formats.ISO8601, timezone?: } function validateDateFormat(input: string) { - if (Reflect.has(formats, input)) return; - throw new Error( - `Date format expected one of ${Reflect.ownKeys(formats).join(', ')}, but given: ${input}` - ); + if (!Reflect.has(formats, input)) { + throw new Error( + `Date format expected one of ${Reflect.ownKeys(formats).join(', ')}, but given: ${input}` + ); + } } function validateTimezone(timezone: string) { @@ -66,7 +67,7 @@ function validateTimezone(timezone: string) { } function validate(rawString: string) { - for (const matched of rawString.matchAll(timestampRegExp)) { + for (const matched of rawString.matchAll(dateRegExp)) { const { format, timezone } = matched.groups!; if (format) { @@ -78,9 +79,9 @@ function validate(rawString: string) { } } -export const TimestampConversion: Conversion = { - pattern: timestampRegExp, - formatter(record: LogRecord, highlight: boolean, ...matched: any[]) { +export const DateConversion: Conversion = { + pattern: dateRegExp, + convert(record: LogRecord, highlight: boolean, ...matched: any[]) { const groups: Record = last(matched); const { format, timezone } = groups; diff --git a/src/core/server/logging/layouts/conversions/index.ts b/src/core/server/logging/layouts/conversions/index.ts new file mode 100644 index 0000000000000..23e6aded6c6f7 --- /dev/null +++ b/src/core/server/logging/layouts/conversions/index.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +export { Conversion } from './type'; + +export { LoggerConversion } from './logger'; +export { LevelConversion } from './level'; +export { MessageConversion } from './message'; +export { MetaConversion } from './meta'; +export { PidConversion } from './pid'; +export { DateConversion } from './date'; diff --git a/src/core/server/logging/layouts/conversions/level.ts b/src/core/server/logging/layouts/conversions/level.ts index 02ed86dd2c24f..58b271140eff5 100644 --- a/src/core/server/logging/layouts/conversions/level.ts +++ b/src/core/server/logging/layouts/conversions/level.ts @@ -32,8 +32,8 @@ const LEVEL_COLORS = new Map([ ]); export const LevelConversion: Conversion = { - pattern: /{level}/gi, - formatter(record: LogRecord, highlight: boolean) { + pattern: /%level/g, + convert(record: LogRecord, highlight: boolean) { let message = record.level.id.toUpperCase().padEnd(5); if (highlight && LEVEL_COLORS.has(record.level)) { const color = LEVEL_COLORS.get(record.level)!; diff --git a/src/core/server/logging/layouts/conversions/context.ts b/src/core/server/logging/layouts/conversions/logger.ts similarity index 89% rename from src/core/server/logging/layouts/conversions/context.ts rename to src/core/server/logging/layouts/conversions/logger.ts index d1fa9ca84f555..debb1737ab95a 100644 --- a/src/core/server/logging/layouts/conversions/context.ts +++ b/src/core/server/logging/layouts/conversions/logger.ts @@ -22,9 +22,9 @@ import chalk from 'chalk'; import { Conversion } from './type'; import { LogRecord } from '../../log_record'; -export const ContextConversion: Conversion = { - pattern: /{context}/gi, - formatter(record: LogRecord, highlight: boolean) { +export const LoggerConversion: Conversion = { + pattern: /%logger/g, + convert(record: LogRecord, highlight: boolean) { let message = record.context; if (highlight) { message = chalk.magenta(message); diff --git a/src/core/server/logging/layouts/conversions/message.ts b/src/core/server/logging/layouts/conversions/message.ts index b95a89b12b780..f8c5e68ada4fb 100644 --- a/src/core/server/logging/layouts/conversions/message.ts +++ b/src/core/server/logging/layouts/conversions/message.ts @@ -21,8 +21,8 @@ import { Conversion } from './type'; import { LogRecord } from '../../log_record'; export const MessageConversion: Conversion = { - pattern: /{message}/gi, - formatter(record: LogRecord) { + pattern: /%message/g, + convert(record: LogRecord) { // Error stack is much more useful than just the message. return (record.error && record.error.stack) || record.message; }, diff --git a/src/core/server/logging/layouts/conversions/meta.ts b/src/core/server/logging/layouts/conversions/meta.ts index f6d4557e0db53..ee8c207389fbe 100644 --- a/src/core/server/logging/layouts/conversions/meta.ts +++ b/src/core/server/logging/layouts/conversions/meta.ts @@ -20,8 +20,8 @@ import { Conversion } from './type'; import { LogRecord } from '../../log_record'; export const MetaConversion: Conversion = { - pattern: /{meta}/gi, - formatter(record: LogRecord) { - return record.meta ? `[${JSON.stringify(record.meta)}]` : ''; + pattern: /%meta/g, + convert(record: LogRecord) { + return record.meta ? `${JSON.stringify(record.meta)}` : ''; }, }; diff --git a/src/core/server/logging/layouts/conversions/pid.ts b/src/core/server/logging/layouts/conversions/pid.ts index 0fcdd93fcda0c..37d34a4f1cf8b 100644 --- a/src/core/server/logging/layouts/conversions/pid.ts +++ b/src/core/server/logging/layouts/conversions/pid.ts @@ -21,8 +21,8 @@ import { Conversion } from './type'; import { LogRecord } from '../../log_record'; export const PidConversion: Conversion = { - pattern: /{pid}/gi, - formatter(record: LogRecord) { + pattern: /%pid/g, + convert(record: LogRecord) { return String(record.pid); }, }; diff --git a/src/core/server/logging/layouts/conversions/type.ts b/src/core/server/logging/layouts/conversions/type.ts index 34a6475138814..a57a1f954e53a 100644 --- a/src/core/server/logging/layouts/conversions/type.ts +++ b/src/core/server/logging/layouts/conversions/type.ts @@ -20,6 +20,6 @@ import { LogRecord } from 'kibana/server'; export interface Conversion { pattern: RegExp; - formatter: (record: LogRecord, highlight: boolean) => string; + convert: (record: LogRecord, highlight: boolean) => string; validate?: (input: string) => void; } diff --git a/src/core/server/logging/layouts/json_layout.test.ts b/src/core/server/logging/layouts/json_layout.test.ts index ec8c44ec62a22..77e2876c143da 100644 --- a/src/core/server/logging/layouts/json_layout.test.ts +++ b/src/core/server/logging/layouts/json_layout.test.ts @@ -90,34 +90,68 @@ test('`format()` correctly formats record with meta-data', () => { const layout = new JsonLayout(); expect( - layout.format({ - context: 'context-with-meta', - level: LogLevel.Debug, - message: 'message-with-meta', - timestamp, - pid: 5355, - meta: { - from: 'v7', - to: 'v8', - }, - }) - ).toMatchSnapshot(); + JSON.parse( + layout.format({ + context: 'context-with-meta', + level: LogLevel.Debug, + message: 'message-with-meta', + timestamp, + pid: 5355, + meta: { + from: 'v7', + to: 'v8', + }, + }) + ) + ).toStrictEqual({ + '@timestamp': '2012-02-01T09:30:22.011-05:00', + context: 'context-with-meta', + level: 'DEBUG', + message: 'message-with-meta', + meta: { + from: 'v7', + to: 'v8', + }, + pid: 5355, + }); }); test('`format()` correctly formats error record with meta-data', () => { const layout = new JsonLayout(); expect( - layout.format({ - context: 'context-with-meta', - level: LogLevel.Debug, - message: 'message-with-meta', - timestamp, - pid: 5355, - meta: { - from: 'v7', - to: 'v8', - }, - }) - ).toMatchSnapshot(); + JSON.parse( + layout.format({ + context: 'error-with-meta', + level: LogLevel.Debug, + error: { + message: 'Some error message', + name: 'Some error name', + stack: 'Some error stack', + }, + message: 'Some error message', + timestamp, + pid: 5355, + meta: { + from: 'v7', + to: 'v8', + }, + }) + ) + ).toStrictEqual({ + '@timestamp': '2012-02-01T09:30:22.011-05:00', + context: 'error-with-meta', + level: 'DEBUG', + error: { + message: 'Some error message', + name: 'Some error name', + stack: 'Some error stack', + }, + message: 'Some error message', + meta: { + from: 'v7', + to: 'v8', + }, + pid: 5355, + }); }); diff --git a/src/core/server/logging/layouts/layouts.test.ts b/src/core/server/logging/layouts/layouts.test.ts index aa1c54c846bc6..b1fb836f40d5d 100644 --- a/src/core/server/logging/layouts/layouts.test.ts +++ b/src/core/server/logging/layouts/layouts.test.ts @@ -33,12 +33,12 @@ test('`configSchema` creates correct schema for `pattern` layout.', () => { const validConfig = { highlight: true, kind: 'pattern', - pattern: '{message}', + pattern: '%message', }; expect(layoutsSchema.validate(validConfig)).toEqual({ highlight: true, kind: 'pattern', - pattern: '{message}', + pattern: '%message', }); const wrongConfig2 = { kind: 'pattern', pattern: 1 }; @@ -56,7 +56,7 @@ test('`create()` creates correct layout.', () => { const patternLayout = Layouts.create({ highlight: false, kind: 'pattern', - pattern: '[{timestamp}][{level}][{context}] {message}', + pattern: '[%date][%level][%logger] %message', }); expect(patternLayout).toBeInstanceOf(PatternLayout); diff --git a/src/core/server/logging/layouts/pattern_layout.test.ts b/src/core/server/logging/layouts/pattern_layout.test.ts index 2d948ea59c6d1..cce55b147e0ed 100644 --- a/src/core/server/logging/layouts/pattern_layout.test.ts +++ b/src/core/server/logging/layouts/pattern_layout.test.ts @@ -88,12 +88,12 @@ test('`createConfigSchema()` creates correct schema.', () => { const validConfig = { highlight: true, kind: 'pattern', - pattern: '{message}', + pattern: '%message', }; expect(layoutSchema.validate(validConfig)).toEqual({ highlight: true, kind: 'pattern', - pattern: '{message}', + pattern: '%message', }); const wrongConfig1 = { kind: 'json' }; @@ -112,7 +112,7 @@ test('`format()` correctly formats record with full pattern.', () => { }); test('`format()` correctly formats record with custom pattern.', () => { - const layout = new PatternLayout('mock-{message}-{context}-{message}'); + const layout = new PatternLayout('mock-%message-%logger-%message'); for (const record of records) { expect(layout.format(record)).toMatchSnapshot(); @@ -134,7 +134,7 @@ test('`format()` correctly formats record with meta data.', () => { to: 'v8', }, }) - ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta][{"from":"v7","to":"v8"}] message-meta'); + ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta]{"from":"v7","to":"v8"} message-meta'); expect( layout.format({ @@ -145,7 +145,7 @@ test('`format()` correctly formats record with meta data.', () => { pid: 5355, meta: {}, }) - ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta][{}] message-meta'); + ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta]{} message-meta'); expect( layout.format({ @@ -167,7 +167,7 @@ test('`format()` correctly formats record with highlighting.', () => { }); test('allows specifying the PID in custom pattern', () => { - const layout = new PatternLayout('{pid}-{context}-{message}'); + const layout = new PatternLayout('%pid-%logger-%message'); for (const record of records) { expect(layout.format(record)).toMatchSnapshot(); @@ -175,7 +175,7 @@ test('allows specifying the PID in custom pattern', () => { }); test('`format()` allows specifying pattern with meta.', () => { - const layout = new PatternLayout('{context}-{meta}-{message}'); + const layout = new PatternLayout('%logger-%meta-%message'); const record = { context: 'context', level: LogLevel.Debug, @@ -187,7 +187,7 @@ test('`format()` allows specifying pattern with meta.', () => { to: 'v8', }, }; - expect(layout.format(record)).toBe('context-[{"from":"v7","to":"v8"}]-message'); + expect(layout.format(record)).toBe('context-{"from":"v7","to":"v8"}-message'); }); describe('format', () => { @@ -207,31 +207,31 @@ describe('format', () => { describe('supports specifying a predefined format', () => { it('ISO8601', () => { - const layout = new PatternLayout('[{timestamp{ISO8601}}][{context}]'); + const layout = new PatternLayout('[%date{ISO8601}][%logger]'); expect(layout.format(record)).toBe('[2012-02-01T14:30:22.011Z][context]'); }); it('ISO8601_TZ', () => { - const layout = new PatternLayout('[{timestamp{ISO8601_TZ}}][{context}]'); + const layout = new PatternLayout('[%date{ISO8601_TZ}][%logger]'); expect(layout.format(record)).toBe('[2012-02-01T09:30:22.011-05:00][context]'); }); it('ABSOLUTE', () => { - const layout = new PatternLayout('[{timestamp{ABSOLUTE}}][{context}]'); + const layout = new PatternLayout('[%date{ABSOLUTE}][%logger]'); expect(layout.format(record)).toBe('[09:30:22.011][context]'); }); it('UNIX', () => { - const layout = new PatternLayout('[{timestamp{UNIX}}][{context}]'); + const layout = new PatternLayout('[%date{UNIX}][%logger]'); expect(layout.format(record)).toBe('[1328106622][context]'); }); it('UNIX_MILLIS', () => { - const layout = new PatternLayout('[{timestamp{UNIX_MILLIS}}][{context}]'); + const layout = new PatternLayout('[%date{UNIX_MILLIS}][%logger]'); expect(layout.format(record)).toBe('[1328106622011][context]'); }); @@ -239,42 +239,38 @@ describe('format', () => { describe('supports specifying a predefined format and timezone', () => { it('ISO8601', () => { - const layout = new PatternLayout('[{timestamp{ISO8601}{America/Los_Angeles}}][{context}]'); + const layout = new PatternLayout('[%date{ISO8601}{America/Los_Angeles}][%logger]'); expect(layout.format(record)).toBe('[2012-02-01T14:30:22.011Z][context]'); }); it('ISO8601_TZ', () => { - const layout = new PatternLayout( - '[{timestamp{ISO8601_TZ}{America/Los_Angeles}}][{context}]' - ); + const layout = new PatternLayout('[%date{ISO8601_TZ}{America/Los_Angeles}][%logger]'); expect(layout.format(record)).toBe('[2012-02-01T06:30:22.011-08:00][context]'); }); it('ABSOLUTE', () => { - const layout = new PatternLayout('[{timestamp{ABSOLUTE}{America/Los_Angeles}}][{context}]'); + const layout = new PatternLayout('[%date{ABSOLUTE}{America/Los_Angeles}][%logger]'); expect(layout.format(record)).toBe('[06:30:22.011][context]'); }); it('UNIX', () => { - const layout = new PatternLayout('[{timestamp{UNIX}{America/Los_Angeles}}][{context}]'); + const layout = new PatternLayout('[%date{UNIX}{America/Los_Angeles}][%logger]'); expect(layout.format(record)).toBe('[1328106622][context]'); }); it('UNIX_MILLIS', () => { - const layout = new PatternLayout( - '[{timestamp{UNIX_MILLIS}{America/Los_Angeles}}][{context}]' - ); + const layout = new PatternLayout('[%date{UNIX_MILLIS}{America/Los_Angeles}][%logger]'); expect(layout.format(record)).toBe('[1328106622011][context]'); }); }); it('formats several conversions patterns correctly', () => { const layout = new PatternLayout( - '[{timestamp{ABSOLUTE}{America/Los_Angeles}}][{context}][{timestamp{UNIX}}]' + '[%date{ABSOLUTE}{America/Los_Angeles}][%logger][%date{UNIX}]' ); expect(layout.format(record)).toBe('[06:30:22.011][context][1328106622]'); @@ -284,45 +280,44 @@ describe('format', () => { describe('schema', () => { describe('pattern', () => { - describe('{timestamp}', () => { - it('does not fail when {timestamp} not present', () => { + describe('%date', () => { + it('does not fail when %date not present', () => { expect(patternSchema.validate('')).toBe(''); expect(patternSchema.validate('{pid}')).toBe('{pid}'); }); - it('does not fail on {timestamp} without params', () => { - expect(patternSchema.validate('{timestamp}')).toBe('{timestamp}'); - expect(patternSchema.validate('{timestamp}}')).toBe('{timestamp}}'); - expect(patternSchema.validate('{{timestamp}}')).toBe('{{timestamp}}'); + it('does not fail on %date without params', () => { + expect(patternSchema.validate('%date')).toBe('%date'); + expect(patternSchema.validate('%date')).toBe('%date'); + expect(patternSchema.validate('{%date}')).toBe('{%date}'); + expect(patternSchema.validate('%date%date')).toBe('%date%date'); }); - it('does not fail on {timestamp} with predefined date format', () => { - expect(patternSchema.validate('{timestamp{ISO8601}}')).toBe('{timestamp{ISO8601}}'); + it('does not fail on %date with predefined date format', () => { + expect(patternSchema.validate('%date{ISO8601}')).toBe('%date{ISO8601}'); }); - it('does not fail on {timestamp} with predefined date format and valid timezone', () => { - expect(patternSchema.validate('{timestamp{ISO8601_TZ}{Europe/Berlin}}')).toBe( - '{timestamp{ISO8601_TZ}{Europe/Berlin}}' + it('does not fail on %date with predefined date format and valid timezone', () => { + expect(patternSchema.validate('%date{ISO8601_TZ}{Europe/Berlin}')).toBe( + '%date{ISO8601_TZ}{Europe/Berlin}' ); }); - it('fails on {timestamp} with unknown date format', () => { - expect(() => - patternSchema.validate('{timestamp{HH:MM:SS}}') - ).toThrowErrorMatchingInlineSnapshot( + it('fails on %date with unknown date format', () => { + expect(() => patternSchema.validate('%date{HH:MM:SS}')).toThrowErrorMatchingInlineSnapshot( `"Date format expected one of ISO8601, ISO8601_TZ, ABSOLUTE, UNIX, UNIX_MILLIS, but given: HH:MM:SS"` ); }); - it('fails on {timestamp} with predefined date format and invalid timezone', () => { + it('fails on %date with predefined date format and invalid timezone', () => { expect(() => - patternSchema.validate('{timestamp{ISO8601_TZ}{Europe/Kibana}}') + patternSchema.validate('%date{ISO8601_TZ}{Europe/Kibana}') ).toThrowErrorMatchingInlineSnapshot(`"Unknown timezone: Europe/Kibana"`); }); - it('validates several {timestamp} in pattern', () => { + it('validates several %date in pattern', () => { expect(() => - patternSchema.validate('{timestamp{ISO8601_TZ}{Europe/Berlin}}{message}{timestamp{HH}}') + patternSchema.validate('%date{ISO8601_TZ}{Europe/Berlin}%message%date{HH}') ).toThrowErrorMatchingInlineSnapshot( `"Date format expected one of ISO8601, ISO8601_TZ, ABSOLUTE, UNIX, UNIX_MILLIS, but given: HH"` ); diff --git a/src/core/server/logging/layouts/pattern_layout.ts b/src/core/server/logging/layouts/pattern_layout.ts index 0a2a25a135069..9490db149cc0f 100644 --- a/src/core/server/logging/layouts/pattern_layout.ts +++ b/src/core/server/logging/layouts/pattern_layout.ts @@ -21,23 +21,24 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { LogRecord } from '../log_record'; import { Layout } from './layouts'; - -import { Conversion } from './conversions/type'; -import { ContextConversion } from './conversions/context'; -import { LevelConversion } from './conversions/level'; -import { MetaConversion } from './conversions/meta'; -import { MessageConversion } from './conversions/message'; -import { PidConversion } from './conversions/pid'; -import { TimestampConversion } from './conversions/timestamp'; +import { + Conversion, + LoggerConversion, + LevelConversion, + MetaConversion, + MessageConversion, + PidConversion, + DateConversion, +} from './conversions'; /** * Default pattern used by PatternLayout if it's not overridden in the configuration. */ -const DEFAULT_PATTERN = `[{timestamp}][{level}][{context}]{meta} {message}`; +const DEFAULT_PATTERN = `[%date][%level][%logger]%meta %message`; export const patternSchema = schema.string({ validate: string => { - TimestampConversion.validate!(string); + DateConversion.validate!(string); }, }); @@ -48,12 +49,12 @@ const patternLayoutSchema = schema.object({ }); const conversions: Conversion[] = [ - ContextConversion, + LoggerConversion, MessageConversion, LevelConversion, MetaConversion, PidConversion, - TimestampConversion, + DateConversion, ]; /** @internal */ @@ -77,7 +78,7 @@ export class PatternLayout implements Layout { for (const conversion of conversions) { recordString = recordString.replace( conversion.pattern, - conversion.formatter.bind(null, record, this.highlight) + conversion.convert.bind(null, record, this.highlight) ); } diff --git a/src/core/server/logging/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts index b3631abb9ff00..75f571d34c25c 100644 --- a/src/core/server/logging/logging_config.test.ts +++ b/src/core/server/logging/logging_config.test.ts @@ -59,7 +59,7 @@ test('`getLoggerContext()` returns correct joined context name.', () => { test('correctly fills in default config.', () => { const configValue = new LoggingConfig(config.schema.validate({})); - expect(configValue.appenders.size).toBe(3); + expect(configValue.appenders.size).toBe(2); expect(configValue.appenders.get('default')).toEqual({ kind: 'console', @@ -69,10 +69,6 @@ test('correctly fills in default config.', () => { kind: 'console', layout: { kind: 'pattern', highlight: true }, }); - expect(configValue.appenders.get('file')).toEqual({ - kind: 'file', - layout: { kind: 'pattern', highlight: false }, - }); }); test('correctly fills in custom `appenders` config.', () => { @@ -83,16 +79,11 @@ test('correctly fills in custom `appenders` config.', () => { kind: 'console', layout: { kind: 'pattern' }, }, - file: { - kind: 'file', - layout: { kind: 'pattern' }, - path: 'path', - }, }, }) ); - expect(configValue.appenders.size).toBe(3); + expect(configValue.appenders.size).toBe(2); expect(configValue.appenders.get('default')).toEqual({ kind: 'console', @@ -103,12 +94,6 @@ test('correctly fills in custom `appenders` config.', () => { kind: 'console', layout: { kind: 'pattern' }, }); - - expect(configValue.appenders.get('file')).toEqual({ - kind: 'file', - layout: { kind: 'pattern' }, - path: 'path', - }); }); test('correctly fills in default `loggers` config.', () => { diff --git a/src/core/server/logging/logging_config.ts b/src/core/server/logging/logging_config.ts index f1fbf787737b4..8f80be7d79cb1 100644 --- a/src/core/server/logging/logging_config.ts +++ b/src/core/server/logging/logging_config.ts @@ -140,13 +140,6 @@ export class LoggingConfig { layout: { kind: 'pattern', highlight: true }, } as AppenderConfigType, ], - [ - 'file', - { - kind: 'file', - layout: { kind: 'pattern', highlight: false }, - } as AppenderConfigType, - ], ]); /**