From cd109fa7c7593cf174b74474734ec8fabf20d0f5 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 28 Nov 2019 13:28:32 +0100 Subject: [PATCH 1/6] [Discover] Improve Percy functional tests (#51699) * Implement new wait for chart rendered function * Add findByCssSelector to ensure the charts have been rendered --- .../tests/discover/chart_visualization.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/visual_regression/tests/discover/chart_visualization.js b/test/visual_regression/tests/discover/chart_visualization.js index 540d95973b547..c90f29c66acb8 100644 --- a/test/visual_regression/tests/discover/chart_visualization.js +++ b/test/visual_regression/tests/discover/chart_visualization.js @@ -27,6 +27,7 @@ export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); const visualTesting = getService('visualTesting'); + const find = getService('find'); const defaultSettings = { defaultIndex: 'logstash-*', 'discover:sampleSize': 1 @@ -48,10 +49,12 @@ export default function ({ getService, getPageObjects }) { describe('query', function () { this.tags(['skipFirefox']); + let renderCounter = 0; it('should show bars in the correct time zone', async function () { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); + await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -61,6 +64,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Hourly'); + await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -70,6 +74,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Daily'); + await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -79,6 +84,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Weekly'); + await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -92,6 +98,7 @@ export default function ({ getService, getPageObjects }) { }); await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); + await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -101,6 +108,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Monthly'); + await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -110,6 +118,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Yearly'); + await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -119,6 +128,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Auto'); + await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); await visualTesting.snapshot({ show: ['discoverChart'], }); From af23f302c0ec213114dce829688fa8f0bf7a158e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 28 Nov 2019 08:59:00 -0500 Subject: [PATCH 2/6] Fix error returned when creating an alert with ES security disabled (#51639) * Fix error returned when creating an alert with ES security disabled * Add test to ensure error gets thrown when inner function throws --- .../server/lib/alerts_client_factory.test.ts | 24 +++++++++++++++++++ .../server/lib/alerts_client_factory.ts | 12 ++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.test.ts b/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.test.ts index 1063e20e4ba3b..a465aebc8bd86 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.test.ts @@ -93,6 +93,16 @@ test('createAPIKey() returns { created: false } when security is disabled', asyn expect(createAPIKeyResult).toEqual({ created: false }); }); +test('createAPIKey() returns { created: false } when security is enabled but ES security is disabled', async () => { + const factory = new AlertsClientFactory(alertsClientFactoryParams); + factory.create(KibanaRequest.from(fakeRequest), fakeRequest); + const constructorCall = jest.requireMock('../alerts_client').AlertsClient.mock.calls[0][0]; + + securityPluginSetup.authc.createAPIKey.mockResolvedValueOnce(null); + const createAPIKeyResult = await constructorCall.createAPIKey(); + expect(createAPIKeyResult).toEqual({ created: false }); +}); + test('createAPIKey() returns an API key when security is enabled', async () => { const factory = new AlertsClientFactory({ ...alertsClientFactoryParams, @@ -105,3 +115,17 @@ test('createAPIKey() returns an API key when security is enabled', async () => { const createAPIKeyResult = await constructorCall.createAPIKey(); expect(createAPIKeyResult).toEqual({ created: true, result: { api_key: '123', id: 'abc' } }); }); + +test('createAPIKey() throws when security plugin createAPIKey throws an error', async () => { + const factory = new AlertsClientFactory({ + ...alertsClientFactoryParams, + securityPluginSetup: securityPluginSetup as any, + }); + factory.create(KibanaRequest.from(fakeRequest), fakeRequest); + const constructorCall = jest.requireMock('../alerts_client').AlertsClient.mock.calls[0][0]; + + securityPluginSetup.authc.createAPIKey.mockRejectedValueOnce(new Error('TLS disabled')); + await expect(constructorCall.createAPIKey()).rejects.toThrowErrorMatchingInlineSnapshot( + `"TLS disabled"` + ); +}); diff --git a/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.ts b/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.ts index bacb346042187..b75d681b6586a 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.ts @@ -53,12 +53,16 @@ export class AlertsClientFactory { if (!securityPluginSetup) { return { created: false }; } + const createAPIKeyResult = await securityPluginSetup.authc.createAPIKey(request, { + name: `source: alerting, generated uuid: "${uuid.v4()}"`, + role_descriptors: {}, + }); + if (!createAPIKeyResult) { + return { created: false }; + } return { created: true, - result: (await securityPluginSetup.authc.createAPIKey(request, { - name: `source: alerting, generated uuid: "${uuid.v4()}"`, - role_descriptors: {}, - }))!, + result: createAPIKeyResult, }; }, }); From 1367814a8eccad501df2ab022c36a00898a58e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 28 Nov 2019 11:02:11 -0500 Subject: [PATCH 3/6] Enable alerting and actions plugin by default (#51254) * Enable alerting and actions plugin by default * Fix test failure * Fix features test --- x-pack/legacy/plugins/actions/README.md | 7 +++---- x-pack/legacy/plugins/actions/index.ts | 2 +- x-pack/legacy/plugins/alerting/README.md | 5 ++--- x-pack/legacy/plugins/alerting/index.ts | 2 +- .../api_integration/apis/features/features/features.ts | 2 ++ x-pack/test/api_integration/apis/security/privileges.ts | 2 ++ 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/x-pack/legacy/plugins/actions/README.md b/x-pack/legacy/plugins/actions/README.md index 150cc4c0472b7..2eec667ce95c4 100644 --- a/x-pack/legacy/plugins/actions/README.md +++ b/x-pack/legacy/plugins/actions/README.md @@ -19,10 +19,9 @@ action types. ## Usage -1. Enable the actions plugin in the `kibana.yml` by setting `xpack.actions.enabled: true`. -2. Develop and register an action type (see action types -> example). -3. Create an action by using the RESTful API (see actions -> create action). -4. Use alerts to execute actions or execute manually (see firing actions). +1. Develop and register an action type (see action types -> example). +2. Create an action by using the RESTful API (see actions -> create action). +3. Use alerts to execute actions or execute manually (see firing actions). ## Kibana Actions Configuration Implemented under the [Actions Config](./server/actions_config.ts). diff --git a/x-pack/legacy/plugins/actions/index.ts b/x-pack/legacy/plugins/actions/index.ts index a58c936c63749..98d4d9f84a729 100644 --- a/x-pack/legacy/plugins/actions/index.ts +++ b/x-pack/legacy/plugins/actions/index.ts @@ -33,7 +33,7 @@ export function actions(kibana: any) { config(Joi: Root) { return Joi.object() .keys({ - enabled: Joi.boolean().default(false), + enabled: Joi.boolean().default(true), whitelistedHosts: Joi.array() .items( Joi.string() diff --git a/x-pack/legacy/plugins/alerting/README.md b/x-pack/legacy/plugins/alerting/README.md index 40f61d11e9ace..85dbd75e14174 100644 --- a/x-pack/legacy/plugins/alerting/README.md +++ b/x-pack/legacy/plugins/alerting/README.md @@ -23,9 +23,8 @@ A Kibana alert detects a condition and executes one or more actions when that co ## Usage -1. Enable the alerting plugin in the `kibana.yml` by setting `xpack.alerting.enabled: true`. -2. Develop and register an alert type (see alert types -> example). -3. Create an alert using the RESTful API (see alerts -> create). +1. Develop and register an alert type (see alert types -> example). +2. Create an alert using the RESTful API (see alerts -> create). ## Limitations diff --git a/x-pack/legacy/plugins/alerting/index.ts b/x-pack/legacy/plugins/alerting/index.ts index b3e33f782688c..5baec07fa1182 100644 --- a/x-pack/legacy/plugins/alerting/index.ts +++ b/x-pack/legacy/plugins/alerting/index.ts @@ -34,7 +34,7 @@ export function alerting(kibana: any) { config(Joi: Root) { return Joi.object() .keys({ - enabled: Joi.boolean().default(false), + enabled: Joi.boolean().default(true), }) .default(); }, diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index ef0f0451ee058..db08fc24a474a 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -115,6 +115,8 @@ export default function({ getService }: FtrProviderContext) { 'maps', 'uptime', 'siem', + 'alerting', + 'actions', ].sort() ); }); diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index d4c8a3e68c50e..d6ad1608f3688 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -37,6 +37,8 @@ export default function({ getService }: FtrProviderContext) { uptime: ['all', 'read'], apm: ['all', 'read'], siem: ['all', 'read'], + actions: ['all', 'read'], + alerting: ['all', 'read'], }, global: ['all', 'read'], space: ['all', 'read'], From 0f8533730c4d777669e590f9bc8796f394e55d68 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 28 Nov 2019 22:53:02 +0100 Subject: [PATCH 4/6] Unify uiSettingsClient contracts (#51167) * introduce IUiSettingsClient on the client * switch uiSettings service to IUiSettingsClient * update uiSettings service tests * all plugins should use IUiSettingsClient * stop is not public methods anymore * remove savedobject attribute type references * regen docs * remove all references to UiSettingsClient class * regen docs * add migration example for uiSettings * update consumer types and tests * address comments --- ...bana-plugin-public.appmountcontext.core.md | 2 +- .../kibana-plugin-public.appmountcontext.md | 2 +- .../kibana-plugin-public.contextsetup.md | 2 +- .../public/kibana-plugin-public.coresetup.md | 2 +- ...bana-plugin-public.coresetup.uisettings.md | 4 +- .../public/kibana-plugin-public.corestart.md | 2 +- ...bana-plugin-public.corestart.uisettings.md | 4 +- ...ana-plugin-public.iuisettingsclient.get.md | 13 + ...na-plugin-public.iuisettingsclient.get_.md | 13 + ...-plugin-public.iuisettingsclient.getall.md | 13 + ...ugin-public.iuisettingsclient.getsaved_.md | 17 ++ ...gin-public.iuisettingsclient.getupdate_.md | 17 ++ ...lic.iuisettingsclient.getupdateerrors_.md} | 10 +- ...lugin-public.iuisettingsclient.iscustom.md | 13 + ...gin-public.iuisettingsclient.isdeclared.md | 13 + ...ugin-public.iuisettingsclient.isdefault.md | 13 + ...n-public.iuisettingsclient.isoverridden.md | 13 + .../kibana-plugin-public.iuisettingsclient.md | 32 +++ ....iuisettingsclient.overridelocaldefault.md | 13 + ...plugin-public.iuisettingsclient.remove.md} | 17 +- ...ana-plugin-public.iuisettingsclient.set.md | 13 + .../core/public/kibana-plugin-public.md | 3 +- ...a-plugin-public.toastsapi._constructor_.md | 4 +- ...n-public.uisettingsclient._constructor_.md | 20 -- ...bana-plugin-public.uisettingsclient.get.md | 25 -- ...ana-plugin-public.uisettingsclient.get_.md | 25 -- ...a-plugin-public.uisettingsclient.getall.md | 17 -- ...lugin-public.uisettingsclient.getsaved_.md | 25 -- ...ugin-public.uisettingsclient.getupdate_.md | 25 -- ...plugin-public.uisettingsclient.iscustom.md | 24 -- ...ugin-public.uisettingsclient.isdeclared.md | 24 -- ...lugin-public.uisettingsclient.isdefault.md | 24 -- ...in-public.uisettingsclient.isoverridden.md | 24 -- .../kibana-plugin-public.uisettingsclient.md | 38 --- ...c.uisettingsclient.overridelocaldefault.md | 25 -- ...bana-plugin-public.uisettingsclient.set.md | 25 -- ...ana-plugin-public.uisettingsclient.stop.md | 17 -- ...-plugin-public.uisettingsclientcontract.md | 13 - .../kibana-plugin-server.contextsetup.md | 2 +- ...ana-plugin-server.iuisettingsclient.get.md | 2 +- ...-plugin-server.iuisettingsclient.getall.md | 2 +- ...erver.iuisettingsclient.getuserprovided.md | 2 +- .../kibana-plugin-server.iuisettingsclient.md | 10 +- ...ana-plugin-server.iuisettingsclient.set.md | 2 +- ...plugin-server.iuisettingsclient.setmany.md | 2 +- ...kibana-plugin-server.userprovidedvalues.md | 2 +- src/core/MIGRATION.md | 32 +++ src/core/public/application/types.ts | 6 +- src/core/public/context/context_service.ts | 2 +- src/core/public/index.ts | 13 +- .../integrations/integrations_service.ts | 4 +- .../integrations/moment/moment_service.ts | 4 +- .../integrations/styles/styles_service.ts | 4 +- .../notifications/notifications_service.ts | 4 +- .../notifications/toasts/toasts_api.tsx | 6 +- .../notifications/toasts/toasts_service.tsx | 4 +- .../overlays/banners/banners_service.tsx | 4 +- .../overlays/banners/user_banner_service.tsx | 4 +- src/core/public/overlays/overlay_service.ts | 4 +- src/core/public/public.api.md | 64 ++--- .../ui_settings_client.test.ts.snap | 18 +- .../ui_settings_service.test.ts.snap | 117 -------- src/core/public/ui_settings/index.ts | 4 +- src/core/public/ui_settings/types.ts | 103 +++++++ .../ui_settings/ui_settings_client.test.ts | 251 +++++++++--------- .../public/ui_settings/ui_settings_client.ts | 142 +++------- .../ui_settings/ui_settings_service.mock.ts | 6 +- .../ui_settings_service.test.mocks.ts | 57 ---- .../ui_settings/ui_settings_service.test.ts | 55 +--- .../public/ui_settings/ui_settings_service.ts | 14 +- src/core/server/context/context_service.ts | 2 +- src/core/server/server.api.md | 12 +- .../create_or_upgrade_saved_config.ts | 6 +- src/core/server/ui_settings/types.ts | 14 +- .../server/ui_settings/ui_settings_client.ts | 28 +- .../server/ui_settings/ui_settings_service.ts | 4 +- .../index_patterns/index_patterns.test.ts | 8 +- .../index_patterns/index_patterns.ts | 6 +- .../index_patterns/index_patterns_service.ts | 4 +- .../query_string_input.test.tsx.snap | 6 - .../components/fetch_index_patterns.ts | 4 +- .../kibana/public/dashboard/application.ts | 4 +- .../kibana/public/home/kibana_services.ts | 4 +- .../core_plugins/region_map/public/plugin.ts | 4 +- .../core_plugins/tile_map/public/plugin.ts | 4 +- .../core_plugins/timelion/public/plugin.ts | 4 +- .../contexts/query_input_bar_context.ts | 4 +- .../vis_type_timeseries/public/plugin.ts | 4 +- .../vis_type_timeseries/public/services.ts | 6 +- .../vis_type_vega/public/plugin.ts | 4 +- .../public/np_ready/public/services.ts | 6 +- .../agg_types/buckets/date_range.test.ts | 2 +- .../agg_types/buckets/histogram.test.ts | 2 +- .../public/courier/fetch/fetch_soon.test.ts | 4 +- .../courier/fetch/get_search_params.test.ts | 4 +- .../public/courier/fetch/get_search_params.ts | 12 +- src/legacy/ui/public/courier/fetch/types.ts | 4 +- .../default_search_strategy.test.ts | 4 +- .../ui/public/test_harness/test_harness.js | 9 +- .../es_query/get_es_query_config.test.ts | 4 +- .../field_formats_provider/field_formats.ts | 6 +- .../field_formats_service.ts | 4 +- .../query/filter_manager/filter_manager.ts | 6 +- .../data/public/query/lib/get_query_log.ts | 4 +- .../query/timefilter/timefilter_service.ts | 4 +- .../value_suggestions.test.ts | 6 +- .../suggestions_provider/value_suggestions.ts | 4 +- .../data/public/ui/filter_bar/filter_item.tsx | 4 +- .../language_switcher.test.tsx.snap | 2 - .../views/data/components/data_table.tsx | 4 +- .../views/data/components/data_view.test.tsx | 4 +- .../views/data/components/data_view.tsx | 4 +- .../inspector/public/views/data/index.tsx | 4 +- .../table_list_view/table_list_view.tsx | 4 +- src/test_utils/public/stub_field_formats.ts | 4 +- .../legacy/plugins/graph/public/render_app.ts | 4 +- .../dimension_panel/dimension_panel.test.tsx | 8 +- .../dimension_panel/dimension_panel.tsx | 8 +- .../definitions/date_histogram.test.tsx | 8 +- .../operations/definitions/index.ts | 8 +- .../operations/definitions/terms.test.tsx | 8 +- .../public/xy_visualization_plugin/plugin.tsx | 4 +- .../transform/public/app/lib/kibana/common.ts | 4 +- 123 files changed, 739 insertions(+), 1092 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-public.iuisettingsclient.get.md create mode 100644 docs/development/core/public/kibana-plugin-public.iuisettingsclient.get_.md create mode 100644 docs/development/core/public/kibana-plugin-public.iuisettingsclient.getall.md create mode 100644 docs/development/core/public/kibana-plugin-public.iuisettingsclient.getsaved_.md create mode 100644 docs/development/core/public/kibana-plugin-public.iuisettingsclient.getupdate_.md rename docs/development/core/public/{kibana-plugin-public.uisettingsclient.getupdateerrors_.md => kibana-plugin-public.iuisettingsclient.getupdateerrors_.md} (50%) create mode 100644 docs/development/core/public/kibana-plugin-public.iuisettingsclient.iscustom.md create mode 100644 docs/development/core/public/kibana-plugin-public.iuisettingsclient.isdeclared.md create mode 100644 docs/development/core/public/kibana-plugin-public.iuisettingsclient.isdefault.md create mode 100644 docs/development/core/public/kibana-plugin-public.iuisettingsclient.isoverridden.md create mode 100644 docs/development/core/public/kibana-plugin-public.iuisettingsclient.md create mode 100644 docs/development/core/public/kibana-plugin-public.iuisettingsclient.overridelocaldefault.md rename docs/development/core/public/{kibana-plugin-public.uisettingsclient.remove.md => kibana-plugin-public.iuisettingsclient.remove.md} (51%) create mode 100644 docs/development/core/public/kibana-plugin-public.iuisettingsclient.set.md delete mode 100644 docs/development/core/public/kibana-plugin-public.uisettingsclient._constructor_.md delete mode 100644 docs/development/core/public/kibana-plugin-public.uisettingsclient.get.md delete mode 100644 docs/development/core/public/kibana-plugin-public.uisettingsclient.get_.md delete mode 100644 docs/development/core/public/kibana-plugin-public.uisettingsclient.getall.md delete mode 100644 docs/development/core/public/kibana-plugin-public.uisettingsclient.getsaved_.md delete mode 100644 docs/development/core/public/kibana-plugin-public.uisettingsclient.getupdate_.md delete mode 100644 docs/development/core/public/kibana-plugin-public.uisettingsclient.iscustom.md delete mode 100644 docs/development/core/public/kibana-plugin-public.uisettingsclient.isdeclared.md delete mode 100644 docs/development/core/public/kibana-plugin-public.uisettingsclient.isdefault.md delete mode 100644 docs/development/core/public/kibana-plugin-public.uisettingsclient.isoverridden.md delete mode 100644 docs/development/core/public/kibana-plugin-public.uisettingsclient.md delete mode 100644 docs/development/core/public/kibana-plugin-public.uisettingsclient.overridelocaldefault.md delete mode 100644 docs/development/core/public/kibana-plugin-public.uisettingsclient.set.md delete mode 100644 docs/development/core/public/kibana-plugin-public.uisettingsclient.stop.md delete mode 100644 docs/development/core/public/kibana-plugin-public.uisettingsclientcontract.md delete mode 100644 src/core/public/ui_settings/__snapshots__/ui_settings_service.test.ts.snap delete mode 100644 src/core/public/ui_settings/ui_settings_service.test.mocks.ts diff --git a/docs/development/core/public/kibana-plugin-public.appmountcontext.core.md b/docs/development/core/public/kibana-plugin-public.appmountcontext.core.md index f4dee0f29af34..960d610b589b8 100644 --- a/docs/development/core/public/kibana-plugin-public.appmountcontext.core.md +++ b/docs/development/core/public/kibana-plugin-public.appmountcontext.core.md @@ -17,7 +17,7 @@ core: { i18n: I18nStart; notifications: NotificationsStart; overlays: OverlayStart; - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; injectedMetadata: { getInjectedVar: (name: string, defaultValue?: any) => unknown; }; diff --git a/docs/development/core/public/kibana-plugin-public.appmountcontext.md b/docs/development/core/public/kibana-plugin-public.appmountcontext.md index 97d143d518f60..e12121e0e3ebb 100644 --- a/docs/development/core/public/kibana-plugin-public.appmountcontext.md +++ b/docs/development/core/public/kibana-plugin-public.appmountcontext.md @@ -16,5 +16,5 @@ export interface AppMountContext | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-public.appmountcontext.core.md) | {
application: Pick<ApplicationStart, 'capabilities' | 'navigateToApp'>;
chrome: ChromeStart;
docLinks: DocLinksStart;
http: HttpStart;
i18n: I18nStart;
notifications: NotificationsStart;
overlays: OverlayStart;
uiSettings: UiSettingsClientContract;
injectedMetadata: {
getInjectedVar: (name: string, defaultValue?: any) => unknown;
};
} | Core service APIs available to mounted applications. | +| [core](./kibana-plugin-public.appmountcontext.core.md) | {
application: Pick<ApplicationStart, 'capabilities' | 'navigateToApp'>;
chrome: ChromeStart;
docLinks: DocLinksStart;
http: HttpStart;
i18n: I18nStart;
notifications: NotificationsStart;
overlays: OverlayStart;
uiSettings: IUiSettingsClient;
injectedMetadata: {
getInjectedVar: (name: string, defaultValue?: any) => unknown;
};
} | Core service APIs available to mounted applications. | diff --git a/docs/development/core/public/kibana-plugin-public.contextsetup.md b/docs/development/core/public/kibana-plugin-public.contextsetup.md index 2b67c7cdaf0e1..a006fa7205ca6 100644 --- a/docs/development/core/public/kibana-plugin-public.contextsetup.md +++ b/docs/development/core/public/kibana-plugin-public.contextsetup.md @@ -85,7 +85,7 @@ Say we're creating a plugin for rendering visualizations that allows new renderi export interface VizRenderContext { core: { i18n: I18nStart; - uiSettings: UISettingsClientContract; + uiSettings: IUiSettingsClient; } [contextName: string]: unknown; } diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.md b/docs/development/core/public/kibana-plugin-public.coresetup.md index f9335425fed4c..8314bde7b95f0 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.md @@ -22,5 +22,5 @@ export interface CoreSetup | [http](./kibana-plugin-public.coresetup.http.md) | HttpSetup | [HttpSetup](./kibana-plugin-public.httpsetup.md) | | [injectedMetadata](./kibana-plugin-public.coresetup.injectedmetadata.md) | {
getInjectedVar: (name: string, defaultValue?: any) => unknown;
} | exposed temporarily until https://github.com/elastic/kibana/issues/41990 done use \*only\* to retrieve config values. There is no way to set injected values in the new platform. Use the legacy platform API instead. | | [notifications](./kibana-plugin-public.coresetup.notifications.md) | NotificationsSetup | [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | -| [uiSettings](./kibana-plugin-public.coresetup.uisettings.md) | UiSettingsClientContract | [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | +| [uiSettings](./kibana-plugin-public.coresetup.uisettings.md) | IUiSettingsClient | [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.uisettings.md b/docs/development/core/public/kibana-plugin-public.coresetup.uisettings.md index 78a13fccd23ed..bf9ec12e3eea2 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.uisettings.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.uisettings.md @@ -4,10 +4,10 @@ ## CoreSetup.uiSettings property -[UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) +[IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) Signature: ```typescript -uiSettings: UiSettingsClientContract; +uiSettings: IUiSettingsClient; ``` diff --git a/docs/development/core/public/kibana-plugin-public.corestart.md b/docs/development/core/public/kibana-plugin-public.corestart.md index 47eba78bf43e4..e561ee313f100 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.md @@ -25,5 +25,5 @@ export interface CoreStart | [notifications](./kibana-plugin-public.corestart.notifications.md) | NotificationsStart | [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | | [overlays](./kibana-plugin-public.corestart.overlays.md) | OverlayStart | [OverlayStart](./kibana-plugin-public.overlaystart.md) | | [savedObjects](./kibana-plugin-public.corestart.savedobjects.md) | SavedObjectsStart | [SavedObjectsStart](./kibana-plugin-public.savedobjectsstart.md) | -| [uiSettings](./kibana-plugin-public.corestart.uisettings.md) | UiSettingsClientContract | [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | +| [uiSettings](./kibana-plugin-public.corestart.uisettings.md) | IUiSettingsClient | [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | diff --git a/docs/development/core/public/kibana-plugin-public.corestart.uisettings.md b/docs/development/core/public/kibana-plugin-public.corestart.uisettings.md index 1751135e01981..2ee405591dc08 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.uisettings.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.uisettings.md @@ -4,10 +4,10 @@ ## CoreStart.uiSettings property -[UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) +[IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) Signature: ```typescript -uiSettings: UiSettingsClientContract; +uiSettings: IUiSettingsClient; ``` diff --git a/docs/development/core/public/kibana-plugin-public.iuisettingsclient.get.md b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.get.md new file mode 100644 index 0000000000000..8d14a10951a92 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) > [get](./kibana-plugin-public.iuisettingsclient.get.md) + +## IUiSettingsClient.get property + +Gets the value for a specific uiSetting. If this setting has no user-defined value then the `defaultOverride` parameter is returned (and parsed if setting is of type "json" or "number). If the parameter is not defined and the key is not registered by any plugin then an error is thrown, otherwise reads the default value defined by a plugin. + +Signature: + +```typescript +get: (key: string, defaultOverride?: T) => T; +``` diff --git a/docs/development/core/public/kibana-plugin-public.iuisettingsclient.get_.md b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.get_.md new file mode 100644 index 0000000000000..b7680b769f303 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.get_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) > [get$](./kibana-plugin-public.iuisettingsclient.get_.md) + +## IUiSettingsClient.get$ property + +Gets an observable of the current value for a config key, and all updates to that config key in the future. Providing a `defaultOverride` argument behaves the same as it does in \#get() + +Signature: + +```typescript +get$: (key: string, defaultOverride?: T) => Observable; +``` diff --git a/docs/development/core/public/kibana-plugin-public.iuisettingsclient.getall.md b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.getall.md new file mode 100644 index 0000000000000..b767a8ff603c8 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.getall.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) > [getAll](./kibana-plugin-public.iuisettingsclient.getall.md) + +## IUiSettingsClient.getAll property + +Gets the metadata about all uiSettings, including the type, default value, and user value for each key. + +Signature: + +```typescript +getAll: () => Readonly>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.iuisettingsclient.getsaved_.md b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.getsaved_.md new file mode 100644 index 0000000000000..a4ddb9abcba97 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.getsaved_.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) > [getSaved$](./kibana-plugin-public.iuisettingsclient.getsaved_.md) + +## IUiSettingsClient.getSaved$ property + +Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed. + +Signature: + +```typescript +getSaved$: () => Observable<{ + key: string; + newValue: T; + oldValue: T; + }>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.iuisettingsclient.getupdate_.md b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.getupdate_.md new file mode 100644 index 0000000000000..cec5bc096cf02 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.getupdate_.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) > [getUpdate$](./kibana-plugin-public.iuisettingsclient.getupdate_.md) + +## IUiSettingsClient.getUpdate$ property + +Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed. + +Signature: + +```typescript +getUpdate$: () => Observable<{ + key: string; + newValue: T; + oldValue: T; + }>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.getupdateerrors_.md b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.getupdateerrors_.md similarity index 50% rename from docs/development/core/public/kibana-plugin-public.uisettingsclient.getupdateerrors_.md rename to docs/development/core/public/kibana-plugin-public.iuisettingsclient.getupdateerrors_.md index ada2a56ac8db6..2fbcaac03e2bb 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.getupdateerrors_.md +++ b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.getupdateerrors_.md @@ -1,17 +1,13 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [getUpdateErrors$](./kibana-plugin-public.uisettingsclient.getupdateerrors_.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) > [getUpdateErrors$](./kibana-plugin-public.iuisettingsclient.getupdateerrors_.md) -## UiSettingsClient.getUpdateErrors$() method +## IUiSettingsClient.getUpdateErrors$ property Returns an Observable that notifies subscribers of each error while trying to update the settings, containing the actual Error class. Signature: ```typescript -getUpdateErrors$(): Rx.Observable; +getUpdateErrors$: () => Observable; ``` -Returns: - -`Rx.Observable` - diff --git a/docs/development/core/public/kibana-plugin-public.iuisettingsclient.iscustom.md b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.iscustom.md new file mode 100644 index 0000000000000..30de59c066ee3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.iscustom.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) > [isCustom](./kibana-plugin-public.iuisettingsclient.iscustom.md) + +## IUiSettingsClient.isCustom property + +Returns true if the setting wasn't registered by any plugin, but was either added directly via `set()`, or is an unknown setting found in the uiSettings saved object + +Signature: + +```typescript +isCustom: (key: string) => boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.iuisettingsclient.isdeclared.md b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.isdeclared.md new file mode 100644 index 0000000000000..1ffcb61967e8a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.isdeclared.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) > [isDeclared](./kibana-plugin-public.iuisettingsclient.isdeclared.md) + +## IUiSettingsClient.isDeclared property + +Returns true if the key is a "known" uiSetting, meaning it is either registered by any plugin or was previously added as a custom setting via the `set()` method. + +Signature: + +```typescript +isDeclared: (key: string) => boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.iuisettingsclient.isdefault.md b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.isdefault.md new file mode 100644 index 0000000000000..d61367c9841d4 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.isdefault.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) > [isDefault](./kibana-plugin-public.iuisettingsclient.isdefault.md) + +## IUiSettingsClient.isDefault property + +Returns true if the setting has no user-defined value or is unknown + +Signature: + +```typescript +isDefault: (key: string) => boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.iuisettingsclient.isoverridden.md b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.isoverridden.md new file mode 100644 index 0000000000000..5749e1db1fe43 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.isoverridden.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) > [isOverridden](./kibana-plugin-public.iuisettingsclient.isoverridden.md) + +## IUiSettingsClient.isOverridden property + +Shows whether the uiSettings value set by the user. + +Signature: + +```typescript +isOverridden: (key: string) => boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.iuisettingsclient.md b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.md new file mode 100644 index 0000000000000..4183a30806d9a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.md @@ -0,0 +1,32 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) + +## IUiSettingsClient interface + +Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) + +Signature: + +```typescript +export interface IUiSettingsClient +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [get](./kibana-plugin-public.iuisettingsclient.get.md) | <T = any>(key: string, defaultOverride?: T) => T | Gets the value for a specific uiSetting. If this setting has no user-defined value then the defaultOverride parameter is returned (and parsed if setting is of type "json" or "number). If the parameter is not defined and the key is not registered by any plugin then an error is thrown, otherwise reads the default value defined by a plugin. | +| [get$](./kibana-plugin-public.iuisettingsclient.get_.md) | <T = any>(key: string, defaultOverride?: T) => Observable<T> | Gets an observable of the current value for a config key, and all updates to that config key in the future. Providing a defaultOverride argument behaves the same as it does in \#get() | +| [getAll](./kibana-plugin-public.iuisettingsclient.getall.md) | () => Readonly<Record<string, UiSettingsParams & UserProvidedValues>> | Gets the metadata about all uiSettings, including the type, default value, and user value for each key. | +| [getSaved$](./kibana-plugin-public.iuisettingsclient.getsaved_.md) | <T = any>() => Observable<{
key: string;
newValue: T;
oldValue: T;
}> | Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed. | +| [getUpdate$](./kibana-plugin-public.iuisettingsclient.getupdate_.md) | <T = any>() => Observable<{
key: string;
newValue: T;
oldValue: T;
}> | Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed. | +| [getUpdateErrors$](./kibana-plugin-public.iuisettingsclient.getupdateerrors_.md) | () => Observable<Error> | Returns an Observable that notifies subscribers of each error while trying to update the settings, containing the actual Error class. | +| [isCustom](./kibana-plugin-public.iuisettingsclient.iscustom.md) | (key: string) => boolean | Returns true if the setting wasn't registered by any plugin, but was either added directly via set(), or is an unknown setting found in the uiSettings saved object | +| [isDeclared](./kibana-plugin-public.iuisettingsclient.isdeclared.md) | (key: string) => boolean | Returns true if the key is a "known" uiSetting, meaning it is either registered by any plugin or was previously added as a custom setting via the set() method. | +| [isDefault](./kibana-plugin-public.iuisettingsclient.isdefault.md) | (key: string) => boolean | Returns true if the setting has no user-defined value or is unknown | +| [isOverridden](./kibana-plugin-public.iuisettingsclient.isoverridden.md) | (key: string) => boolean | Shows whether the uiSettings value set by the user. | +| [overrideLocalDefault](./kibana-plugin-public.iuisettingsclient.overridelocaldefault.md) | (key: string, newDefault: any) => void | Overrides the default value for a setting in this specific browser tab. If the page is reloaded the default override is lost. | +| [remove](./kibana-plugin-public.iuisettingsclient.remove.md) | (key: string) => Promise<boolean> | Removes the user-defined value for a setting, causing it to revert to the default. This method behaves the same as calling set(key, null), including the synchronization, custom setting, and error behavior of that method. | +| [set](./kibana-plugin-public.iuisettingsclient.set.md) | (key: string, value: any) => Promise<boolean> | Sets the value for a uiSetting. If the setting is not registered by any plugin it will be stored as a custom setting. The new value will be synchronously available via the get() method and sent to the server in the background. If the request to the server fails then a updateErrors$ will be notified and the setting will be reverted to its value before set() was called. | + diff --git a/docs/development/core/public/kibana-plugin-public.iuisettingsclient.overridelocaldefault.md b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.overridelocaldefault.md new file mode 100644 index 0000000000000..d7e7c01876654 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.overridelocaldefault.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) > [overrideLocalDefault](./kibana-plugin-public.iuisettingsclient.overridelocaldefault.md) + +## IUiSettingsClient.overrideLocalDefault property + +Overrides the default value for a setting in this specific browser tab. If the page is reloaded the default override is lost. + +Signature: + +```typescript +overrideLocalDefault: (key: string, newDefault: any) => void; +``` diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.remove.md b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.remove.md similarity index 51% rename from docs/development/core/public/kibana-plugin-public.uisettingsclient.remove.md rename to docs/development/core/public/kibana-plugin-public.iuisettingsclient.remove.md index 3d07e75449639..c2171e5c883f8 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.remove.md +++ b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.remove.md @@ -1,24 +1,13 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [remove](./kibana-plugin-public.uisettingsclient.remove.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) > [remove](./kibana-plugin-public.iuisettingsclient.remove.md) -## UiSettingsClient.remove() method +## IUiSettingsClient.remove property Removes the user-defined value for a setting, causing it to revert to the default. This method behaves the same as calling `set(key, null)`, including the synchronization, custom setting, and error behavior of that method. Signature: ```typescript -remove(key: string): Promise; +remove: (key: string) => Promise; ``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| key | string | | - -Returns: - -`Promise` - diff --git a/docs/development/core/public/kibana-plugin-public.iuisettingsclient.set.md b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.set.md new file mode 100644 index 0000000000000..d9e62eec4cf08 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.iuisettingsclient.set.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) > [set](./kibana-plugin-public.iuisettingsclient.set.md) + +## IUiSettingsClient.set property + +Sets the value for a uiSetting. If the setting is not registered by any plugin it will be stored as a custom setting. The new value will be synchronously available via the `get()` method and sent to the server in the background. If the request to the server fails then a updateErrors$ will be notified and the setting will be reverted to its value before `set()` was called. + +Signature: + +```typescript +set: (key: string, value: any) => Promise; +``` diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 22794ca945540..f527c92d070de 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -17,7 +17,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state. The client-side SavedObjectsClient is a thin convenience library around the SavedObjects HTTP API for interacting with Saved Objects. | | [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) | This class is a very simple wrapper for SavedObjects loaded from the server with the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md).It provides basic functionality for creating/saving/deleting saved objects, but doesn't include any type-specific implementations. | | [ToastsApi](./kibana-plugin-public.toastsapi.md) | Methods for adding and removing global toast messages. | -| [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | | ## Interfaces @@ -65,6 +64,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IHttpFetchError](./kibana-plugin-public.ihttpfetcherror.md) | | | [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md) | Used to halt a request Promise chain in a [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md). | | [InterceptedHttpResponse](./kibana-plugin-public.interceptedhttpresponse.md) | | +| [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | | [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the ui/new_platform module. | | [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) | Start interface exposed to the legacy platform via the ui/new_platform module. | | [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) | | @@ -126,6 +126,5 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ToastInputFields](./kibana-plugin-public.toastinputfields.md) | Allowed fields for [ToastInput](./kibana-plugin-public.toastinput.md). | | [ToastsSetup](./kibana-plugin-public.toastssetup.md) | [IToasts](./kibana-plugin-public.itoasts.md) | | [ToastsStart](./kibana-plugin-public.toastsstart.md) | [IToasts](./kibana-plugin-public.itoasts.md) | -| [UiSettingsClientContract](./kibana-plugin-public.uisettingsclientcontract.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | | [UnmountCallback](./kibana-plugin-public.unmountcallback.md) | A function that will unmount the element previously mounted by the associated [MountPoint](./kibana-plugin-public.mountpoint.md) | diff --git a/docs/development/core/public/kibana-plugin-public.toastsapi._constructor_.md b/docs/development/core/public/kibana-plugin-public.toastsapi._constructor_.md index 31a16403a41a1..2b5ce41de8ece 100644 --- a/docs/development/core/public/kibana-plugin-public.toastsapi._constructor_.md +++ b/docs/development/core/public/kibana-plugin-public.toastsapi._constructor_.md @@ -10,7 +10,7 @@ Constructs a new instance of the `ToastsApi` class ```typescript constructor(deps: { - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; }); ``` @@ -18,5 +18,5 @@ constructor(deps: { | Parameter | Type | Description | | --- | --- | --- | -| deps | {
uiSettings: UiSettingsClientContract;
} | | +| deps | {
uiSettings: IUiSettingsClient;
} | | diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient._constructor_.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient._constructor_.md deleted file mode 100644 index a7698fe61e162..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient._constructor_.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [(constructor)](./kibana-plugin-public.uisettingsclient._constructor_.md) - -## UiSettingsClient.(constructor) - -Constructs a new instance of the `UiSettingsClient` class - -Signature: - -```typescript -constructor(params: UiSettingsClientParams); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| params | UiSettingsClientParams | | - diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.get.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.get.md deleted file mode 100644 index 03fa38575b6b8..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.get.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [get](./kibana-plugin-public.uisettingsclient.get.md) - -## UiSettingsClient.get() method - -Gets the value for a specific uiSetting. If this setting has no user-defined value then the `defaultOverride` parameter is returned (and parsed if setting is of type "json" or "number). If the parameter is not defined and the key is not defined by a uiSettingDefaults then an error is thrown, otherwise the default is read from the uiSettingDefaults. - -Signature: - -```typescript -get(key: string, defaultOverride?: any): any; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| key | string | | -| defaultOverride | any | | - -Returns: - -`any` - diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.get_.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.get_.md deleted file mode 100644 index 6a515a8f514a2..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.get_.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [get$](./kibana-plugin-public.uisettingsclient.get_.md) - -## UiSettingsClient.get$() method - -Gets an observable of the current value for a config key, and all updates to that config key in the future. Providing a `defaultOverride` argument behaves the same as it does in \#get() - -Signature: - -```typescript -get$(key: string, defaultOverride?: any): Rx.Observable; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| key | string | | -| defaultOverride | any | | - -Returns: - -`Rx.Observable` - diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.getall.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.getall.md deleted file mode 100644 index 06daf8e8151cd..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.getall.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [getAll](./kibana-plugin-public.uisettingsclient.getall.md) - -## UiSettingsClient.getAll() method - -Gets the metadata about all uiSettings, including the type, default value, and user value for each key. - -Signature: - -```typescript -getAll(): Record>; -``` -Returns: - -`Record>` - diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.getsaved_.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.getsaved_.md deleted file mode 100644 index 9e46b286c4009..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.getsaved_.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [getSaved$](./kibana-plugin-public.uisettingsclient.getsaved_.md) - -## UiSettingsClient.getSaved$() method - -Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed. - -Signature: - -```typescript -getSaved$(): Rx.Observable<{ - key: string; - newValue: any; - oldValue: any; - }>; -``` -Returns: - -`Rx.Observable<{ - key: string; - newValue: any; - oldValue: any; - }>` - diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.getupdate_.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.getupdate_.md deleted file mode 100644 index b9cab6e87a996..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.getupdate_.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [getUpdate$](./kibana-plugin-public.uisettingsclient.getupdate_.md) - -## UiSettingsClient.getUpdate$() method - -Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed. - -Signature: - -```typescript -getUpdate$(): Rx.Observable<{ - key: string; - newValue: any; - oldValue: any; - }>; -``` -Returns: - -`Rx.Observable<{ - key: string; - newValue: any; - oldValue: any; - }>` - diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.iscustom.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.iscustom.md deleted file mode 100644 index 8855e39d7e8f3..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.iscustom.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [isCustom](./kibana-plugin-public.uisettingsclient.iscustom.md) - -## UiSettingsClient.isCustom() method - -Returns true if the setting is not a part of the uiSettingDefaults, but was either added directly via `set()`, or is an unknown setting found in the uiSettings saved object - -Signature: - -```typescript -isCustom(key: string): boolean; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| key | string | | - -Returns: - -`boolean` - diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.isdeclared.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.isdeclared.md deleted file mode 100644 index 61b9d3a11a1af..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.isdeclared.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [isDeclared](./kibana-plugin-public.uisettingsclient.isdeclared.md) - -## UiSettingsClient.isDeclared() method - -Returns true if the key is a "known" uiSetting, meaning it is either defined in the uiSettingDefaults or was previously added as a custom setting via the `set()` method. - -Signature: - -```typescript -isDeclared(key: string): boolean; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| key | string | | - -Returns: - -`boolean` - diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.isdefault.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.isdefault.md deleted file mode 100644 index 09a04f99e8285..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.isdefault.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [isDefault](./kibana-plugin-public.uisettingsclient.isdefault.md) - -## UiSettingsClient.isDefault() method - -Returns true if the setting has no user-defined value or is unknown - -Signature: - -```typescript -isDefault(key: string): boolean; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| key | string | | - -Returns: - -`boolean` - diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.isoverridden.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.isoverridden.md deleted file mode 100644 index 5311ffbf40d95..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.isoverridden.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [isOverridden](./kibana-plugin-public.uisettingsclient.isoverridden.md) - -## UiSettingsClient.isOverridden() method - -Returns true if a settings value is overridden by the server. When a setting is overridden its value can not be changed via `set()` or `remove()`. - -Signature: - -```typescript -isOverridden(key: string): boolean; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| key | string | | - -Returns: - -`boolean` - diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.md deleted file mode 100644 index 642e6db144f09..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.md +++ /dev/null @@ -1,38 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) - -## UiSettingsClient class - - -Signature: - -```typescript -export declare class UiSettingsClient -``` - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(params)](./kibana-plugin-public.uisettingsclient._constructor_.md) | | Constructs a new instance of the UiSettingsClient class | - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [get(key, defaultOverride)](./kibana-plugin-public.uisettingsclient.get.md) | | Gets the value for a specific uiSetting. If this setting has no user-defined value then the defaultOverride parameter is returned (and parsed if setting is of type "json" or "number). If the parameter is not defined and the key is not defined by a uiSettingDefaults then an error is thrown, otherwise the default is read from the uiSettingDefaults. | -| [get$(key, defaultOverride)](./kibana-plugin-public.uisettingsclient.get_.md) | | Gets an observable of the current value for a config key, and all updates to that config key in the future. Providing a defaultOverride argument behaves the same as it does in \#get() | -| [getAll()](./kibana-plugin-public.uisettingsclient.getall.md) | | Gets the metadata about all uiSettings, including the type, default value, and user value for each key. | -| [getSaved$()](./kibana-plugin-public.uisettingsclient.getsaved_.md) | | Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed. | -| [getUpdate$()](./kibana-plugin-public.uisettingsclient.getupdate_.md) | | Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed. | -| [getUpdateErrors$()](./kibana-plugin-public.uisettingsclient.getupdateerrors_.md) | | Returns an Observable that notifies subscribers of each error while trying to update the settings, containing the actual Error class. | -| [isCustom(key)](./kibana-plugin-public.uisettingsclient.iscustom.md) | | Returns true if the setting is not a part of the uiSettingDefaults, but was either added directly via set(), or is an unknown setting found in the uiSettings saved object | -| [isDeclared(key)](./kibana-plugin-public.uisettingsclient.isdeclared.md) | | Returns true if the key is a "known" uiSetting, meaning it is either defined in the uiSettingDefaults or was previously added as a custom setting via the set() method. | -| [isDefault(key)](./kibana-plugin-public.uisettingsclient.isdefault.md) | | Returns true if the setting has no user-defined value or is unknown | -| [isOverridden(key)](./kibana-plugin-public.uisettingsclient.isoverridden.md) | | Returns true if a settings value is overridden by the server. When a setting is overridden its value can not be changed via set() or remove(). | -| [overrideLocalDefault(key, newDefault)](./kibana-plugin-public.uisettingsclient.overridelocaldefault.md) | | Overrides the default value for a setting in this specific browser tab. If the page is reloaded the default override is lost. | -| [remove(key)](./kibana-plugin-public.uisettingsclient.remove.md) | | Removes the user-defined value for a setting, causing it to revert to the default. This method behaves the same as calling set(key, null), including the synchronization, custom setting, and error behavior of that method. | -| [set(key, val)](./kibana-plugin-public.uisettingsclient.set.md) | | Sets the value for a uiSetting. If the setting is not defined in the uiSettingDefaults it will be stored as a custom setting. The new value will be synchronously available via the get() method and sent to the server in the background. If the request to the server fails then a toast notification will be displayed and the setting will be reverted it its value before set() was called. | -| [stop()](./kibana-plugin-public.uisettingsclient.stop.md) | | Prepares the uiSettingsClient to be discarded, completing any update$ observables that have been created. | - diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.overridelocaldefault.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.overridelocaldefault.md deleted file mode 100644 index b94fe72fff102..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.overridelocaldefault.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [overrideLocalDefault](./kibana-plugin-public.uisettingsclient.overridelocaldefault.md) - -## UiSettingsClient.overrideLocalDefault() method - -Overrides the default value for a setting in this specific browser tab. If the page is reloaded the default override is lost. - -Signature: - -```typescript -overrideLocalDefault(key: string, newDefault: any): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| key | string | | -| newDefault | any | | - -Returns: - -`void` - diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.set.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.set.md deleted file mode 100644 index ad1d97b8fe9b3..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.set.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [set](./kibana-plugin-public.uisettingsclient.set.md) - -## UiSettingsClient.set() method - -Sets the value for a uiSetting. If the setting is not defined in the uiSettingDefaults it will be stored as a custom setting. The new value will be synchronously available via the `get()` method and sent to the server in the background. If the request to the server fails then a toast notification will be displayed and the setting will be reverted it its value before `set()` was called. - -Signature: - -```typescript -set(key: string, val: any): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| key | string | | -| val | any | | - -Returns: - -`Promise` - diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.stop.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.stop.md deleted file mode 100644 index 215a94544d2d0..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.stop.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) > [stop](./kibana-plugin-public.uisettingsclient.stop.md) - -## UiSettingsClient.stop() method - -Prepares the uiSettingsClient to be discarded, completing any update$ observables that have been created. - -Signature: - -```typescript -stop(): void; -``` -Returns: - -`void` - diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclientcontract.md b/docs/development/core/public/kibana-plugin-public.uisettingsclientcontract.md deleted file mode 100644 index 7173386d88265..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclientcontract.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsClientContract](./kibana-plugin-public.uisettingsclientcontract.md) - -## UiSettingsClientContract type - -Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) - -Signature: - -```typescript -export declare type UiSettingsClientContract = PublicMethodsOf; -``` diff --git a/docs/development/core/server/kibana-plugin-server.contextsetup.md b/docs/development/core/server/kibana-plugin-server.contextsetup.md index 67504faf0534a..1f285efe92b68 100644 --- a/docs/development/core/server/kibana-plugin-server.contextsetup.md +++ b/docs/development/core/server/kibana-plugin-server.contextsetup.md @@ -85,7 +85,7 @@ Say we're creating a plugin for rendering visualizations that allows new renderi export interface VizRenderContext { core: { i18n: I18nStart; - uiSettings: UISettingsClientContract; + uiSettings: IUiSettingsClient; } [contextName: string]: unknown; } diff --git a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.get.md b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.get.md index 0ec3ac45c6cb5..a73061f457a4b 100644 --- a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.get.md +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.get.md @@ -9,5 +9,5 @@ Retrieves uiSettings values set by the user with fallbacks to default values if Signature: ```typescript -get: (key: string) => Promise; +get: (key: string) => Promise; ``` diff --git a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getall.md b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getall.md index d6765a5e5407e..600116b86d1c0 100644 --- a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getall.md +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getall.md @@ -9,5 +9,5 @@ Retrieves a set of all uiSettings values set by the user with fallbacks to defau Signature: ```typescript -getAll: () => Promise>; +getAll: () => Promise>; ``` diff --git a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getuserprovided.md b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getuserprovided.md index 134039cfa91f3..94b7575519cee 100644 --- a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getuserprovided.md +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getuserprovided.md @@ -9,5 +9,5 @@ Retrieves a set of all uiSettings values set by the user. Signature: ```typescript -getUserProvided: () => Promise>>; +getUserProvided: () => Promise>>; ``` diff --git a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.md b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.md index a4697ddbbb85e..c254321e02291 100644 --- a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.md +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.md @@ -16,13 +16,13 @@ export interface IUiSettingsClient | Property | Type | Description | | --- | --- | --- | -| [get](./kibana-plugin-server.iuisettingsclient.get.md) | <T extends SavedObjectAttribute = any>(key: string) => Promise<T> | Retrieves uiSettings values set by the user with fallbacks to default values if not specified. | -| [getAll](./kibana-plugin-server.iuisettingsclient.getall.md) | <T extends SavedObjectAttribute = any>() => Promise<Record<string, T>> | Retrieves a set of all uiSettings values set by the user with fallbacks to default values if not specified. | +| [get](./kibana-plugin-server.iuisettingsclient.get.md) | <T = any>(key: string) => Promise<T> | Retrieves uiSettings values set by the user with fallbacks to default values if not specified. | +| [getAll](./kibana-plugin-server.iuisettingsclient.getall.md) | <T = any>() => Promise<Record<string, T>> | Retrieves a set of all uiSettings values set by the user with fallbacks to default values if not specified. | | [getRegistered](./kibana-plugin-server.iuisettingsclient.getregistered.md) | () => Readonly<Record<string, UiSettingsParams>> | Returns registered uiSettings values [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) | -| [getUserProvided](./kibana-plugin-server.iuisettingsclient.getuserprovided.md) | <T extends SavedObjectAttribute = any>() => Promise<Record<string, UserProvidedValues<T>>> | Retrieves a set of all uiSettings values set by the user. | +| [getUserProvided](./kibana-plugin-server.iuisettingsclient.getuserprovided.md) | <T = any>() => Promise<Record<string, UserProvidedValues<T>>> | Retrieves a set of all uiSettings values set by the user. | | [isOverridden](./kibana-plugin-server.iuisettingsclient.isoverridden.md) | (key: string) => boolean | Shows whether the uiSettings value set by the user. | | [remove](./kibana-plugin-server.iuisettingsclient.remove.md) | (key: string) => Promise<void> | Removes uiSettings value by key. | | [removeMany](./kibana-plugin-server.iuisettingsclient.removemany.md) | (keys: string[]) => Promise<void> | Removes multiple uiSettings values by keys. | -| [set](./kibana-plugin-server.iuisettingsclient.set.md) | <T extends SavedObjectAttribute = any>(key: string, value: T) => Promise<void> | Writes uiSettings value and marks it as set by the user. | -| [setMany](./kibana-plugin-server.iuisettingsclient.setmany.md) | <T extends SavedObjectAttribute = any>(changes: Record<string, T>) => Promise<void> | Writes multiple uiSettings values and marks them as set by the user. | +| [set](./kibana-plugin-server.iuisettingsclient.set.md) | (key: string, value: any) => Promise<void> | Writes uiSettings value and marks it as set by the user. | +| [setMany](./kibana-plugin-server.iuisettingsclient.setmany.md) | (changes: Record<string, any>) => Promise<void> | Writes multiple uiSettings values and marks them as set by the user. | diff --git a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.set.md b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.set.md index bc67d05b3f0ee..5d5897a7159ad 100644 --- a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.set.md +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.set.md @@ -9,5 +9,5 @@ Writes uiSettings value and marks it as set by the user. Signature: ```typescript -set: (key: string, value: T) => Promise; +set: (key: string, value: any) => Promise; ``` diff --git a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.setmany.md b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.setmany.md index ec2c24951f0ec..e1d2595d8e1c7 100644 --- a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.setmany.md +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.setmany.md @@ -9,5 +9,5 @@ Writes multiple uiSettings values and marks them as set by the user. Signature: ```typescript -setMany: (changes: Record) => Promise; +setMany: (changes: Record) => Promise; ``` diff --git a/docs/development/core/server/kibana-plugin-server.userprovidedvalues.md b/docs/development/core/server/kibana-plugin-server.userprovidedvalues.md index 7b2114404d7f2..e0f5f7fadd12f 100644 --- a/docs/development/core/server/kibana-plugin-server.userprovidedvalues.md +++ b/docs/development/core/server/kibana-plugin-server.userprovidedvalues.md @@ -9,7 +9,7 @@ Describes the values explicitly set by user. Signature: ```typescript -export interface UserProvidedValues +export interface UserProvidedValues ``` ## Properties diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index e88f1675114bc..7c1489a345e55 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1246,6 +1246,38 @@ This table shows where these uiExports have moved to in the New Platform. In mos | `visTypes` | | | | `visualize` | | | +Examples: + +- **uiSettingDefaults** + +Before: +```js +uiExports: { + uiSettingDefaults: { + 'my-plugin:my-setting': { + name: 'just-work', + value: true, + description: 'make it work', + category: ['my-category'], + }, + } +} +``` +After: +```ts +// src/plugins/my-plugin/server/plugin.ts +setup(core: CoreSetup){ + core.uiSettings.register({ + 'my-plugin:my-setting': { + name: 'just-work', + value: true, + description: 'make it work', + category: ['my-category'], + }, + }) +} +``` + ## How to ### Configure plugin diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 5be22ea151c32..6313a27b6b821 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -28,7 +28,7 @@ import { I18nStart } from '../i18n'; import { NotificationsStart } from '../notifications'; import { OverlayStart } from '../overlays'; import { PluginOpaqueId } from '../plugins'; -import { UiSettingsClientContract } from '../ui_settings'; +import { IUiSettingsClient } from '../ui_settings'; import { RecursiveReadonly } from '../../utils'; /** @public */ @@ -118,8 +118,8 @@ export interface AppMountContext { notifications: NotificationsStart; /** {@link OverlayStart} */ overlays: OverlayStart; - /** {@link UiSettingsClient} */ - uiSettings: UiSettingsClientContract; + /** {@link IUiSettingsClient} */ + uiSettings: IUiSettingsClient; /** * exposed temporarily until https://github.com/elastic/kibana/issues/41990 done * use *only* to retrieve config values. There is no way to set injected values diff --git a/src/core/public/context/context_service.ts b/src/core/public/context/context_service.ts index e39292f87d7b9..7860b486da959 100644 --- a/src/core/public/context/context_service.ts +++ b/src/core/public/context/context_service.ts @@ -47,7 +47,7 @@ export class ContextService { * export interface VizRenderContext { * core: { * i18n: I18nStart; - * uiSettings: UISettingsClientContract; + * uiSettings: IUiSettingsClient; * } * [contextName: string]: unknown; * } diff --git a/src/core/public/index.ts b/src/core/public/index.ts index c723c282a7caa..cfec03427f3e7 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -62,7 +62,7 @@ import { InjectedMetadataSetup, InjectedMetadataStart, LegacyNavLink } from './i import { NotificationsSetup, NotificationsStart } from './notifications'; import { OverlayStart } from './overlays'; import { Plugin, PluginInitializer, PluginInitializerContext, PluginOpaqueId } from './plugins'; -import { UiSettingsClient, UiSettingsState, UiSettingsClientContract } from './ui_settings'; +import { UiSettingsState, IUiSettingsClient } from './ui_settings'; import { ApplicationSetup, Capabilities, ApplicationStart } from './application'; import { DocLinksStart } from './doc_links'; import { SavedObjectsStart } from './saved_objects'; @@ -157,8 +157,8 @@ export interface CoreSetup { http: HttpSetup; /** {@link NotificationsSetup} */ notifications: NotificationsSetup; - /** {@link UiSettingsClient} */ - uiSettings: UiSettingsClientContract; + /** {@link IUiSettingsClient} */ + uiSettings: IUiSettingsClient; /** * exposed temporarily until https://github.com/elastic/kibana/issues/41990 done * use *only* to retrieve config values. There is no way to set injected values @@ -196,8 +196,8 @@ export interface CoreStart { notifications: NotificationsStart; /** {@link OverlayStart} */ overlays: OverlayStart; - /** {@link UiSettingsClient} */ - uiSettings: UiSettingsClientContract; + /** {@link IUiSettingsClient} */ + uiSettings: IUiSettingsClient; /** * exposed temporarily until https://github.com/elastic/kibana/issues/41990 done * use *only* to retrieve config values. There is no way to set injected values @@ -281,7 +281,6 @@ export { PluginInitializerContext, SavedObjectsStart, PluginOpaqueId, - UiSettingsClient, - UiSettingsClientContract, + IUiSettingsClient, UiSettingsState, }; diff --git a/src/core/public/integrations/integrations_service.ts b/src/core/public/integrations/integrations_service.ts index 5d5c31c2df18c..f85650ced430d 100644 --- a/src/core/public/integrations/integrations_service.ts +++ b/src/core/public/integrations/integrations_service.ts @@ -17,14 +17,14 @@ * under the License. */ -import { UiSettingsClientContract } from '../ui_settings'; +import { IUiSettingsClient } from '../ui_settings'; import { CoreService } from '../../types'; import { MomentService } from './moment'; import { StylesService } from './styles'; interface Deps { - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; } /** @internal */ diff --git a/src/core/public/integrations/moment/moment_service.ts b/src/core/public/integrations/moment/moment_service.ts index 2714750d9a65e..65f2bdea02933 100644 --- a/src/core/public/integrations/moment/moment_service.ts +++ b/src/core/public/integrations/moment/moment_service.ts @@ -21,11 +21,11 @@ import moment from 'moment-timezone'; import { merge, Subscription } from 'rxjs'; import { tap } from 'rxjs/operators'; -import { UiSettingsClientContract } from '../../ui_settings'; +import { IUiSettingsClient } from '../../ui_settings'; import { CoreService } from '../../../types'; interface StartDeps { - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; } /** @internal */ diff --git a/src/core/public/integrations/styles/styles_service.ts b/src/core/public/integrations/styles/styles_service.ts index ba8b812fe9988..41fc861d6cb39 100644 --- a/src/core/public/integrations/styles/styles_service.ts +++ b/src/core/public/integrations/styles/styles_service.ts @@ -19,13 +19,13 @@ import { Subscription } from 'rxjs'; -import { UiSettingsClientContract } from '../../ui_settings'; +import { IUiSettingsClient } from '../../ui_settings'; import { CoreService } from '../../../types'; // @ts-ignore import disableAnimationsCss from '!!raw-loader!./disable_animations.css'; interface StartDeps { - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; } /** @internal */ diff --git a/src/core/public/notifications/notifications_service.ts b/src/core/public/notifications/notifications_service.ts index 2c14f2f650078..82dadf4746567 100644 --- a/src/core/public/notifications/notifications_service.ts +++ b/src/core/public/notifications/notifications_service.ts @@ -22,11 +22,11 @@ import { i18n } from '@kbn/i18n'; import { Subscription } from 'rxjs'; import { I18nStart } from '../i18n'; import { ToastsService, ToastsSetup, ToastsStart } from './toasts'; -import { UiSettingsClientContract } from '../ui_settings'; +import { IUiSettingsClient } from '../ui_settings'; import { OverlayStart } from '../overlays'; interface SetupDeps { - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; } interface StartDeps { diff --git a/src/core/public/notifications/toasts/toasts_api.tsx b/src/core/public/notifications/toasts/toasts_api.tsx index a21b727b02d73..8b1850ff9508f 100644 --- a/src/core/public/notifications/toasts/toasts_api.tsx +++ b/src/core/public/notifications/toasts/toasts_api.tsx @@ -24,7 +24,7 @@ import * as Rx from 'rxjs'; import { ErrorToast } from './error_toast'; import { MountPoint } from '../../types'; import { mountReactNode } from '../../utils'; -import { UiSettingsClientContract } from '../../ui_settings'; +import { IUiSettingsClient } from '../../ui_settings'; import { OverlayStart } from '../../overlays'; import { I18nStart } from '../../i18n'; @@ -94,12 +94,12 @@ export type IToasts = Pick< export class ToastsApi implements IToasts { private toasts$ = new Rx.BehaviorSubject([]); private idCounter = 0; - private uiSettings: UiSettingsClientContract; + private uiSettings: IUiSettingsClient; private overlays?: OverlayStart; private i18n?: I18nStart; - constructor(deps: { uiSettings: UiSettingsClientContract }) { + constructor(deps: { uiSettings: IUiSettingsClient }) { this.uiSettings = deps.uiSettings; } diff --git a/src/core/public/notifications/toasts/toasts_service.tsx b/src/core/public/notifications/toasts/toasts_service.tsx index 81d23afc4f4d3..619a3fe952abb 100644 --- a/src/core/public/notifications/toasts/toasts_service.tsx +++ b/src/core/public/notifications/toasts/toasts_service.tsx @@ -21,13 +21,13 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nStart } from '../../i18n'; -import { UiSettingsClientContract } from '../../ui_settings'; +import { IUiSettingsClient } from '../../ui_settings'; import { GlobalToastList } from './global_toast_list'; import { ToastsApi, IToasts } from './toasts_api'; import { OverlayStart } from '../../overlays'; interface SetupDeps { - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; } interface StartDeps { diff --git a/src/core/public/overlays/banners/banners_service.tsx b/src/core/public/overlays/banners/banners_service.tsx index a26a7c71bc61f..ed59ed819b1c2 100644 --- a/src/core/public/overlays/banners/banners_service.tsx +++ b/src/core/public/overlays/banners/banners_service.tsx @@ -23,7 +23,7 @@ import { map } from 'rxjs/operators'; import { PriorityMap } from './priority_map'; import { BannersList } from './banners_list'; -import { UiSettingsClientContract } from '../../ui_settings'; +import { IUiSettingsClient } from '../../ui_settings'; import { I18nStart } from '../../i18n'; import { MountPoint } from '../../types'; import { UserBannerService } from './user_banner_service'; @@ -73,7 +73,7 @@ export interface OverlayBanner { interface StartDeps { i18n: I18nStart; - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; } /** @internal */ diff --git a/src/core/public/overlays/banners/user_banner_service.tsx b/src/core/public/overlays/banners/user_banner_service.tsx index b258e2127883d..e3f4d9dee5b78 100644 --- a/src/core/public/overlays/banners/user_banner_service.tsx +++ b/src/core/public/overlays/banners/user_banner_service.tsx @@ -27,13 +27,13 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCallOut, EuiButton } from '@elastic/eui'; import { I18nStart } from '../../i18n'; -import { UiSettingsClientContract } from '../../ui_settings'; +import { IUiSettingsClient } from '../../ui_settings'; import { OverlayBannersStart } from './banners_service'; interface StartDeps { banners: OverlayBannersStart; i18n: I18nStart; - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; } /** diff --git a/src/core/public/overlays/overlay_service.ts b/src/core/public/overlays/overlay_service.ts index 82fe753d6f283..f628182e965d8 100644 --- a/src/core/public/overlays/overlay_service.ts +++ b/src/core/public/overlays/overlay_service.ts @@ -18,7 +18,7 @@ */ import { I18nStart } from '../i18n'; -import { UiSettingsClientContract } from '../ui_settings'; +import { IUiSettingsClient } from '../ui_settings'; import { OverlayBannersStart, OverlayBannersService } from './banners'; import { FlyoutService, OverlayFlyoutStart } from './flyout'; import { ModalService, OverlayModalStart } from './modal'; @@ -26,7 +26,7 @@ import { ModalService, OverlayModalStart } from './modal'; interface StartDeps { i18n: I18nStart; targetDomElement: HTMLElement; - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; } /** @internal */ diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 30a98c9046ff5..bde148f1e1e4a 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -63,7 +63,7 @@ export interface AppMountContext { i18n: I18nStart; notifications: NotificationsStart; overlays: OverlayStart; - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; injectedMetadata: { getInjectedVar: (name: string, defaultValue?: any) => unknown; }; @@ -289,7 +289,7 @@ export interface CoreSetup { // (undocumented) notifications: NotificationsSetup; // (undocumented) - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; } // @public @@ -315,7 +315,7 @@ export interface CoreStart { // (undocumented) savedObjects: SavedObjectsStart; // (undocumented) - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; } // @internal @@ -621,6 +621,31 @@ export interface InterceptedHttpResponse { // @public export type IToasts = Pick; +// @public +export interface IUiSettingsClient { + get$: (key: string, defaultOverride?: T) => Observable; + get: (key: string, defaultOverride?: T) => T; + getAll: () => Readonly>; + getSaved$: () => Observable<{ + key: string; + newValue: T; + oldValue: T; + }>; + getUpdate$: () => Observable<{ + key: string; + newValue: T; + oldValue: T; + }>; + getUpdateErrors$: () => Observable; + isCustom: (key: string) => boolean; + isDeclared: (key: string) => boolean; + isDefault: (key: string) => boolean; + isOverridden: (key: string) => boolean; + overrideLocalDefault: (key: string, newDefault: any) => void; + remove: (key: string) => Promise; + set: (key: string, value: any) => Promise; +} + // @public @deprecated export interface LegacyCoreSetup extends CoreSetup { // Warning: (ae-forgotten-export) The symbol "InjectedMetadataSetup" needs to be exported by the entry point index.d.ts @@ -968,7 +993,7 @@ export type ToastInputFields = Pick; - get(key: string, defaultOverride?: any): any; - getAll(): Record>; - getSaved$(): Rx.Observable<{ - key: string; - newValue: any; - oldValue: any; - }>; - getUpdate$(): Rx.Observable<{ - key: string; - newValue: any; - oldValue: any; - }>; - getUpdateErrors$(): Rx.Observable; - isCustom(key: string): boolean; - isDeclared(key: string): boolean; - isDefault(key: string): boolean; - isOverridden(key: string): boolean; - overrideLocalDefault(key: string, newDefault: any): void; - remove(key: string): Promise; - set(key: string, val: any): Promise; - stop(): void; - } - -// @public -export type UiSettingsClientContract = PublicMethodsOf; - // @public (undocumented) export interface UiSettingsState { // (undocumented) diff --git a/src/core/public/ui_settings/__snapshots__/ui_settings_client.test.ts.snap b/src/core/public/ui_settings/__snapshots__/ui_settings_client.test.ts.snap index e49c546f3550c..cd233704d2f54 100644 --- a/src/core/public/ui_settings/__snapshots__/ui_settings_client.test.ts.snap +++ b/src/core/public/ui_settings/__snapshots__/ui_settings_client.test.ts.snap @@ -1,26 +1,26 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`#get after a get for an unknown property, the property is not persisted 1`] = ` -"Unexpected \`config.get(\\"obscureProperty2\\")\` call on unrecognized configuration setting \\"obscureProperty2\\". -Setting an initial value via \`config.set(\\"obscureProperty2\\", value)\` before attempting to retrieve +"Unexpected \`IUiSettingsClient.get(\\"obscureProperty2\\")\` call on unrecognized configuration setting \\"obscureProperty2\\". +Setting an initial value via \`IUiSettingsClient.set(\\"obscureProperty2\\", value)\` before attempting to retrieve any custom setting value for \\"obscureProperty2\\" may fix this issue. -You can use \`config.get(\\"obscureProperty2\\", defaultValue)\`, which will just return +You can use \`IUiSettingsClient.get(\\"obscureProperty2\\", defaultValue)\`, which will just return \`defaultValue\` when the key is unrecognized." `; -exports[`#get gives access to config values 1`] = `"Browser"`; +exports[`#get gives access to uiSettings values 1`] = `"Browser"`; exports[`#get supports the default value overload 1`] = `"default"`; exports[`#get throws on unknown properties that don't have a value yet. 1`] = ` -"Unexpected \`config.get(\\"throwableProperty\\")\` call on unrecognized configuration setting \\"throwableProperty\\". -Setting an initial value via \`config.set(\\"throwableProperty\\", value)\` before attempting to retrieve +"Unexpected \`IUiSettingsClient.get(\\"throwableProperty\\")\` call on unrecognized configuration setting \\"throwableProperty\\". +Setting an initial value via \`IUiSettingsClient.set(\\"throwableProperty\\", value)\` before attempting to retrieve any custom setting value for \\"throwableProperty\\" may fix this issue. -You can use \`config.get(\\"throwableProperty\\", defaultValue)\`, which will just return +You can use \`IUiSettingsClient.get(\\"throwableProperty\\", defaultValue)\`, which will just return \`defaultValue\` when the key is unrecognized." `; -exports[`#getUpdate$ sends { key, newValue, oldValue } notifications when config changes 1`] = ` +exports[`#getUpdate$ sends { key, newValue, oldValue } notifications when client changes 1`] = ` Array [ Array [ Object { @@ -32,7 +32,7 @@ Array [ ] `; -exports[`#getUpdate$ sends { key, newValue, oldValue } notifications when config changes 2`] = ` +exports[`#getUpdate$ sends { key, newValue, oldValue } notifications when client changes 2`] = ` Array [ Array [ Object { diff --git a/src/core/public/ui_settings/__snapshots__/ui_settings_service.test.ts.snap b/src/core/public/ui_settings/__snapshots__/ui_settings_service.test.ts.snap deleted file mode 100644 index 84f9a5ab7c5cd..0000000000000 --- a/src/core/public/ui_settings/__snapshots__/ui_settings_service.test.ts.snap +++ /dev/null @@ -1,117 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`#setup constructs UiSettingsClient and UiSettingsApi: UiSettingsApi args 1`] = ` -[MockFunction MockUiSettingsApi] { - "calls": Array [ - Array [ - Object { - "addLoadingCount": [MockFunction] { - "calls": Array [ - Array [ - Object { - "loadingCountObservable": true, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "anonymousPaths": AnonymousPaths { - "basePath": BasePath { - "basePath": "", - "get": [Function], - "prepend": [Function], - "remove": [Function], - }, - "paths": Set {}, - }, - "basePath": BasePath { - "basePath": "", - "get": [Function], - "prepend": [Function], - "remove": [Function], - }, - "delete": [MockFunction], - "fetch": [MockFunction], - "get": [MockFunction], - "getLoadingCount$": [MockFunction], - "head": [MockFunction], - "intercept": [MockFunction], - "options": [MockFunction], - "patch": [MockFunction], - "post": [MockFunction], - "put": [MockFunction], - "removeAllInterceptors": [MockFunction], - "stop": [MockFunction], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], -} -`; - -exports[`#setup constructs UiSettingsClient and UiSettingsApi: UiSettingsClient args 1`] = ` -[MockFunction MockUiSettingsClient] { - "calls": Array [ - Array [ - Object { - "api": MockUiSettingsApi { - "getLoadingCount$": [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": Object { - "loadingCountObservable": true, - }, - }, - ], - }, - "stop": [MockFunction], - }, - "defaults": Object { - "legacyInjectedUiSettingDefaults": true, - }, - "initialSettings": Object { - "legacyInjectedUiSettingUserValues": true, - }, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], -} -`; - -exports[`#setup passes the uiSettings loading count to the loading count api: http.addLoadingCount calls 1`] = ` -[MockFunction] { - "calls": Array [ - Array [ - Object { - "loadingCountObservable": true, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], -} -`; diff --git a/src/core/public/ui_settings/index.ts b/src/core/public/ui_settings/index.ts index 7f10cfb1539d7..d94f85db31bb6 100644 --- a/src/core/public/ui_settings/index.ts +++ b/src/core/public/ui_settings/index.ts @@ -18,5 +18,5 @@ */ export { UiSettingsService } from './ui_settings_service'; -export { UiSettingsClient, UiSettingsClientContract } from './ui_settings_client'; -export { UiSettingsState } from './types'; +export { UiSettingsClient } from './ui_settings_client'; +export { UiSettingsState, IUiSettingsClient } from './types'; diff --git a/src/core/public/ui_settings/types.ts b/src/core/public/ui_settings/types.ts index 24e87eb04f026..19fd91924f247 100644 --- a/src/core/public/ui_settings/types.ts +++ b/src/core/public/ui_settings/types.ts @@ -17,9 +17,112 @@ * under the License. */ +import { Observable } from 'rxjs'; import { UiSettingsParams, UserProvidedValues } from 'src/core/server/types'; /** @public */ export interface UiSettingsState { [key: string]: UiSettingsParams & UserProvidedValues; } + +/** + * Client-side client that provides access to the advanced settings stored in elasticsearch. + * The settings provide control over the behavior of the Kibana application. + * For example, a user can specify how to display numeric or date fields. + * Users can adjust the settings via Management UI. + * {@link IUiSettingsClient} + * + * @public + */ +export interface IUiSettingsClient { + /** + * Gets the value for a specific uiSetting. If this setting has no user-defined value + * then the `defaultOverride` parameter is returned (and parsed if setting is of type + * "json" or "number). If the parameter is not defined and the key is not registered + * by any plugin then an error is thrown, otherwise reads the default value defined by a plugin. + */ + get: (key: string, defaultOverride?: T) => T; + + /** + * Gets an observable of the current value for a config key, and all updates to that config + * key in the future. Providing a `defaultOverride` argument behaves the same as it does in #get() + */ + get$: (key: string, defaultOverride?: T) => Observable; + + /** + * Gets the metadata about all uiSettings, including the type, default value, and user value + * for each key. + */ + getAll: () => Readonly>; + + /** + * Sets the value for a uiSetting. If the setting is not registered by any plugin + * it will be stored as a custom setting. The new value will be synchronously available via + * the `get()` method and sent to the server in the background. If the request to the + * server fails then a updateErrors$ will be notified and the setting will be + * reverted to its value before `set()` was called. + */ + set: (key: string, value: any) => Promise; + + /** + * Overrides the default value for a setting in this specific browser tab. If the page + * is reloaded the default override is lost. + */ + overrideLocalDefault: (key: string, newDefault: any) => void; + + /** + * Removes the user-defined value for a setting, causing it to revert to the default. This + * method behaves the same as calling `set(key, null)`, including the synchronization, custom + * setting, and error behavior of that method. + */ + remove: (key: string) => Promise; + + /** + * Returns true if the key is a "known" uiSetting, meaning it is either registered + * by any plugin or was previously added as a custom setting via the `set()` method. + */ + isDeclared: (key: string) => boolean; + + /** + * Returns true if the setting has no user-defined value or is unknown + */ + isDefault: (key: string) => boolean; + + /** + * Returns true if the setting wasn't registered by any plugin, but was either + * added directly via `set()`, or is an unknown setting found in the uiSettings saved + * object + */ + isCustom: (key: string) => boolean; + + /** + * Shows whether the uiSettings value set by the user. + */ + isOverridden: (key: string) => boolean; + + /** + * Returns an Observable that notifies subscribers of each update to the uiSettings, + * including the key, newValue, and oldValue of the setting that changed. + */ + getUpdate$: () => Observable<{ + key: string; + newValue: T; + oldValue: T; + }>; + + /** + * Returns an Observable that notifies subscribers of each update to the uiSettings, + * including the key, newValue, and oldValue of the setting that changed. + */ + getSaved$: () => Observable<{ + key: string; + newValue: T; + oldValue: T; + }>; + + /** + * Returns an Observable that notifies subscribers of each error while trying to update + * the settings, containing the actual Error class. + */ + getUpdateErrors$: () => Observable; +} diff --git a/src/core/public/ui_settings/ui_settings_client.test.ts b/src/core/public/ui_settings/ui_settings_client.test.ts index c58ba14d0da3e..f394036e3e046 100644 --- a/src/core/public/ui_settings/ui_settings_client.test.ts +++ b/src/core/public/ui_settings/ui_settings_client.test.ts @@ -16,72 +16,79 @@ * specific language governing permissions and limitations * under the License. */ - +import { Subject } from 'rxjs'; import { materialize, take, toArray } from 'rxjs/operators'; import { UiSettingsClient } from './ui_settings_client'; +let done$: Subject; + function setup(options: { defaults?: any; initialSettings?: any } = {}) { const { defaults = { dateFormat: { value: 'Browser' } }, initialSettings = {} } = options; const batchSet = jest.fn(() => ({ settings: {}, })); - - const config = new UiSettingsClient({ + done$ = new Subject(); + const client = new UiSettingsClient({ defaults, initialSettings, api: { batchSet, } as any, + done$, }); - return { config, batchSet }; + return { client, batchSet }; } +afterEach(() => { + done$.complete(); +}); + describe('#get', () => { - it('gives access to config values', () => { - const { config } = setup(); - expect(config.get('dateFormat')).toMatchSnapshot(); + it('gives access to uiSettings values', () => { + const { client } = setup(); + expect(client.get('dateFormat')).toMatchSnapshot(); }); it('supports the default value overload', () => { - const { config } = setup(); + const { client } = setup(); // default values are consumed and returned atomically - expect(config.get('obscureProperty1', 'default')).toMatchSnapshot(); + expect(client.get('obscureProperty1', 'default')).toMatchSnapshot(); }); it('after a get for an unknown property, the property is not persisted', () => { - const { config } = setup(); - config.get('obscureProperty2', 'default'); + const { client } = setup(); + client.get('obscureProperty2', 'default'); // after a get, default values are NOT persisted - expect(() => config.get('obscureProperty2')).toThrowErrorMatchingSnapshot(); + expect(() => client.get('obscureProperty2')).toThrowErrorMatchingSnapshot(); }); it('honors the default parameter for unset options that are exported', () => { - const { config } = setup(); - // if you are hitting this error, then a test is setting this config value globally and not unsetting it! - expect(config.isDefault('dateFormat')).toBe(true); + const { client } = setup(); + // if you are hitting this error, then a test is setting this client value globally and not unsetting it! + expect(client.isDefault('dateFormat')).toBe(true); - const defaultDateFormat = config.get('dateFormat'); + const defaultDateFormat = client.get('dateFormat'); - expect(config.get('dateFormat', 'xyz')).toBe('xyz'); + expect(client.get('dateFormat', 'xyz')).toBe('xyz'); // shouldn't change other usages - expect(config.get('dateFormat')).toBe(defaultDateFormat); - expect(config.get('dataFormat', defaultDateFormat)).toBe(defaultDateFormat); + expect(client.get('dateFormat')).toBe(defaultDateFormat); + expect(client.get('dataFormat', defaultDateFormat)).toBe(defaultDateFormat); }); it("throws on unknown properties that don't have a value yet.", () => { - const { config } = setup(); - expect(() => config.get('throwableProperty')).toThrowErrorMatchingSnapshot(); + const { client } = setup(); + expect(() => client.get('throwableProperty')).toThrowErrorMatchingSnapshot(); }); }); describe('#get$', () => { it('emits the current value when called', async () => { - const { config } = setup(); - const values = await config + const { client } = setup(); + const values = await client .get$('dateFormat') .pipe(take(1), toArray()) .toPromise(); @@ -90,18 +97,18 @@ describe('#get$', () => { }); it('emits an error notification if the key is unknown', async () => { - const { config } = setup(); - const values = await config + const { client } = setup(); + const values = await client .get$('unknown key') .pipe(materialize()) .toPromise(); expect(values).toMatchInlineSnapshot(` Notification { - "error": [Error: Unexpected \`config.get("unknown key")\` call on unrecognized configuration setting "unknown key". -Setting an initial value via \`config.set("unknown key", value)\` before attempting to retrieve + "error": [Error: Unexpected \`IUiSettingsClient.get("unknown key")\` call on unrecognized configuration setting "unknown key". +Setting an initial value via \`IUiSettingsClient.set("unknown key", value)\` before attempting to retrieve any custom setting value for "unknown key" may fix this issue. -You can use \`config.get("unknown key", defaultValue)\`, which will just return +You can use \`IUiSettingsClient.get("unknown key", defaultValue)\`, which will just return \`defaultValue\` when the key is unrecognized.], "hasValue": false, "kind": "E", @@ -111,13 +118,13 @@ You can use \`config.get("unknown key", defaultValue)\`, which will just return }); it('emits the new value when it changes', async () => { - const { config } = setup(); + const { client } = setup(); setTimeout(() => { - config.set('dateFormat', 'new format'); + client.set('dateFormat', 'new format'); }, 10); - const values = await config + const values = await client .get$('dateFormat') .pipe(take(2), toArray()) .toPromise(); @@ -126,17 +133,17 @@ You can use \`config.get("unknown key", defaultValue)\`, which will just return }); it('emits the default override if no value is set, or if the value is removed', async () => { - const { config } = setup(); + const { client } = setup(); setTimeout(() => { - config.set('dateFormat', 'new format'); + client.set('dateFormat', 'new format'); }, 10); setTimeout(() => { - config.remove('dateFormat'); + client.remove('dateFormat'); }, 20); - const values = await config + const values = await client .get$('dateFormat', 'my default') .pipe(take(3), toArray()) .toPromise(); @@ -146,37 +153,37 @@ You can use \`config.get("unknown key", defaultValue)\`, which will just return }); describe('#set', () => { - it('stores a value in the config val set', () => { - const { config } = setup(); - const original = config.get('dateFormat'); - config.set('dateFormat', 'notaformat'); - expect(config.get('dateFormat')).toBe('notaformat'); - config.set('dateFormat', original); + it('stores a value in the client val set', () => { + const { client } = setup(); + const original = client.get('dateFormat'); + client.set('dateFormat', 'notaformat'); + expect(client.get('dateFormat')).toBe('notaformat'); + client.set('dateFormat', original); }); - it('stores a value in a previously unknown config key', () => { - const { config } = setup(); - expect(() => config.set('unrecognizedProperty', 'somevalue')).not.toThrowError(); - expect(config.get('unrecognizedProperty')).toBe('somevalue'); + it('stores a value in a previously unknown client key', () => { + const { client } = setup(); + expect(() => client.set('unrecognizedProperty', 'somevalue')).not.toThrowError(); + expect(client.get('unrecognizedProperty')).toBe('somevalue'); }); it('resolves to true on success', async () => { - const { config } = setup(); - await expect(config.set('foo', 'bar')).resolves.toBe(true); + const { client } = setup(); + await expect(client.set('foo', 'bar')).resolves.toBe(true); }); it('resolves to false on failure', async () => { - const { config, batchSet } = setup(); + const { client, batchSet } = setup(); batchSet.mockImplementation(() => { throw new Error('Error in request'); }); - await expect(config.set('foo', 'bar')).resolves.toBe(false); + await expect(client.set('foo', 'bar')).resolves.toBe(false); }); it('throws an error if key is overridden', async () => { - const { config } = setup({ + const { client } = setup({ initialSettings: { foo: { isOverridden: true, @@ -184,28 +191,28 @@ describe('#set', () => { }, }, }); - await expect(config.set('foo', true)).rejects.toThrowErrorMatchingSnapshot(); + await expect(client.set('foo', true)).rejects.toThrowErrorMatchingSnapshot(); }); }); describe('#remove', () => { it('resolves to true on success', async () => { - const { config } = setup(); - await expect(config.remove('dateFormat')).resolves.toBe(true); + const { client } = setup(); + await expect(client.remove('dateFormat')).resolves.toBe(true); }); it('resolves to false on failure', async () => { - const { config, batchSet } = setup(); + const { client, batchSet } = setup(); batchSet.mockImplementation(() => { throw new Error('Error in request'); }); - await expect(config.remove('dateFormat')).resolves.toBe(false); + await expect(client.remove('dateFormat')).resolves.toBe(false); }); it('throws an error if key is overridden', async () => { - const { config } = setup({ + const { client } = setup({ initialSettings: { bar: { isOverridden: true, @@ -213,81 +220,81 @@ describe('#remove', () => { }, }, }); - await expect(config.remove('bar')).rejects.toThrowErrorMatchingSnapshot(); + await expect(client.remove('bar')).rejects.toThrowErrorMatchingSnapshot(); }); }); describe('#isDeclared', () => { it('returns true if name is know', () => { - const { config } = setup(); - expect(config.isDeclared('dateFormat')).toBe(true); + const { client } = setup(); + expect(client.isDeclared('dateFormat')).toBe(true); }); it('returns false if name is not known', () => { - const { config } = setup(); - expect(config.isDeclared('dateFormat')).toBe(true); + const { client } = setup(); + expect(client.isDeclared('dateFormat')).toBe(true); }); }); describe('#isDefault', () => { it('returns true if value is default', () => { - const { config } = setup(); - expect(config.isDefault('dateFormat')).toBe(true); + const { client } = setup(); + expect(client.isDefault('dateFormat')).toBe(true); }); it('returns false if name is not known', () => { - const { config } = setup(); - config.set('dateFormat', 'foo'); - expect(config.isDefault('dateFormat')).toBe(false); + const { client } = setup(); + client.set('dateFormat', 'foo'); + expect(client.isDefault('dateFormat')).toBe(false); }); }); describe('#isCustom', () => { it('returns false if name is in from defaults', () => { - const { config } = setup(); - expect(config.isCustom('dateFormat')).toBe(false); + const { client } = setup(); + expect(client.isCustom('dateFormat')).toBe(false); }); it('returns false for unknown name', () => { - const { config } = setup(); - expect(config.isCustom('foo')).toBe(false); + const { client } = setup(); + expect(client.isCustom('foo')).toBe(false); }); it('returns true if name is from unknown set()', () => { - const { config } = setup(); - config.set('foo', 'bar'); - expect(config.isCustom('foo')).toBe(true); + const { client } = setup(); + client.set('foo', 'bar'); + expect(client.isCustom('foo')).toBe(true); }); }); describe('#getUpdate$', () => { - it('sends { key, newValue, oldValue } notifications when config changes', () => { + it('sends { key, newValue, oldValue } notifications when client changes', () => { const handler = jest.fn(); - const { config } = setup(); + const { client } = setup(); - config.getUpdate$().subscribe(handler); + client.getUpdate$().subscribe(handler); expect(handler).not.toHaveBeenCalled(); - config.set('foo', 'bar'); + client.set('foo', 'bar'); expect(handler).toHaveBeenCalledTimes(1); expect(handler.mock.calls).toMatchSnapshot(); handler.mockClear(); - config.set('foo', 'baz'); + client.set('foo', 'baz'); expect(handler).toHaveBeenCalledTimes(1); expect(handler.mock.calls).toMatchSnapshot(); }); it('observables complete when client is stopped', () => { const onComplete = jest.fn(); - const { config } = setup(); + const { client } = setup(); - config.getUpdate$().subscribe({ + client.getUpdate$().subscribe({ complete: onComplete, }); expect(onComplete).not.toHaveBeenCalled(); - config.stop(); + done$.complete(); expect(onComplete).toHaveBeenCalled(); }); }); @@ -295,84 +302,84 @@ describe('#getUpdate$', () => { describe('#overrideLocalDefault', () => { describe('key has no user value', () => { it('synchronously modifies the default value returned by get()', () => { - const { config } = setup(); + const { client } = setup(); - expect(config.get('dateFormat')).toMatchSnapshot('get before override'); - config.overrideLocalDefault('dateFormat', 'bar'); - expect(config.get('dateFormat')).toMatchSnapshot('get after override'); + expect(client.get('dateFormat')).toMatchSnapshot('get before override'); + client.overrideLocalDefault('dateFormat', 'bar'); + expect(client.get('dateFormat')).toMatchSnapshot('get after override'); }); it('synchronously modifies the value returned by getAll()', () => { - const { config } = setup(); + const { client } = setup(); - expect(config.getAll()).toMatchSnapshot('getAll before override'); - config.overrideLocalDefault('dateFormat', 'bar'); - expect(config.getAll()).toMatchSnapshot('getAll after override'); + expect(client.getAll()).toMatchSnapshot('getAll before override'); + client.overrideLocalDefault('dateFormat', 'bar'); + expect(client.getAll()).toMatchSnapshot('getAll after override'); }); it('calls subscriber with new and previous value', () => { const handler = jest.fn(); - const { config } = setup(); + const { client } = setup(); - config.getUpdate$().subscribe(handler); - config.overrideLocalDefault('dateFormat', 'bar'); + client.getUpdate$().subscribe(handler); + client.overrideLocalDefault('dateFormat', 'bar'); expect(handler.mock.calls).toMatchSnapshot('single subscriber call'); }); }); describe('key with user value', () => { it('does not modify the return value of get', () => { - const { config } = setup(); + const { client } = setup(); - config.set('dateFormat', 'foo'); - expect(config.get('dateFormat')).toMatchSnapshot('get before override'); - config.overrideLocalDefault('dateFormat', 'bar'); - expect(config.get('dateFormat')).toMatchSnapshot('get after override'); + client.set('dateFormat', 'foo'); + expect(client.get('dateFormat')).toMatchSnapshot('get before override'); + client.overrideLocalDefault('dateFormat', 'bar'); + expect(client.get('dateFormat')).toMatchSnapshot('get after override'); }); it('is included in the return value of getAll', () => { - const { config } = setup(); + const { client } = setup(); - config.set('dateFormat', 'foo'); - expect(config.getAll()).toMatchSnapshot('getAll before override'); - config.overrideLocalDefault('dateFormat', 'bar'); - expect(config.getAll()).toMatchSnapshot('getAll after override'); + client.set('dateFormat', 'foo'); + expect(client.getAll()).toMatchSnapshot('getAll before override'); + client.overrideLocalDefault('dateFormat', 'bar'); + expect(client.getAll()).toMatchSnapshot('getAll after override'); }); it('does not call subscriber', () => { const handler = jest.fn(); - const { config } = setup(); + const { client } = setup(); - config.set('dateFormat', 'foo'); - config.getUpdate$().subscribe(handler); - config.overrideLocalDefault('dateFormat', 'bar'); + client.set('dateFormat', 'foo'); + client.getUpdate$().subscribe(handler); + client.overrideLocalDefault('dateFormat', 'bar'); expect(handler).not.toHaveBeenCalled(); }); it('returns default override when setting removed', () => { - const { config } = setup(); + const { client } = setup(); - config.set('dateFormat', 'foo'); - config.overrideLocalDefault('dateFormat', 'bar'); + client.set('dateFormat', 'foo'); + client.overrideLocalDefault('dateFormat', 'bar'); - expect(config.get('dateFormat')).toMatchSnapshot('get before override'); - expect(config.getAll()).toMatchSnapshot('getAll before override'); + expect(client.get('dateFormat')).toMatchSnapshot('get before override'); + expect(client.getAll()).toMatchSnapshot('getAll before override'); - config.remove('dateFormat'); + client.remove('dateFormat'); - expect(config.get('dateFormat')).toMatchSnapshot('get after override'); - expect(config.getAll()).toMatchSnapshot('getAll after override'); + expect(client.get('dateFormat')).toMatchSnapshot('get after override'); + expect(client.getAll()).toMatchSnapshot('getAll after override'); }); }); describe('#isOverridden()', () => { it('returns false if key is unknown', () => { - const { config } = setup(); - expect(config.isOverridden('foo')).toBe(false); + const { client } = setup(); + expect(client.isOverridden('foo')).toBe(false); }); it('returns false if key is no overridden', () => { - const { config } = setup({ + const { client } = setup({ initialSettings: { foo: { userValue: 1, @@ -383,11 +390,11 @@ describe('#overrideLocalDefault', () => { }, }, }); - expect(config.isOverridden('foo')).toBe(false); + expect(client.isOverridden('foo')).toBe(false); }); it('returns true when key is overridden', () => { - const { config } = setup({ + const { client } = setup({ initialSettings: { foo: { userValue: 1, @@ -398,12 +405,12 @@ describe('#overrideLocalDefault', () => { }, }, }); - expect(config.isOverridden('bar')).toBe(true); + expect(client.isOverridden('bar')).toBe(true); }); it('returns false for object prototype properties', () => { - const { config } = setup(); - expect(config.isOverridden('hasOwnProperty')).toBe(false); + const { client } = setup(); + expect(client.isOverridden('hasOwnProperty')).toBe(false); }); }); }); diff --git a/src/core/public/ui_settings/ui_settings_client.ts b/src/core/public/ui_settings/ui_settings_client.ts index c3190847130d5..f0071ed08435c 100644 --- a/src/core/public/ui_settings/ui_settings_client.ts +++ b/src/core/public/ui_settings/ui_settings_client.ts @@ -18,37 +18,25 @@ */ import { cloneDeep, defaultsDeep } from 'lodash'; -import * as Rx from 'rxjs'; +import { Observable, Subject, concat, defer, of } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { UiSettingsParams, UserProvidedValues } from 'src/core/server/types'; -import { UiSettingsState } from './types'; +import { IUiSettingsClient, UiSettingsState } from './types'; import { UiSettingsApi } from './ui_settings_api'; -/** @public */ interface UiSettingsClientParams { api: UiSettingsApi; defaults: Record; initialSettings?: UiSettingsState; + done$: Observable; } -/** - * Client-side client that provides access to the advanced settings stored in elasticsearch. - * The settings provide control over the behavior of the Kibana application. - * For example, a user can specify how to display numeric or date fields. - * Users can adjust the settings via Management UI. - * {@link UiSettingsClient} - * - * @public - */ -export type UiSettingsClientContract = PublicMethodsOf; - -/** @public */ -export class UiSettingsClient { - private readonly update$ = new Rx.Subject<{ key: string; newValue: any; oldValue: any }>(); - private readonly saved$ = new Rx.Subject<{ key: string; newValue: any; oldValue: any }>(); - private readonly updateErrors$ = new Rx.Subject(); +export class UiSettingsClient implements IUiSettingsClient { + private readonly update$ = new Subject<{ key: string; newValue: any; oldValue: any }>(); + private readonly saved$ = new Subject<{ key: string; newValue: any; oldValue: any }>(); + private readonly updateErrors$ = new Subject(); private readonly api: UiSettingsApi; private readonly defaults: Record; @@ -58,24 +46,21 @@ export class UiSettingsClient { this.api = params.api; this.defaults = cloneDeep(params.defaults); this.cache = defaultsDeep({}, this.defaults, cloneDeep(params.initialSettings)); + + params.done$.subscribe({ + complete: () => { + this.update$.complete(); + this.saved$.complete(); + this.updateErrors$.complete(); + }, + }); } - /** - * Gets the metadata about all uiSettings, including the type, default value, and user value - * for each key. - */ - public getAll() { + getAll() { return cloneDeep(this.cache); } - /** - * Gets the value for a specific uiSetting. If this setting has no user-defined value - * then the `defaultOverride` parameter is returned (and parsed if setting is of type - * "json" or "number). If the parameter is not defined and the key is not defined by a - * uiSettingDefaults then an error is thrown, otherwise the default is read - * from the uiSettingDefaults. - */ - public get(key: string, defaultOverride?: any) { + get(key: string, defaultOverride?: T) { const declared = this.isDeclared(key); if (!declared && defaultOverride !== undefined) { @@ -84,10 +69,10 @@ export class UiSettingsClient { if (!declared) { throw new Error( - `Unexpected \`config.get("${key}")\` call on unrecognized configuration setting "${key}". -Setting an initial value via \`config.set("${key}", value)\` before attempting to retrieve + `Unexpected \`IUiSettingsClient.get("${key}")\` call on unrecognized configuration setting "${key}". +Setting an initial value via \`IUiSettingsClient.set("${key}", value)\` before attempting to retrieve any custom setting value for "${key}" may fix this issue. -You can use \`config.get("${key}", defaultValue)\`, which will just return +You can use \`IUiSettingsClient.get("${key}", defaultValue)\`, which will just return \`defaultValue\` when the key is unrecognized.` ); } @@ -108,13 +93,9 @@ You can use \`config.get("${key}", defaultValue)\`, which will just return return value; } - /** - * Gets an observable of the current value for a config key, and all updates to that config - * key in the future. Providing a `defaultOverride` argument behaves the same as it does in #get() - */ - public get$(key: string, defaultOverride?: any) { - return Rx.concat( - Rx.defer(() => Rx.of(this.get(key, defaultOverride))), + get$(key: string, defaultOverride?: T) { + return concat( + defer(() => of(this.get(key, defaultOverride))), this.update$.pipe( filter(update => update.key === key), map(() => this.get(key, defaultOverride)) @@ -122,63 +103,31 @@ You can use \`config.get("${key}", defaultValue)\`, which will just return ); } - /** - * Sets the value for a uiSetting. If the setting is not defined in the uiSettingDefaults - * it will be stored as a custom setting. The new value will be synchronously available via - * the `get()` method and sent to the server in the background. If the request to the - * server fails then a toast notification will be displayed and the setting will be - * reverted it its value before `set()` was called. - */ - public async set(key: string, val: any) { - return await this.update(key, val); + async set(key: string, value: any) { + return await this.update(key, value); } - /** - * Removes the user-defined value for a setting, causing it to revert to the default. This - * method behaves the same as calling `set(key, null)`, including the synchronization, custom - * setting, and error behavior of that method. - */ - public async remove(key: string) { + async remove(key: string) { return await this.update(key, null); } - /** - * Returns true if the key is a "known" uiSetting, meaning it is either defined in the - * uiSettingDefaults or was previously added as a custom setting via the `set()` method. - */ - public isDeclared(key: string) { + isDeclared(key: string) { return key in this.cache; } - /** - * Returns true if the setting has no user-defined value or is unknown - */ - public isDefault(key: string) { + isDefault(key: string) { return !this.isDeclared(key) || this.cache[key].userValue == null; } - /** - * Returns true if the setting is not a part of the uiSettingDefaults, but was either - * added directly via `set()`, or is an unknown setting found in the uiSettings saved - * object - */ - public isCustom(key: string) { + isCustom(key: string) { return this.isDeclared(key) && !('value' in this.cache[key]); } - /** - * Returns true if a settings value is overridden by the server. When a setting is overridden - * its value can not be changed via `set()` or `remove()`. - */ - public isOverridden(key: string) { + isOverridden(key: string) { return this.isDeclared(key) && Boolean(this.cache[key].isOverridden); } - /** - * Overrides the default value for a setting in this specific browser tab. If the page - * is reloaded the default override is lost. - */ - public overrideLocalDefault(key: string, newDefault: any) { + overrideLocalDefault(key: string, newDefault: any) { // capture the previous value const prevDefault = this.defaults[key] ? this.defaults[key].value : undefined; @@ -201,39 +150,18 @@ You can use \`config.get("${key}", defaultValue)\`, which will just return } } - /** - * Returns an Observable that notifies subscribers of each update to the uiSettings, - * including the key, newValue, and oldValue of the setting that changed. - */ - public getUpdate$() { + getUpdate$() { return this.update$.asObservable(); } - /** - * Returns an Observable that notifies subscribers of each update to the uiSettings, - * including the key, newValue, and oldValue of the setting that changed. - */ - public getSaved$() { + getSaved$() { return this.saved$.asObservable(); } - /** - * Returns an Observable that notifies subscribers of each error while trying to update - * the settings, containing the actual Error class. - */ - public getUpdateErrors$() { + getUpdateErrors$() { return this.updateErrors$.asObservable(); } - /** - * Prepares the uiSettingsClient to be discarded, completing any update$ observables - * that have been created. - */ - public stop() { - this.update$.complete(); - this.saved$.complete(); - } - private assertUpdateAllowed(key: string) { if (this.isOverridden(key)) { throw new Error( @@ -242,7 +170,7 @@ You can use \`config.get("${key}", defaultValue)\`, which will just return } } - private async update(key: string, newVal: any) { + private async update(key: string, newVal: any): Promise { this.assertUpdateAllowed(key); const declared = this.isDeclared(key); diff --git a/src/core/public/ui_settings/ui_settings_service.mock.ts b/src/core/public/ui_settings/ui_settings_service.mock.ts index 2ec6175ff67d5..27dde2f10703e 100644 --- a/src/core/public/ui_settings/ui_settings_service.mock.ts +++ b/src/core/public/ui_settings/ui_settings_service.mock.ts @@ -17,10 +17,11 @@ * under the License. */ import * as Rx from 'rxjs'; -import { UiSettingsService, UiSettingsClientContract } from './'; +import { UiSettingsService } from './'; +import { IUiSettingsClient } from './types'; const createSetupContractMock = () => { - const setupContract: jest.Mocked = { + const setupContract: jest.Mocked = { getAll: jest.fn(), get: jest.fn(), get$: jest.fn(), @@ -34,7 +35,6 @@ const createSetupContractMock = () => { getUpdate$: jest.fn(), getSaved$: jest.fn(), getUpdateErrors$: jest.fn(), - stop: jest.fn(), }; setupContract.get$.mockReturnValue(new Rx.Subject()); setupContract.getUpdate$.mockReturnValue(new Rx.Subject()); diff --git a/src/core/public/ui_settings/ui_settings_service.test.mocks.ts b/src/core/public/ui_settings/ui_settings_service.test.mocks.ts deleted file mode 100644 index b94401c5928df..0000000000000 --- a/src/core/public/ui_settings/ui_settings_service.test.mocks.ts +++ /dev/null @@ -1,57 +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. - */ - -function mockClass( - module: string, - Class: new (...args: any[]) => T, - setup: (instance: any, args: any[]) => void -) { - const MockClass = jest.fn(function(this: any, ...args: any[]) { - setup(this, args); - }); - - // define the mock name which is used in some snapshots - MockClass.mockName(`Mock${Class.name}`); - - // define the class name for the MockClass which is used in other snapshots - Object.defineProperty(MockClass, 'name', { - value: `Mock${Class.name}`, - }); - - jest.doMock(module, () => ({ - [Class.name]: MockClass, - })); - - return MockClass; -} - -// Mock the UiSettingsApi class -import { UiSettingsApi } from './ui_settings_api'; -export const MockUiSettingsApi = mockClass('./ui_settings_api', UiSettingsApi, inst => { - inst.stop = jest.fn(); - inst.getLoadingCount$ = jest.fn().mockReturnValue({ - loadingCountObservable: true, - }); -}); - -// Mock the UiSettingsClient class -import { UiSettingsClient } from './ui_settings_client'; -export const MockUiSettingsClient = mockClass('./ui_settings_client', UiSettingsClient, inst => { - inst.stop = jest.fn(); -}); diff --git a/src/core/public/ui_settings/ui_settings_service.test.ts b/src/core/public/ui_settings/ui_settings_service.test.ts index 94e5e6e2418be..afb68c4844901 100644 --- a/src/core/public/ui_settings/ui_settings_service.test.ts +++ b/src/core/public/ui_settings/ui_settings_service.test.ts @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - -import { MockUiSettingsApi, MockUiSettingsClient } from './ui_settings_service.test.mocks'; +import * as Rx from 'rxjs'; import { httpServiceMock } from '../http/http_service.mock'; import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; @@ -30,53 +29,27 @@ const defaultDeps = { injectedMetadata: injectedMetadataServiceMock.createSetupContract(), }; -afterEach(() => { - jest.clearAllMocks(); -}); - -describe('#setup', () => { - it('returns an instance of UiSettingsClient', () => { - const setup = new UiSettingsService().setup(defaultDeps); - expect(setup).toBeInstanceOf(MockUiSettingsClient); - }); - - it('constructs UiSettingsClient and UiSettingsApi', () => { - new UiSettingsService().setup(defaultDeps); - - expect(MockUiSettingsApi).toMatchSnapshot('UiSettingsApi args'); - expect(MockUiSettingsClient).toMatchSnapshot('UiSettingsClient args'); - }); - - it('passes the uiSettings loading count to the loading count api', () => { - new UiSettingsService().setup(defaultDeps); - - expect(httpSetup.addLoadingCount).toMatchSnapshot('http.addLoadingCount calls'); - }); -}); - -describe('#start', () => { - it('returns an instance of UiSettingsClient', () => { - const uiSettings = new UiSettingsService(); - uiSettings.setup(defaultDeps); - const start = uiSettings.start(); - expect(start).toBeInstanceOf(MockUiSettingsClient); - }); -}); - describe('#stop', () => { it('runs fine if service never set up', () => { const service = new UiSettingsService(); expect(() => service.stop()).not.toThrowError(); }); - it('stops the uiSettingsClient and uiSettingsApi', () => { + it('stops the uiSettingsClient and uiSettingsApi', async () => { const service = new UiSettingsService(); + let loadingCount$: Rx.Observable; + defaultDeps.http.addLoadingCount.mockImplementation(obs$ => (loadingCount$ = obs$)); const client = service.setup(defaultDeps); - const [[{ api }]] = MockUiSettingsClient.mock.calls; - jest.spyOn(client, 'stop'); - jest.spyOn(api, 'stop'); + service.stop(); - expect(api.stop).toHaveBeenCalledTimes(1); - expect(client.stop).toHaveBeenCalledTimes(1); + + await expect( + Rx.combineLatest( + client.getUpdate$(), + client.getSaved$(), + client.getUpdateErrors$(), + loadingCount$! + ).toPromise() + ).resolves.toBe(undefined); }); }); diff --git a/src/core/public/ui_settings/ui_settings_service.ts b/src/core/public/ui_settings/ui_settings_service.ts index 2efb0884312d8..5a03cd1cfeedc 100644 --- a/src/core/public/ui_settings/ui_settings_service.ts +++ b/src/core/public/ui_settings/ui_settings_service.ts @@ -16,12 +16,14 @@ * specific language governing permissions and limitations * under the License. */ +import { Subject } from 'rxjs'; import { HttpSetup } from '../http'; import { InjectedMetadataSetup } from '../injected_metadata'; import { UiSettingsApi } from './ui_settings_api'; -import { UiSettingsClient, UiSettingsClientContract } from './ui_settings_client'; +import { UiSettingsClient } from './ui_settings_client'; +import { IUiSettingsClient } from './types'; interface UiSettingsServiceDeps { http: HttpSetup; @@ -32,8 +34,9 @@ interface UiSettingsServiceDeps { export class UiSettingsService { private uiSettingsApi?: UiSettingsApi; private uiSettingsClient?: UiSettingsClient; + private done$ = new Subject(); - public setup({ http, injectedMetadata }: UiSettingsServiceDeps): UiSettingsClientContract { + public setup({ http, injectedMetadata }: UiSettingsServiceDeps): IUiSettingsClient { this.uiSettingsApi = new UiSettingsApi(http); http.addLoadingCount(this.uiSettingsApi.getLoadingCount$()); @@ -44,19 +47,18 @@ export class UiSettingsService { api: this.uiSettingsApi, defaults: legacyMetadata.uiSettings.defaults, initialSettings: legacyMetadata.uiSettings.user, + done$: this.done$, }); return this.uiSettingsClient; } - public start(): UiSettingsClientContract { + public start(): IUiSettingsClient { return this.uiSettingsClient!; } public stop() { - if (this.uiSettingsClient) { - this.uiSettingsClient.stop(); - } + this.done$.complete(); if (this.uiSettingsApi) { this.uiSettingsApi.stop(); diff --git a/src/core/server/context/context_service.ts b/src/core/server/context/context_service.ts index 1625fc9ad75ed..bbb7660bf9615 100644 --- a/src/core/server/context/context_service.ts +++ b/src/core/server/context/context_service.ts @@ -48,7 +48,7 @@ export class ContextService { * export interface VizRenderContext { * core: { * i18n: I18nStart; - * uiSettings: UISettingsClientContract; + * uiSettings: IUiSettingsClient; * } * [contextName: string]: unknown; * } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 25ca8ade77aca..c8a68b4e2ea2a 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -742,15 +742,15 @@ export type IScopedClusterClient = Pick(key: string) => Promise; - getAll: () => Promise>; + get: (key: string) => Promise; + getAll: () => Promise>; getRegistered: () => Readonly>; - getUserProvided: () => Promise>>; + getUserProvided: () => Promise>>; isOverridden: (key: string) => boolean; remove: (key: string) => Promise; removeMany: (keys: string[]) => Promise; - set: (key: string, value: T) => Promise; - setMany: (changes: Record) => Promise; + set: (key: string, value: any) => Promise; + setMany: (changes: Record) => Promise; } // @public @@ -1713,7 +1713,7 @@ export interface UiSettingsServiceSetup { export type UiSettingsType = 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string'; // @public -export interface UserProvidedValues { +export interface UserProvidedValues { // (undocumented) isOverridden?: boolean; // (undocumented) diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts index 809e15248b5b0..0544a1806e09a 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts @@ -19,7 +19,7 @@ import { defaults } from 'lodash'; -import { SavedObjectsClientContract, SavedObjectAttribute } from '../../saved_objects/types'; +import { SavedObjectsClientContract } from '../../saved_objects/types'; import { SavedObjectsErrorHelpers } from '../../saved_objects/'; import { Logger } from '../../logging'; @@ -33,9 +33,9 @@ interface Options { handleWriteErrors: boolean; } -export async function createOrUpgradeSavedConfig( +export async function createOrUpgradeSavedConfig( options: Options -): Promise | undefined> { +): Promise | undefined> { const { savedObjectsClient, version, buildNum, log, handleWriteErrors } = options; // try to find an older config we can upgrade diff --git a/src/core/server/ui_settings/types.ts b/src/core/server/ui_settings/types.ts index 0fa6b3702af24..49d3d3b33392f 100644 --- a/src/core/server/ui_settings/types.ts +++ b/src/core/server/ui_settings/types.ts @@ -33,25 +33,23 @@ export interface IUiSettingsClient { /** * Retrieves uiSettings values set by the user with fallbacks to default values if not specified. */ - get: (key: string) => Promise; + get: (key: string) => Promise; /** * Retrieves a set of all uiSettings values set by the user with fallbacks to default values if not specified. */ - getAll: () => Promise>; + getAll: () => Promise>; /** * Retrieves a set of all uiSettings values set by the user. */ - getUserProvided: () => Promise< - Record> - >; + getUserProvided: () => Promise>>; /** * Writes multiple uiSettings values and marks them as set by the user. */ - setMany: (changes: Record) => Promise; + setMany: (changes: Record) => Promise; /** * Writes uiSettings value and marks it as set by the user. */ - set: (key: string, value: T) => Promise; + set: (key: string, value: any) => Promise; /** * Removes uiSettings value by key. */ @@ -70,7 +68,7 @@ export interface IUiSettingsClient { * Describes the values explicitly set by user. * @public * */ -export interface UserProvidedValues { +export interface UserProvidedValues { userValue?: T; isOverridden?: boolean; } diff --git a/src/core/server/ui_settings/ui_settings_client.ts b/src/core/server/ui_settings/ui_settings_client.ts index 1a0f29f6ae6d9..3c9c232bff280 100644 --- a/src/core/server/ui_settings/ui_settings_client.ts +++ b/src/core/server/ui_settings/ui_settings_client.ts @@ -19,7 +19,7 @@ import { defaultsDeep } from 'lodash'; import { SavedObjectsErrorHelpers } from '../saved_objects'; -import { SavedObjectsClientContract, SavedObjectAttribute } from '../saved_objects/types'; +import { SavedObjectsClientContract } from '../saved_objects/types'; import { Logger } from '../logging'; import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; import { IUiSettingsClient, UiSettingsParams } from './types'; @@ -30,7 +30,7 @@ export interface UiSettingsServiceOptions { id: string; buildNum: number; savedObjectsClient: SavedObjectsClientContract; - overrides?: Record; + overrides?: Record; defaults?: Record; log: Logger; } @@ -40,14 +40,14 @@ interface ReadOptions { autoCreateOrUpgradeIfMissing?: boolean; } -interface UserProvidedValue { +interface UserProvidedValue { userValue?: T; isOverridden?: boolean; } type UiSettingsRawValue = UiSettingsParams & UserProvidedValue; -type UserProvided = Record>; +type UserProvided = Record>; type UiSettingsRaw = Record; export class UiSettingsClient implements IUiSettingsClient { @@ -75,12 +75,12 @@ export class UiSettingsClient implements IUiSettingsClient { return this.defaults; } - async get(key: string): Promise { + async get(key: string): Promise { const all = await this.getAll(); return all[key]; } - async getAll() { + async getAll() { const raw = await this.getRaw(); return Object.keys(raw).reduce((all, key) => { @@ -90,7 +90,7 @@ export class UiSettingsClient implements IUiSettingsClient { }, {} as Record); } - async getUserProvided(): Promise> { + async getUserProvided(): Promise> { const userProvided: UserProvided = {}; // write the userValue for each key stored in the saved object that is not overridden @@ -112,11 +112,11 @@ export class UiSettingsClient implements IUiSettingsClient { return userProvided; } - async setMany(changes: Record) { + async setMany(changes: Record) { await this.write({ changes }); } - async set(key: string, value: T) { + async set(key: string, value: any) { await this.setMany({ [key]: value }); } @@ -147,11 +147,11 @@ export class UiSettingsClient implements IUiSettingsClient { return defaultsDeep(userProvided, this.defaults); } - private async write({ + private async write({ changes, autoCreateOrUpgradeIfMissing = true, }: { - changes: Record; + changes: Record; autoCreateOrUpgradeIfMissing?: boolean; }) { for (const key of Object.keys(changes)) { @@ -180,16 +180,16 @@ export class UiSettingsClient implements IUiSettingsClient { } } - private async read({ + private async read({ ignore401Errors = false, autoCreateOrUpgradeIfMissing = true, - }: ReadOptions = {}): Promise> { + }: ReadOptions = {}): Promise> { try { const resp = await this.savedObjectsClient.get(this.type, this.id); return resp.attributes; } catch (error) { if (SavedObjectsErrorHelpers.isNotFoundError(error) && autoCreateOrUpgradeIfMissing) { - const failedUpgradeAttributes = await createOrUpgradeSavedConfig({ + const failedUpgradeAttributes = await createOrUpgradeSavedConfig({ savedObjectsClient: this.savedObjectsClient, version: this.id, buildNum: this.buildNum, diff --git a/src/core/server/ui_settings/ui_settings_service.ts b/src/core/server/ui_settings/ui_settings_service.ts index a8f5663f8bd1e..8458a80de4952 100644 --- a/src/core/server/ui_settings/ui_settings_service.ts +++ b/src/core/server/ui_settings/ui_settings_service.ts @@ -23,7 +23,7 @@ import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; -import { SavedObjectsClientContract, SavedObjectAttribute } from '../saved_objects/types'; +import { SavedObjectsClientContract } from '../saved_objects/types'; import { InternalHttpServiceSetup } from '../http'; import { UiSettingsConfigType } from './ui_settings_config'; import { UiSettingsClient } from './ui_settings_client'; @@ -84,7 +84,7 @@ export class UiSettingsService implements CoreService = config.overrides; + const overrides: Record = config.overrides; // manually implemented deprecation until New platform Config service // supports them https://github.com/elastic/kibana/issues/40255 if (typeof deps.http.config.defaultRoute !== 'undefined') { diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts index 2ad0a1f1394e5..591290065d024 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts @@ -19,11 +19,7 @@ // eslint-disable-next-line max-classes-per-file import { IndexPatterns } from './index_patterns'; -import { - SavedObjectsClientContract, - UiSettingsClientContract, - HttpServiceBase, -} from 'kibana/public'; +import { SavedObjectsClientContract, IUiSettingsClient, HttpServiceBase } from 'kibana/public'; jest.mock('./index_pattern', () => { class IndexPattern { @@ -52,7 +48,7 @@ describe('IndexPatterns', () => { beforeEach(() => { const savedObjectsClient = {} as SavedObjectsClientContract; - const uiSettings = {} as UiSettingsClientContract; + const uiSettings = {} as IUiSettingsClient; const http = {} as HttpServiceBase; indexPatterns = new IndexPatterns(uiSettings, savedObjectsClient, http); diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.ts index c8e80b3aede20..d6a8e7b20451d 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.ts @@ -21,7 +21,7 @@ import { idx } from '@kbn/elastic-idx'; import { SavedObjectsClientContract, SimpleSavedObject, - UiSettingsClientContract, + IUiSettingsClient, HttpServiceBase, } from 'src/core/public'; @@ -32,13 +32,13 @@ import { IndexPatternsApiClient, GetFieldsOptions } from './index_patterns_api_c const indexPatternCache = createIndexPatternCache(); export class IndexPatterns { - private config: UiSettingsClientContract; + private config: IUiSettingsClient; private savedObjectsClient: SavedObjectsClientContract; private savedObjectsCache?: Array>> | null; private apiClient: IndexPatternsApiClient; constructor( - config: UiSettingsClientContract, + config: IUiSettingsClient, savedObjectsClient: SavedObjectsClientContract, http: HttpServiceBase ) { diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts index 9973a7081443d..83738ffe5b747 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts @@ -18,7 +18,7 @@ */ import { - UiSettingsClientContract, + IUiSettingsClient, SavedObjectsClientContract, HttpServiceBase, NotificationsStart, @@ -36,7 +36,7 @@ import { } from './index_patterns'; export interface IndexPatternDependencies { - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; savedObjectsClient: SavedObjectsClientContract; http: HttpServiceBase; notifications: NotificationsStart; diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_string_input.test.tsx.snap b/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_string_input.test.tsx.snap index 6f155de95d6eb..61aac70b4a7ec 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_string_input.test.tsx.snap +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_string_input.test.tsx.snap @@ -344,7 +344,6 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "overrideLocalDefault": [MockFunction], "remove": [MockFunction], "set": [MockFunction], - "stop": [MockFunction], }, } } @@ -907,7 +906,6 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "overrideLocalDefault": [MockFunction], "remove": [MockFunction], "set": [MockFunction], - "stop": [MockFunction], }, }, } @@ -1458,7 +1456,6 @@ exports[`QueryStringInput Should pass the query language to the language switche "overrideLocalDefault": [MockFunction], "remove": [MockFunction], "set": [MockFunction], - "stop": [MockFunction], }, } } @@ -2018,7 +2015,6 @@ exports[`QueryStringInput Should pass the query language to the language switche "overrideLocalDefault": [MockFunction], "remove": [MockFunction], "set": [MockFunction], - "stop": [MockFunction], }, }, } @@ -2569,7 +2565,6 @@ exports[`QueryStringInput Should render the given query 1`] = ` "overrideLocalDefault": [MockFunction], "remove": [MockFunction], "set": [MockFunction], - "stop": [MockFunction], }, } } @@ -3129,7 +3124,6 @@ exports[`QueryStringInput Should render the given query 1`] = ` "overrideLocalDefault": [MockFunction], "remove": [MockFunction], "set": [MockFunction], - "stop": [MockFunction], }, }, } diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/fetch_index_patterns.ts b/src/legacy/core_plugins/data/public/query/query_bar/components/fetch_index_patterns.ts index 4cf17dc9be37e..3dcab22605c07 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/fetch_index_patterns.ts +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/fetch_index_patterns.ts @@ -17,13 +17,13 @@ * under the License. */ import { isEmpty } from 'lodash'; -import { UiSettingsClientContract, SavedObjectsClientContract } from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract } from 'src/core/public'; import { getFromSavedObject } from '../../../index_patterns'; export async function fetchIndexPatterns( savedObjectsClient: SavedObjectsClientContract, indexPatternStrings: string[], - uiSettings: UiSettingsClientContract + uiSettings: IUiSettingsClient ) { if (!indexPatternStrings || isEmpty(indexPatternStrings)) { return []; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/application.ts index f98a4ca53f467..797583362a8f8 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/application.ts @@ -26,7 +26,7 @@ import { ChromeStart, LegacyCoreStart, SavedObjectsClientContract, - UiSettingsClientContract, + IUiSettingsClient, } from 'kibana/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; import { @@ -64,7 +64,7 @@ export interface RenderDeps { dashboardConfig: any; savedDashboards: any; dashboardCapabilities: any; - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; chrome: ChromeStart; addBasePath: (path: string) => string; savedQueryService: NpDataStart['query']['savedQueries']; diff --git a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts index 5ef6e019db042..3ec095f4f26bf 100644 --- a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts @@ -25,7 +25,7 @@ import { NotificationsSetup, OverlayStart, SavedObjectsClientContract, - UiSettingsClientContract, + IUiSettingsClient, UiSettingsState, } from 'kibana/public'; import { UiStatsMetricType } from '@kbn/analytics'; @@ -50,7 +50,7 @@ export interface HomeKibanaServices { getInjected: (name: string, defaultValue?: any) => unknown; chrome: ChromeStart; telemetryOptInProvider: any; - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; http: HttpStart; savedObjectsClient: SavedObjectsClientContract; toastNotifications: NotificationsSetup['toasts']; diff --git a/src/legacy/core_plugins/region_map/public/plugin.ts b/src/legacy/core_plugins/region_map/public/plugin.ts index a41d638986ae5..aaf0a8a308aea 100644 --- a/src/legacy/core_plugins/region_map/public/plugin.ts +++ b/src/legacy/core_plugins/region_map/public/plugin.ts @@ -21,7 +21,7 @@ import { CoreStart, Plugin, PluginInitializerContext, - UiSettingsClientContract, + IUiSettingsClient, } from '../../../../core/public'; import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; import { VisualizationsSetup } from '../../visualizations/public'; @@ -35,7 +35,7 @@ import { createRegionMapTypeDefinition } from './region_map_type'; /** @private */ interface RegionMapVisualizationDependencies extends LegacyDependenciesPluginSetup { - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; } /** @internal */ diff --git a/src/legacy/core_plugins/tile_map/public/plugin.ts b/src/legacy/core_plugins/tile_map/public/plugin.ts index 14a348f624002..52acaf51b39b1 100644 --- a/src/legacy/core_plugins/tile_map/public/plugin.ts +++ b/src/legacy/core_plugins/tile_map/public/plugin.ts @@ -21,7 +21,7 @@ import { CoreStart, Plugin, PluginInitializerContext, - UiSettingsClientContract, + IUiSettingsClient, } from '../../../../core/public'; import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; import { VisualizationsSetup } from '../../visualizations/public'; @@ -35,7 +35,7 @@ import { createTileMapTypeDefinition } from './tile_map_type'; /** @private */ interface TileMapVisualizationDependencies extends LegacyDependenciesPluginSetup { - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; } /** @internal */ diff --git a/src/legacy/core_plugins/timelion/public/plugin.ts b/src/legacy/core_plugins/timelion/public/plugin.ts index b0123cd34b49e..ba8c25c20abea 100644 --- a/src/legacy/core_plugins/timelion/public/plugin.ts +++ b/src/legacy/core_plugins/timelion/public/plugin.ts @@ -21,7 +21,7 @@ import { CoreStart, Plugin, PluginInitializerContext, - UiSettingsClientContract, + IUiSettingsClient, HttpSetup, } from 'kibana/public'; import { Plugin as ExpressionsPlugin } from 'src/plugins/expressions/public'; @@ -35,7 +35,7 @@ import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim' /** @internal */ export interface TimelionVisualizationDependencies extends LegacyDependenciesPluginSetup { - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; http: HttpSetup; timelionPanels: Map; timefilter: TimefilterContract; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/contexts/query_input_bar_context.ts b/src/legacy/core_plugins/vis_type_timeseries/public/contexts/query_input_bar_context.ts index 925b483905d01..04a63f60aacf2 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/contexts/query_input_bar_context.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/public/contexts/query_input_bar_context.ts @@ -18,12 +18,12 @@ */ import React from 'react'; -import { UiSettingsClientContract, SavedObjectsClientContract } from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; export interface ICoreStartContext { appName: string; - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; savedObjectsClient: SavedObjectsClientContract; storage: IStorageWrapper; } diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts b/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts index 75a65e131797d..4d1222d6f5a87 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts @@ -22,7 +22,7 @@ import { CoreStart, Plugin, SavedObjectsClientContract, - UiSettingsClientContract, + IUiSettingsClient, } from '../../../../core/public'; import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; import { VisualizationsSetup } from '../../visualizations/public'; @@ -37,7 +37,7 @@ export interface MetricsPluginSetupDependencies { visualizations: VisualizationsSetup; } export interface MetricsVisualizationDependencies { - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; savedObjectsClient: SavedObjectsClientContract; } diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/services.ts b/src/legacy/core_plugins/vis_type_timeseries/public/services.ts index dcc7de4098bdd..af04578b8e27f 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/services.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/public/services.ts @@ -17,12 +17,10 @@ * under the License. */ -import { I18nStart, SavedObjectsStart, UiSettingsClientContract } from 'src/core/public'; +import { I18nStart, SavedObjectsStart, IUiSettingsClient } from 'src/core/public'; import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; -export const [getUISettings, setUISettings] = createGetterSetter( - 'UISettings' -); +export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); export const [getSavedObjectsClient, setSavedObjectsClient] = createGetterSetter( 'SavedObjectsClient' diff --git a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts index 9001164afe820..5166770d1727b 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts @@ -21,7 +21,7 @@ import { CoreSetup, CoreStart, Plugin, - UiSettingsClientContract, + IUiSettingsClient, } from '../../../../core/public'; import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim'; import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; @@ -32,7 +32,7 @@ import { createVegaTypeDefinition } from './vega_type'; /** @internal */ export interface VegaVisualizationDependencies extends LegacyDependenciesPluginSetup { - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; } /** @internal */ diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts index 63afbca71a280..434612d11b28a 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts @@ -17,13 +17,11 @@ * under the License. */ -import { I18nStart, UiSettingsClientContract } from 'src/core/public'; +import { I18nStart, IUiSettingsClient } from 'src/core/public'; import { TypesStart } from './types'; import { createGetterSetter } from '../../../../../../plugins/kibana_utils/public'; -export const [getUISettings, setUISettings] = createGetterSetter( - 'UISettings' -); +export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); export const [getTypes, setTypes] = createGetterSetter('Types'); diff --git a/src/legacy/ui/public/agg_types/buckets/date_range.test.ts b/src/legacy/ui/public/agg_types/buckets/date_range.test.ts index 7d9fe002636a2..e34cb4e36720f 100644 --- a/src/legacy/ui/public/agg_types/buckets/date_range.test.ts +++ b/src/legacy/ui/public/agg_types/buckets/date_range.test.ts @@ -95,7 +95,7 @@ describe('date_range params', () => { }); it('should use the Kibana time_zone if no parameter specified', () => { - npStart.core.uiSettings.get = jest.fn(() => 'kibanaTimeZone'); + npStart.core.uiSettings.get = jest.fn(() => 'kibanaTimeZone' as any); const aggConfigs = getAggConfigs( { diff --git a/src/legacy/ui/public/agg_types/buckets/histogram.test.ts b/src/legacy/ui/public/agg_types/buckets/histogram.test.ts index 338af2e41cb88..4e89d7db1ff64 100644 --- a/src/legacy/ui/public/agg_types/buckets/histogram.test.ts +++ b/src/legacy/ui/public/agg_types/buckets/histogram.test.ts @@ -159,7 +159,7 @@ describe('Histogram Agg', () => { } // mock histogram:maxBars value; - npStart.core.uiSettings.get = jest.fn(() => maxBars); + npStart.core.uiSettings.get = jest.fn(() => maxBars as any); return aggConfig.write(aggConfigs).params; }; diff --git a/src/legacy/ui/public/courier/fetch/fetch_soon.test.ts b/src/legacy/ui/public/courier/fetch/fetch_soon.test.ts index e753c526b748d..d96fb536985da 100644 --- a/src/legacy/ui/public/courier/fetch/fetch_soon.test.ts +++ b/src/legacy/ui/public/courier/fetch/fetch_soon.test.ts @@ -19,14 +19,14 @@ import { fetchSoon } from './fetch_soon'; import { callClient } from './call_client'; -import { UiSettingsClientContract } from '../../../../../core/public'; +import { IUiSettingsClient } from '../../../../../core/public'; import { FetchHandlers, FetchOptions } from './types'; import { SearchRequest, SearchResponse } from '../types'; function getConfigStub(config: any = {}) { return { get: key => config[key], - } as UiSettingsClientContract; + } as IUiSettingsClient; } const mockResponses: Record = { diff --git a/src/legacy/ui/public/courier/fetch/get_search_params.test.ts b/src/legacy/ui/public/courier/fetch/get_search_params.test.ts index d6f3d33099599..76f3105d7f942 100644 --- a/src/legacy/ui/public/courier/fetch/get_search_params.test.ts +++ b/src/legacy/ui/public/courier/fetch/get_search_params.test.ts @@ -18,12 +18,12 @@ */ import { getMSearchParams, getSearchParams } from './get_search_params'; -import { UiSettingsClientContract } from '../../../../../core/public'; +import { IUiSettingsClient } from '../../../../../core/public'; function getConfigStub(config: any = {}) { return { get: key => config[key], - } as UiSettingsClientContract; + } as IUiSettingsClient; } describe('getMSearchParams', () => { diff --git a/src/legacy/ui/public/courier/fetch/get_search_params.ts b/src/legacy/ui/public/courier/fetch/get_search_params.ts index 6b8da07ca93d4..21cdbf97945c5 100644 --- a/src/legacy/ui/public/courier/fetch/get_search_params.ts +++ b/src/legacy/ui/public/courier/fetch/get_search_params.ts @@ -17,11 +17,11 @@ * under the License. */ -import { UiSettingsClientContract } from '../../../../../core/public'; +import { IUiSettingsClient } from '../../../../../core/public'; const sessionId = Date.now(); -export function getMSearchParams(config: UiSettingsClientContract) { +export function getMSearchParams(config: IUiSettingsClient) { return { rest_total_hits_as_int: true, ignore_throttled: getIgnoreThrottled(config), @@ -29,7 +29,7 @@ export function getMSearchParams(config: UiSettingsClientContract) { }; } -export function getSearchParams(config: UiSettingsClientContract, esShardTimeout: number = 0) { +export function getSearchParams(config: IUiSettingsClient, esShardTimeout: number = 0) { return { rest_total_hits_as_int: true, ignore_unavailable: true, @@ -40,16 +40,16 @@ export function getSearchParams(config: UiSettingsClientContract, esShardTimeout }; } -export function getIgnoreThrottled(config: UiSettingsClientContract) { +export function getIgnoreThrottled(config: IUiSettingsClient) { return !config.get('search:includeFrozen'); } -export function getMaxConcurrentShardRequests(config: UiSettingsClientContract) { +export function getMaxConcurrentShardRequests(config: IUiSettingsClient) { const maxConcurrentShardRequests = config.get('courier:maxConcurrentShardRequests'); return maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined; } -export function getPreference(config: UiSettingsClientContract) { +export function getPreference(config: IUiSettingsClient) { const setRequestPreference = config.get('courier:setRequestPreference'); if (setRequestPreference === 'sessionId') return sessionId; return setRequestPreference === 'custom' diff --git a/src/legacy/ui/public/courier/fetch/types.ts b/src/legacy/ui/public/courier/fetch/types.ts index e341e1ab35c5c..03bf51ae15d45 100644 --- a/src/legacy/ui/public/courier/fetch/types.ts +++ b/src/legacy/ui/public/courier/fetch/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { UiSettingsClientContract } from '../../../../../core/public'; +import { IUiSettingsClient } from '../../../../../core/public'; import { SearchRequest, SearchResponse } from '../types'; export interface ApiCaller { @@ -36,6 +36,6 @@ export interface FetchOptions { export interface FetchHandlers { es: ApiCaller; - config: UiSettingsClientContract; + config: IUiSettingsClient; esShardTimeout: number; } diff --git a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.ts b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.ts index 29921fc7a11d3..53a857a72c1a3 100644 --- a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.ts +++ b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.ts @@ -18,7 +18,7 @@ */ import { defaultSearchStrategy } from './default_search_strategy'; -import { UiSettingsClientContract } from '../../../../../core/public'; +import { IUiSettingsClient } from '../../../../../core/public'; import { SearchStrategySearchParams } from './types'; const { search } = defaultSearchStrategy; @@ -26,7 +26,7 @@ const { search } = defaultSearchStrategy; function getConfigStub(config: any = {}) { return { get: key => config[key], - } as UiSettingsClientContract; + } as IUiSettingsClient; } const msearchMockResponse: any = Promise.resolve([]); diff --git a/src/legacy/ui/public/test_harness/test_harness.js b/src/legacy/ui/public/test_harness/test_harness.js index 8c58ca4e0ad03..fa7ca0dd62ac1 100644 --- a/src/legacy/ui/public/test_harness/test_harness.js +++ b/src/legacy/ui/public/test_harness/test_harness.js @@ -21,9 +21,11 @@ import chrome from '../chrome'; import { parse as parseUrl } from 'url'; +import { Subject } from 'rxjs'; import sinon from 'sinon'; import { metadata } from '../metadata'; -import { UiSettingsClient } from '../../../../core/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { UiSettingsClient } from '../../../../core/public/ui_settings'; import './test_harness.css'; import 'ng_mock'; @@ -46,10 +48,12 @@ before(() => { }); let stubUiSettings; +let done$; function createStubUiSettings() { if (stubUiSettings) { - stubUiSettings.stop(); + done$.complete(); } + done$ = new Subject(); stubUiSettings = new UiSettingsClient({ api: { @@ -60,6 +64,7 @@ function createStubUiSettings() { onUpdateError: () => {}, defaults: metadata.uiSettings.defaults, initialSettings: {}, + done$, }); } diff --git a/src/plugins/data/common/es_query/es_query/get_es_query_config.test.ts b/src/plugins/data/common/es_query/es_query/get_es_query_config.test.ts index a4ab03687f92e..d146d81973d0d 100644 --- a/src/plugins/data/common/es_query/es_query/get_es_query_config.test.ts +++ b/src/plugins/data/common/es_query/es_query/get_es_query_config.test.ts @@ -18,7 +18,7 @@ */ import { get } from 'lodash'; import { getEsQueryConfig } from './get_es_query_config'; -import { UiSettingsClientContract } from 'kibana/public'; +import { IUiSettingsClient } from 'kibana/public'; const config = ({ get(item: string) { @@ -36,7 +36,7 @@ const config = ({ 'dateFormat:tz': { dateFormatTZ: 'Browser', }, -} as unknown) as UiSettingsClientContract; +} as unknown) as IUiSettingsClient; describe('getEsQueryConfig', () => { test('should return the parameters of an Elasticsearch query config requested', () => { diff --git a/src/plugins/data/public/field_formats_provider/field_formats.ts b/src/plugins/data/public/field_formats_provider/field_formats.ts index f46994c209ded..20e90b8e4a545 100644 --- a/src/plugins/data/public/field_formats_provider/field_formats.ts +++ b/src/plugins/data/public/field_formats_provider/field_formats.ts @@ -18,7 +18,7 @@ */ import { forOwn, isFunction, memoize } from 'lodash'; -import { UiSettingsClientContract } from 'kibana/public'; +import { IUiSettingsClient } from 'kibana/public'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES, @@ -31,7 +31,7 @@ import { FieldType } from './types'; export class FieldFormatRegisty { private fieldFormats: Map; - private uiSettings!: UiSettingsClientContract; + private uiSettings!: IUiSettingsClient; private defaultMap: Record; constructor() { @@ -41,7 +41,7 @@ export class FieldFormatRegisty { getConfig = (key: string, override?: any) => this.uiSettings.get(key, override); - init(uiSettings: UiSettingsClientContract) { + init(uiSettings: IUiSettingsClient) { this.uiSettings = uiSettings; this.parseDefaultTypeMap(this.uiSettings.get('format:defaultTypeMap')); diff --git a/src/plugins/data/public/field_formats_provider/field_formats_service.ts b/src/plugins/data/public/field_formats_provider/field_formats_service.ts index b144ea7ec2530..ea1a8af2930b0 100644 --- a/src/plugins/data/public/field_formats_provider/field_formats_service.ts +++ b/src/plugins/data/public/field_formats_provider/field_formats_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { UiSettingsClientContract } from 'src/core/public'; +import { IUiSettingsClient } from 'src/core/public'; import { FieldFormatRegisty } from './field_formats'; import { @@ -43,7 +43,7 @@ import { * @internal */ interface FieldFormatsServiceDependencies { - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; } export class FieldFormatsService { diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.ts b/src/plugins/data/public/query/filter_manager/filter_manager.ts index feab75ed7457f..c25e5df2b168a 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.ts @@ -20,7 +20,7 @@ import _ from 'lodash'; import { Subject } from 'rxjs'; -import { UiSettingsClientContract } from 'src/core/public'; +import { IUiSettingsClient } from 'src/core/public'; import { compareFilters } from './lib/compare_filters'; import { mapAndFlattenFilters } from './lib/map_and_flatten_filters'; @@ -33,9 +33,9 @@ export class FilterManager { private filters: esFilters.Filter[] = []; private updated$: Subject = new Subject(); private fetch$: Subject = new Subject(); - private uiSettings: UiSettingsClientContract; + private uiSettings: IUiSettingsClient; - constructor(uiSettings: UiSettingsClientContract) { + constructor(uiSettings: IUiSettingsClient) { this.uiSettings = uiSettings; } diff --git a/src/plugins/data/public/query/lib/get_query_log.ts b/src/plugins/data/public/query/lib/get_query_log.ts index 67073a9078046..a71eb7580cf07 100644 --- a/src/plugins/data/public/query/lib/get_query_log.ts +++ b/src/plugins/data/public/query/lib/get_query_log.ts @@ -17,12 +17,12 @@ * under the License. */ -import { UiSettingsClientContract } from 'src/core/public'; +import { IUiSettingsClient } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { PersistedLog } from '../persisted_log'; export function getQueryLog( - uiSettings: UiSettingsClientContract, + uiSettings: IUiSettingsClient, storage: IStorageWrapper, appName: string, language: string diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.ts b/src/plugins/data/public/query/timefilter/timefilter_service.ts index 831ccebedc9cc..413163ed059ad 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { UiSettingsClientContract } from 'src/core/public'; +import { IUiSettingsClient } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { TimeHistory, Timefilter, TimeHistoryContract, TimefilterContract } from './index'; @@ -27,7 +27,7 @@ import { TimeHistory, Timefilter, TimeHistoryContract, TimefilterContract } from */ export interface TimeFilterServiceDependencies { - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; storage: IStorageWrapper; } diff --git a/src/plugins/data/public/suggestions_provider/value_suggestions.test.ts b/src/plugins/data/public/suggestions_provider/value_suggestions.test.ts index 7dc8ff0fe133d..02aaaaf6f4689 100644 --- a/src/plugins/data/public/suggestions_provider/value_suggestions.test.ts +++ b/src/plugins/data/public/suggestions_provider/value_suggestions.test.ts @@ -22,7 +22,7 @@ jest.mock('ui/new_platform'); import { stubIndexPattern, stubFields } from '../stubs'; import { getSuggestionsProvider } from './value_suggestions'; -import { UiSettingsClientContract } from 'kibana/public'; +import { IUiSettingsClient } from 'kibana/public'; describe('getSuggestions', () => { let getSuggestions: any; @@ -30,7 +30,7 @@ describe('getSuggestions', () => { describe('with value suggestions disabled', () => { beforeEach(() => { - const config = { get: (key: string) => false } as UiSettingsClientContract; + const config = { get: (key: string) => false } as IUiSettingsClient; http = { fetch: jest.fn() }; getSuggestions = getSuggestionsProvider(config, http); }); @@ -47,7 +47,7 @@ describe('getSuggestions', () => { describe('with value suggestions enabled', () => { beforeEach(() => { - const config = { get: (key: string) => true } as UiSettingsClientContract; + const config = { get: (key: string) => true } as IUiSettingsClient; http = { fetch: jest.fn() }; getSuggestions = getSuggestionsProvider(config, http); }); diff --git a/src/plugins/data/public/suggestions_provider/value_suggestions.ts b/src/plugins/data/public/suggestions_provider/value_suggestions.ts index 3bc1b45d87395..282f4ee65dc96 100644 --- a/src/plugins/data/public/suggestions_provider/value_suggestions.ts +++ b/src/plugins/data/public/suggestions_provider/value_suggestions.ts @@ -19,12 +19,12 @@ import { memoize } from 'lodash'; -import { UiSettingsClientContract, HttpServiceBase } from 'src/core/public'; +import { IUiSettingsClient, HttpServiceBase } from 'src/core/public'; import { IGetSuggestions } from './types'; import { IFieldType } from '../../common'; export function getSuggestionsProvider( - uiSettings: UiSettingsClientContract, + uiSettings: IUiSettingsClient, http: HttpServiceBase ): IGetSuggestions { const requestSuggestions = memoize( diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index 4ef0b2740e5fa..1921f6672755d 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -21,7 +21,7 @@ import { EuiContextMenu, EuiPopover } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import classNames from 'classnames'; import React, { Component } from 'react'; -import { UiSettingsClientContract } from 'src/core/public'; +import { IUiSettingsClient } from 'src/core/public'; import { FilterEditor } from './filter_editor'; import { FilterView } from './filter_view'; import { esFilters, utils, IIndexPattern } from '../..'; @@ -34,7 +34,7 @@ interface Props { onUpdate: (filter: esFilters.Filter) => void; onRemove: () => void; intl: InjectedIntl; - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; } interface State { diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/language_switcher.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/language_switcher.test.tsx.snap index d4990ed59f441..7ab7d7653eb5e 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/language_switcher.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/language_switcher.test.tsx.snap @@ -206,7 +206,6 @@ exports[`LanguageSwitcher should toggle off if language is lucene 1`] = ` "overrideLocalDefault": [MockFunction], "remove": [MockFunction], "set": [MockFunction], - "stop": [MockFunction], }, } } @@ -497,7 +496,6 @@ exports[`LanguageSwitcher should toggle on if language is kuery 1`] = ` "overrideLocalDefault": [MockFunction], "remove": [MockFunction], "set": [MockFunction], - "stop": [MockFunction], }, } } diff --git a/src/plugins/inspector/public/views/data/components/data_table.tsx b/src/plugins/inspector/public/views/data/components/data_table.tsx index d5f2d4645ce0b..b78a3920804d2 100644 --- a/src/plugins/inspector/public/views/data/components/data_table.tsx +++ b/src/plugins/inspector/public/views/data/components/data_table.tsx @@ -36,7 +36,7 @@ import { i18n } from '@kbn/i18n'; import { DataDownloadOptions } from './download_options'; import { DataViewRow, DataViewColumn } from '../types'; import { TabularData } from '../../../adapters/data/types'; -import { UiSettingsClientContract } from '../../../../../../core/public'; +import { IUiSettingsClient } from '../../../../../../core/public'; interface DataTableFormatState { columns: DataViewColumn[]; @@ -46,7 +46,7 @@ interface DataTableFormatState { interface DataTableFormatProps { data: TabularData; exportTitle: string; - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; isFormatted?: boolean; } diff --git a/src/plugins/inspector/public/views/data/components/data_view.test.tsx b/src/plugins/inspector/public/views/data/components/data_view.test.tsx index d067757350b51..55322bf5ec91a 100644 --- a/src/plugins/inspector/public/views/data/components/data_view.test.tsx +++ b/src/plugins/inspector/public/views/data/components/data_view.test.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { getDataViewDescription } from '../index'; import { DataAdapter } from '../../../adapters/data'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { UiSettingsClientContract } from '../../../../../../core/public'; +import { IUiSettingsClient } from '../../../../../../core/public'; jest.mock('../lib/export_csv', () => ({ exportAsCsv: jest.fn(), @@ -31,7 +31,7 @@ describe('Inspector Data View', () => { let DataView: any; beforeEach(() => { - const uiSettings = {} as UiSettingsClientContract; + const uiSettings = {} as IUiSettingsClient; DataView = getDataViewDescription(uiSettings); }); diff --git a/src/plugins/inspector/public/views/data/components/data_view.tsx b/src/plugins/inspector/public/views/data/components/data_view.tsx index 34e0bfaa52693..91f42a54f64d0 100644 --- a/src/plugins/inspector/public/views/data/components/data_view.tsx +++ b/src/plugins/inspector/public/views/data/components/data_view.tsx @@ -32,7 +32,7 @@ import { import { DataTableFormat } from './data_table'; import { InspectorViewProps, Adapters } from '../../../types'; import { TabularLoaderOptions, TabularData, TabularCallback } from '../../../adapters/data/types'; -import { UiSettingsClientContract } from '../../../../../../core/public'; +import { IUiSettingsClient } from '../../../../../../core/public'; interface DataViewComponentState { tabularData: TabularData | null; @@ -42,7 +42,7 @@ interface DataViewComponentState { } interface DataViewComponentProps extends InspectorViewProps { - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; } export class DataViewComponent extends Component { diff --git a/src/plugins/inspector/public/views/data/index.tsx b/src/plugins/inspector/public/views/data/index.tsx index a33bf5ebf1f52..0cd88442bf8f8 100644 --- a/src/plugins/inspector/public/views/data/index.tsx +++ b/src/plugins/inspector/public/views/data/index.tsx @@ -21,10 +21,10 @@ import { i18n } from '@kbn/i18n'; import { DataViewComponent } from './components/data_view'; import { Adapters, InspectorViewDescription, InspectorViewProps } from '../../types'; -import { UiSettingsClientContract } from '../../../../../core/public'; +import { IUiSettingsClient } from '../../../../../core/public'; export const getDataViewDescription = ( - uiSettings: UiSettingsClientContract + uiSettings: IUiSettingsClient ): InspectorViewDescription => ({ title: i18n.translate('inspector.data.dataTitle', { defaultMessage: 'Data', diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx index e3be0b08ab83f..4bb7ce75073ae 100644 --- a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx +++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx @@ -37,7 +37,7 @@ import { EuiConfirmModal, EuiCallOut, } from '@elastic/eui'; -import { ToastsStart, UiSettingsClientContract } from 'kibana/public'; +import { ToastsStart, IUiSettingsClient } from 'kibana/public'; import { toMountPoint } from '../util'; export const EMPTY_FILTER = ''; @@ -66,7 +66,7 @@ export interface TableListViewProps { tableColumns: Column[]; tableListTitle: string; toastNotifications: ToastsStart; - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; } export interface TableListViewState { diff --git a/src/test_utils/public/stub_field_formats.ts b/src/test_utils/public/stub_field_formats.ts index 39c6fb2f6d10e..da1a31f1cc7a5 100644 --- a/src/test_utils/public/stub_field_formats.ts +++ b/src/test_utils/public/stub_field_formats.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { UiSettingsClientContract } from 'kibana/public'; +import { IUiSettingsClient } from 'kibana/public'; import { FieldFormatRegisty, @@ -37,7 +37,7 @@ import { UrlFormat, } from '../../plugins/data/public/'; -export const getFieldFormatsRegistry = (uiSettings: UiSettingsClientContract) => { +export const getFieldFormatsRegistry = (uiSettings: IUiSettingsClient) => { const fieldFormats = new FieldFormatRegisty(); fieldFormats.register([ diff --git a/x-pack/legacy/plugins/graph/public/render_app.ts b/x-pack/legacy/plugins/graph/public/render_app.ts index 18cdf0ddd81b2..1beee2e73721b 100644 --- a/x-pack/legacy/plugins/graph/public/render_app.ts +++ b/x-pack/legacy/plugins/graph/public/render_app.ts @@ -28,7 +28,7 @@ import { LegacyCoreStart, SavedObjectsClientContract, ToastsStart, - UiSettingsClientContract, + IUiSettingsClient, } from 'kibana/public'; // @ts-ignore import { initGraphApp } from './app'; @@ -48,7 +48,7 @@ export interface GraphDependencies extends LegacyAngularInjectedDependencies { coreStart: AppMountContext['core']; navigation: NavigationStart; chrome: ChromeStart; - config: UiSettingsClientContract; + config: IUiSettingsClient; toastNotifications: ToastsStart; indexPatterns: DataStart['indexPatterns']['indexPatterns']; npData: ReturnType; 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_plugin/dimension_panel/dimension_panel.test.tsx index f615914360a35..f3e86c5b59214 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -17,11 +17,7 @@ import { import { DropHandler, DragContextState } from '../../drag_drop'; import { createMockedDragDropContext } from '../mocks'; import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; -import { - UiSettingsClientContract, - SavedObjectsClientContract, - HttpServiceBase, -} from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpServiceBase } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; @@ -140,7 +136,7 @@ describe('IndexPatternDimensionPanel', () => { uniqueLabel: 'stuff', filterOperations: () => true, storage: {} as IStorageWrapper, - uiSettings: {} as UiSettingsClientContract, + uiSettings: {} as IUiSettingsClient, savedObjectsClient: {} as SavedObjectsClientContract, http: {} as HttpServiceBase, }; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx index dd9fde4bf1572..fded53cd35f59 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx @@ -8,11 +8,7 @@ import _ from 'lodash'; import React, { memo, useMemo } from 'react'; import { EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { - UiSettingsClientContract, - SavedObjectsClientContract, - HttpServiceBase, -} from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpServiceBase } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { DatasourceDimensionPanelProps, StateSetter } from '../../types'; import { IndexPatternColumn, OperationType } from '../indexpattern'; @@ -29,7 +25,7 @@ export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { state: IndexPatternPrivateState; setState: StateSetter; dragDropContext: DragContextState; - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; storage: IStorageWrapper; savedObjectsClient: SavedObjectsClientContract; layerId: string; 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_plugin/operations/definitions/date_histogram.test.tsx index d0b77a425d14a..f7125a1adae52 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx @@ -9,11 +9,7 @@ import { DateHistogramIndexPatternColumn } from './date_histogram'; import { dateHistogramOperation } from '.'; import { shallow } from 'enzyme'; import { EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; -import { - UiSettingsClientContract, - SavedObjectsClientContract, - HttpServiceBase, -} from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpServiceBase } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { createMockedIndexPattern } from '../../mocks'; import { IndexPatternPrivateState } from '../../types'; @@ -34,7 +30,7 @@ jest.mock('ui/new_platform', () => ({ const defaultOptions = { storage: {} as IStorageWrapper, - uiSettings: {} as UiSettingsClientContract, + uiSettings: {} as IUiSettingsClient, savedObjectsClient: {} as SavedObjectsClientContract, dateRange: { fromDate: 'now-1y', diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts index 9ad3fb679471e..252a3d788fd30 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - UiSettingsClientContract, - SavedObjectsClientContract, - HttpServiceBase, -} from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpServiceBase } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { termsOperation } from './terms'; import { cardinalityOperation } from './cardinality'; @@ -48,7 +44,7 @@ export interface ParamEditorProps { setState: StateSetter; columnId: string; layerId: string; - uiSettings: UiSettingsClientContract; + uiSettings: IUiSettingsClient; storage: IStorageWrapper; savedObjectsClient: SavedObjectsClientContract; http: HttpServiceBase; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx index 7b21ef92ab82b..c0d995d420760 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx @@ -7,11 +7,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { EuiRange, EuiSelect } from '@elastic/eui'; -import { - UiSettingsClientContract, - SavedObjectsClientContract, - HttpServiceBase, -} from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpServiceBase } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { createMockedIndexPattern } from '../../mocks'; import { TermsIndexPatternColumn } from './terms'; @@ -22,7 +18,7 @@ jest.mock('ui/new_platform'); const defaultProps = { storage: {} as IStorageWrapper, - uiSettings: {} as UiSettingsClientContract, + uiSettings: {} as IUiSettingsClient, savedObjectsClient: {} as SavedObjectsClientContract, dateRange: { fromDate: 'now-1d', toDate: 'now' }, http: {} as HttpServiceBase, diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx index 9867d014217e7..f0603f021c452 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx @@ -5,7 +5,7 @@ */ import { npSetup } from 'ui/new_platform'; -import { CoreSetup, UiSettingsClientContract } from 'src/core/public'; +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'; @@ -24,7 +24,7 @@ export interface XyVisualizationPluginSetupPlugins { }; } -function getTimeZone(uiSettings: UiSettingsClientContract) { +function getTimeZone(uiSettings: IUiSettingsClient) { const configuredTimeZone = uiSettings.get('dateFormat:tz'); if (configuredTimeZone === 'Browser') { return moment.tz.guess(); diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts b/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts index b465392a50ae1..b40645799fb4b 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract, UiSettingsClientContract } from 'src/core/public'; +import { SavedObjectsClientContract, IUiSettingsClient } from 'src/core/public'; import { IndexPattern as IndexPatternType, IndexPatterns as IndexPatternsType, @@ -73,7 +73,7 @@ export function loadCurrentSavedSearch(savedSearches: any, savedSearchId: SavedS export function createSearchItems( indexPattern: IndexPatternType | undefined, savedSearch: any, - config: UiSettingsClientContract + config: IUiSettingsClient ) { // query is only used by the data visualizer as it needs // a lucene query_string. From bd63596d184b145a5ebffeb91ce8b5e5318aca50 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 28 Nov 2019 22:54:21 +0100 Subject: [PATCH 5/6] add eslint rule banning the core to import plugin code (#51563) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add eslint rule banning the core to import plugin code * Ban importing legacy plugin code in the сore * fix eslint errors * core cannot import xpack rule * regen docs --- .eslintrc.js | 15 +++++++++++++++ .../public/saved_objects/saved_objects_client.ts | 1 + .../legacy/plugins/find_legacy_plugin_specs.ts | 1 + .../service/lib/filter_utils.test.ts | 2 +- .../saved_objects/service/lib/filter_utils.ts | 1 + .../saved_objects/service/lib/repository.ts | 13 +++++++------ .../service/lib/search_dsl/query_params.ts | 1 + .../service/lib/search_dsl/search_dsl.ts | 1 + src/core/server/server.api.md | 1 - .../ui_settings/integration_tests/lib/servers.ts | 6 +++--- 10 files changed, 31 insertions(+), 11 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index fe546ec02a668..58b74d0bf5c25 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -350,6 +350,21 @@ module.exports = { ], allowSameFolder: true, }, + { + target: ['src/core/**/*'], + from: ['x-pack/**/*'], + errorMessage: 'OSS cannot import x-pack files.', + }, + { + target: ['src/core/**/*'], + from: [ + 'plugins/**/*', + 'src/plugins/**/*', + 'src/legacy/core_plugins/**/*', + 'src/legacy/ui/**/*', + ], + errorMessage: 'The core cannot depend on any plugins.', + }, { from: ['src/legacy/ui/**/*', 'ui/**/*'], target: [ diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index 729d356e76ebd..c71fe51956c28 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -33,6 +33,7 @@ import { import { isAutoCreateIndexError, showAutoCreateIndexErrorPage, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../legacy/ui/public/error_auto_create_index/error_auto_create_index'; import { SimpleSavedObject } from './simple_saved_object'; import { HttpFetchOptions, HttpServiceBase } from '../http'; diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts index c0a6026708af3..9c95ed61ae66e 100644 --- a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts +++ b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts @@ -25,6 +25,7 @@ import { // @ts-ignore } from '../../../../legacy/plugin_discovery/find_plugin_specs.js'; import { LoggerFactory } from '../../logging'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { collectUiExports as collectLegacyUiExports } from '../../../../legacy/ui/ui_exports/collect_ui_exports'; import { Config } from '../../config'; diff --git a/src/core/server/saved_objects/service/lib/filter_utils.test.ts b/src/core/server/saved_objects/service/lib/filter_utils.test.ts index 13a132ab9dd67..9ae4b32202823 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.test.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { esKuery } from '../../../../../plugins/data/server'; import { validateFilterKueryNode, validateConvertFilterToKueryNode } from './filter_utils'; diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts index 3cf499de541ee..e7509933a38aa 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.ts @@ -20,6 +20,7 @@ import { get, set } from 'lodash'; import { SavedObjectsErrorHelpers } from './errors'; import { IndexMapping } from '../../mappings'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { esKuery } from '../../../../../plugins/data/server'; export const validateConvertFilterToKueryNode = ( diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index f9e48aba5a70e..a7c55a25c3bab 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -18,7 +18,8 @@ */ import { omit } from 'lodash'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { APICaller } from '../../../elasticsearch/'; + import { getRootPropertiesObjects, IndexMapping } from '../../mappings'; import { getSearchDsl } from './search_dsl'; import { includedFields } from './included_fields'; @@ -76,7 +77,7 @@ export interface SavedObjectsRepositoryOptions { /** @deprecated Will be removed once SavedObjectsSchema is exposed from Core */ config: Config; mappings: IndexMapping; - callCluster: CallCluster; + callCluster: APICaller; schema: SavedObjectsSchema; serializer: SavedObjectsSerializer; migrator: KibanaMigrator; @@ -120,7 +121,7 @@ export class SavedObjectsRepository { private _mappings: IndexMapping; private _schema: SavedObjectsSchema; private _allowedTypes: string[]; - private _unwrappedCallCluster: CallCluster; + private _unwrappedCallCluster: APICaller; private _serializer: SavedObjectsSerializer; /** @internal */ @@ -153,7 +154,7 @@ export class SavedObjectsRepository { } this._allowedTypes = allowedTypes; - this._unwrappedCallCluster = async (...args: Parameters) => { + this._unwrappedCallCluster = async (...args: Parameters) => { await migrator.runMigrations(); return callCluster(...args); }; @@ -886,7 +887,7 @@ export class SavedObjectsRepository { }; } - private async _writeToCluster(...args: Parameters) { + private async _writeToCluster(...args: Parameters) { try { return await this._callCluster(...args); } catch (err) { @@ -894,7 +895,7 @@ export class SavedObjectsRepository { } } - private async _callCluster(...args: Parameters) { + private async _callCluster(...args: Parameters) { try { return await this._unwrappedCallCluster(...args); } catch (err) { diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index cfeb258c2f03b..a1e3ae9620299 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { esKuery } from '../../../../../../plugins/data/server'; import { getRootPropertiesObjects, IndexMapping } from '../../../mappings'; diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index f2bbc3ef564a1..1b6e1361bb92a 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -23,6 +23,7 @@ import { IndexMapping } from '../../../mappings'; import { SavedObjectsSchema } from '../../../schema'; import { getQueryParams } from './query_params'; import { getSortingParams } from './sorting_params'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { esKuery } from '../../../../../../plugins/data/server'; interface GetSearchDslOptions { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index c8a68b4e2ea2a..e8cafe1e1334e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -6,7 +6,6 @@ import Boom from 'boom'; import { BulkIndexDocumentsParams } from 'elasticsearch'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; import { CatAliasesParams } from 'elasticsearch'; import { CatAllocationParams } from 'elasticsearch'; import { CatCommonParams } from 'elasticsearch'; diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts index a1be1e7e7291e..1abe6dc2d0683 100644 --- a/src/core/server/ui_settings/integration_tests/lib/servers.ts +++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts @@ -25,7 +25,7 @@ import { TestKibanaUtils, TestUtils, } from '../../../../../test_utils/kbn_server'; -import { CallCluster } from '../../../../../legacy/core_plugins/elasticsearch'; +import { APICaller } from '../../../elasticsearch/'; let servers: TestUtils; let esServer: TestElasticsearchUtils; @@ -36,7 +36,7 @@ let kbnServer: TestKibanaUtils['kbnServer']; interface AllServices { kbnServer: TestKibanaUtils['kbnServer']; savedObjectsClient: SavedObjectsClientContract; - callCluster: CallCluster; + callCluster: APICaller; uiSettings: IUiSettingsClient; deleteKibanaIndex: typeof deleteKibanaIndex; } @@ -61,7 +61,7 @@ export async function startServers() { kbnServer = kbn.kbnServer; } -async function deleteKibanaIndex(callCluster: CallCluster) { +async function deleteKibanaIndex(callCluster: APICaller) { const kibanaIndices = await callCluster('cat.indices', { index: '.kibana*', format: 'json' }); const indexNames = kibanaIndices.map((x: any) => x.index); if (!indexNames.length) { From d4dedbb54697a533c868dc69e64817d5ce128950 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 28 Nov 2019 23:51:02 +0100 Subject: [PATCH 6/6] [ML] Anomaly Explorer: Deprecate explorer_controller (#51269) The main goal of this PR is to get rid of explorer_controller.js to unblock the migration to react-router. Previously we already used rxjs observables to migrate away from angular events. Observables were used to trigger actions to manage the react component's state as well as AppState. This PR builds upon this previous work. The actions already were done similar to redux, now the use of observables has been extended to use scan (see rxjs docs) which allows us to transform the actions into state updates. --- .../new_platform/new_platform.karma_mock.js | 1 + .../controls/checkbox_showcharts/index.d.ts | 9 + .../controls/select_interval/index.d.ts | 12 + .../controls/select_severity/index.d.ts | 13 + .../job_select_service_utils.d.ts | 22 + .../job_selector/job_select_service_utils.js | 8 +- .../components/job_selector/job_selector.js | 6 +- .../__tests__/loading_indicator_directive.js | 79 -- .../loading_indicator/_loading_indicator.scss | 21 - .../components/loading_indicator/index.js | 5 +- .../loading_indicator/loading_indicator.html | 4 - .../loading_indicator/loading_indicator.js | 13 +- .../loading_indicator_directive.js | 27 - .../loading_indicator_wrapper.html | 2 - .../navigation_menu/top_nav/top_nav.test.tsx | 2 +- .../ui/__mocks__/{mocks.ts => mocks_jest.ts} | 0 .../contexts/ui/__mocks__/mocks_mocha.ts | 84 ++ .../ui/__mocks__/use_ui_chrome_context.ts | 2 +- .../contexts/ui/__mocks__/use_ui_context.ts | 2 +- .../explorer/__tests__/explorer_controller.js | 25 - .../explorer/__tests__/explorer_directive.js | 58 + .../application/explorer/actions/index.ts | 8 + .../explorer/actions/job_selection.ts | 46 + .../explorer/actions/load_explorer_data.ts | 197 +++ .../{breadcrumbs.js => breadcrumbs.ts} | 11 +- ...explorer_no_influencers_found.test.js.snap | 4 +- .../explorer_no_influencers_found.js | 12 +- .../explorer_no_influencers_found.test.js | 2 +- .../public/application/explorer/explorer.d.ts | 18 + .../public/application/explorer/explorer.js | 1057 +++-------------- .../explorer_chart_distribution.test.js | 6 +- .../explorer_chart_single_metric.test.js | 6 +- .../explorer_charts_container_service.d.ts | 18 + .../explorer_charts_container_service.js | 8 - ...rer_constants.js => explorer_constants.ts} | 38 +- .../explorer/explorer_controller.js | 289 ----- .../explorer/explorer_dashboard_service.js | 19 - .../explorer/explorer_dashboard_service.ts | 179 +++ .../explorer/explorer_directive.tsx | 110 ++ .../explorer_react_wrapper_directive.js | 62 - .../application/explorer/explorer_route.ts | 27 + .../application/explorer/explorer_utils.d.ts | 202 ++++ .../application/explorer/explorer_utils.js | 318 ++++- .../explorer/{index.js => index.ts} | 6 +- .../application/explorer/legacy_utils.js | 27 - .../application/explorer/legacy_utils.ts | 18 + .../explorer/reducers/app_state_reducer.ts | 89 ++ .../explorer_reducer/check_selected_cells.ts | 60 + .../clear_influencer_filter_settings.ts | 34 + .../explorer_reducer/get_index_pattern.ts | 23 + .../reducers/explorer_reducer/index.ts | 9 + .../reducers/explorer_reducer/initialize.ts | 35 + .../explorer_reducer/job_selection_change.ts | 61 + .../reducers/explorer_reducer/reducer.ts | 249 ++++ .../set_influencer_filter_settings.ts | 72 ++ .../set_kql_query_bar_placeholder.ts | 31 + .../reducers/explorer_reducer/state.ts | 104 ++ .../application/explorer/reducers/index.ts | 13 + .../services/annotations_service.test.tsx | 4 +- .../services/annotations_service.tsx | 4 +- .../services/field_format_service.ts | 2 +- .../application/services/job_service.d.ts | 2 + .../timeseriesexplorer/timeseriesexplorer.js | 18 +- .../application/util/app_state_utils.d.ts | 16 + .../application/util/observable_utils.tsx | 18 +- .../public/application/util/time_buckets.d.ts | 19 +- .../translations/translations/ja-JP.json | 4 +- .../translations/translations/zh-CN.json | 4 +- 68 files changed, 2362 insertions(+), 1592 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/public/application/components/controls/checkbox_showcharts/index.d.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/components/controls/select_interval/index.d.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/index.d.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/components/job_selector/job_select_service_utils.d.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/components/loading_indicator/__tests__/loading_indicator_directive.js delete mode 100644 x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.html delete mode 100644 x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator_directive.js delete mode 100644 x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator_wrapper.html rename x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/{mocks.ts => mocks_jest.ts} (100%) create mode 100644 x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks_mocha.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/__tests__/explorer_controller.js create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/__tests__/explorer_directive.js create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/actions/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/actions/job_selection.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/actions/load_explorer_data.ts rename x-pack/legacy/plugins/ml/public/application/explorer/{breadcrumbs.js => breadcrumbs.ts} (90%) create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/explorer.d.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.d.ts rename x-pack/legacy/plugins/ml/public/application/explorer/{explorer_constants.js => explorer_constants.ts} (54%) delete mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/explorer_controller.js delete mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/explorer_dashboard_service.js create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/explorer_dashboard_service.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/explorer_directive.tsx delete mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/explorer_react_wrapper_directive.js create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/explorer_route.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.d.ts rename x-pack/legacy/plugins/ml/public/application/explorer/{index.js => index.ts} (80%) delete mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/legacy_utils.js create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/legacy_utils.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/reducers/app_state_reducer.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/check_selected_cells.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/get_index_pattern.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/initialize.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_influencer_filter_settings.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_kql_query_bar_placeholder.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/reducers/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/util/app_state_utils.d.ts diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index e816b1858f21e..80031efba6e48 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -199,6 +199,7 @@ export const npStart = { }, getTime: sinon.fake(), setTime: sinon.fake(), + getActiveBounds: sinon.fake(), getBounds: sinon.fake(), calculateBounds: sinon.fake(), createFilter: sinon.fake(), diff --git a/x-pack/legacy/plugins/ml/public/application/components/controls/checkbox_showcharts/index.d.ts b/x-pack/legacy/plugins/ml/public/application/components/controls/checkbox_showcharts/index.d.ts new file mode 100644 index 0000000000000..4d6952d3b3fc3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/controls/checkbox_showcharts/index.d.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. + */ + +import { BehaviorSubject } from 'rxjs'; + +export const showCharts$: BehaviorSubject; diff --git a/x-pack/legacy/plugins/ml/public/application/components/controls/select_interval/index.d.ts b/x-pack/legacy/plugins/ml/public/application/components/controls/select_interval/index.d.ts new file mode 100644 index 0000000000000..4a8273972389a --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/controls/select_interval/index.d.ts @@ -0,0 +1,12 @@ +/* + * 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 { BehaviorSubject } from 'rxjs'; + +export const interval$: BehaviorSubject<{ + value: string; + text: string; +}>; diff --git a/x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/index.d.ts b/x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/index.d.ts new file mode 100644 index 0000000000000..006d23da56f82 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/index.d.ts @@ -0,0 +1,13 @@ +/* + * 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 { BehaviorSubject } from 'rxjs'; + +export const severity$: BehaviorSubject<{ + val: number; + display: string; + color: string; +}>; diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_select_service_utils.d.ts b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_select_service_utils.d.ts new file mode 100644 index 0000000000000..fe5966524c7e5 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_select_service_utils.d.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { BehaviorSubject } from 'rxjs'; + +import { State } from 'ui/state_management/state'; + +export declare type JobSelectService$ = BehaviorSubject<{ + selection: string[]; + groups: string[]; + resetSelection: boolean; +}>; + +declare interface JobSelectService { + jobSelectService$: JobSelectService$; + unsubscribeFromGlobalState(): void; +} + +export const jobSelectServiceFactory: (globalState: State) => JobSelectService; diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_select_service_utils.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_select_service_utils.js index 7615a9ddc8a68..8d84c16a40b3b 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_select_service_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_select_service_utils.js @@ -37,16 +37,16 @@ function getInvalidJobIds(ids) { export const jobSelectServiceFactory = (globalState) => { const { jobIds, selectedGroups } = getSelectedJobIds(globalState); - const jobSelectService = new BehaviorSubject({ selection: jobIds, groups: selectedGroups, resetSelection: false }); + const jobSelectService$ = new BehaviorSubject({ selection: jobIds, groups: selectedGroups, resetSelection: false }); // Subscribe to changes to globalState and trigger // a jobSelectService update if the job selection changed. const listener = () => { const { jobIds: newJobIds, selectedGroups: newSelectedGroups } = getSelectedJobIds(globalState); - const oldSelectedJobIds = jobSelectService.getValue().selection; + const oldSelectedJobIds = jobSelectService$.getValue().selection; if (newJobIds && !(isEqual(oldSelectedJobIds, newJobIds))) { - jobSelectService.next({ selection: newJobIds, groups: newSelectedGroups }); + jobSelectService$.next({ selection: newJobIds, groups: newSelectedGroups }); } }; @@ -56,7 +56,7 @@ export const jobSelectServiceFactory = (globalState) => { globalState.off('save_with_changes', listener); }; - return { jobSelectService, unsubscribeFromGlobalState }; + return { jobSelectService$, unsubscribeFromGlobalState }; }; function loadJobIdsFromGlobalState(globalState) { // jobIds, groups diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.js index 7725cf5e59482..7ccc3962ba923 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.js +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.js @@ -73,7 +73,7 @@ const DEFAULT_GANTT_BAR_WIDTH = 299; // pixels export function JobSelector({ dateFormatTz, globalState, - jobSelectService, + jobSelectService$, selectedJobIds, selectedGroups, singleSelection, @@ -93,7 +93,7 @@ export function JobSelector({ useEffect(() => { // listen for update from Single Metric Viewer - const subscription = jobSelectService.subscribe(({ selection, resetSelection }) => { + const subscription = jobSelectService$.subscribe(({ selection, resetSelection }) => { if (resetSelection === true) { setSelectedIds(selection); } @@ -405,7 +405,7 @@ export function JobSelector({ JobSelector.propTypes = { globalState: PropTypes.object, - jobSelectService: PropTypes.object, + jobSelectService$: PropTypes.object, selectedJobIds: PropTypes.array, singleSelection: PropTypes.bool, timeseriesOnly: PropTypes.bool diff --git a/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/__tests__/loading_indicator_directive.js b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/__tests__/loading_indicator_directive.js deleted file mode 100644 index c0d461d95cd7b..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/__tests__/loading_indicator_directive.js +++ /dev/null @@ -1,79 +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 ngMock from 'ng_mock'; -import expect from '@kbn/expect'; - -describe('ML - ', () => { - let $scope; - let $compile; - let $element; - - beforeEach(() => { - ngMock.module('apps/ml'); - ngMock.inject(function (_$compile_, $rootScope) { - $compile = _$compile_; - $scope = $rootScope.$new(); - }); - }); - - afterEach(function () { - $scope.$destroy(); - }); - - it('Default loading indicator without attributes should not be visible', () => { - $element = $compile('')($scope); - $scope.$apply(); - $element.on('renderComplete', () => { - expect($element.find('*').length).to.be(0); - }); - }); - - it('Enables the loading indicator, checks the default height and non-existant label', () => { - $element = $compile('')($scope); - $scope.$apply(); - $element.on('renderComplete', () => { - expect($element.find('.loading-indicator').length).to.be(1); - expect($element.find('.loading-indicator').css('height')).to.be('100px'); - expect($element.find('[ml-loading-indicator-label]').length).to.be(0); - }); - }); - - it('Sets a custom height', () => { - $element = $compile('')($scope); - $scope.$apply(); - $element.on('renderComplete', () => { - expect($element.find('.loading-indicator').css('height')).to.be('200px'); - }); - }); - - it('Sets a custom label', () => { - const labelName = 'my-label'; - $element = $compile(``)($scope); - $scope.$apply(); - $element.on('renderComplete', () => { - expect($element.find('[ml-loading-indicator-label]').text()).to.be(labelName); - }); - }); - - it('Triggers a scope-change of isLoading', () => { - $scope.isLoading = false; - $element = $compile('')($scope); - $scope.$apply(); - - $element.on('renderComplete', () => { - expect($element.find('*').length).to.be(0); - - $scope.isLoading = true; - $scope.$apply(); - $element.on('renderComplete', () => { - expect($element.find('.loading-indicator').length).to.be(1); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/_loading_indicator.scss b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/_loading_indicator.scss index 7bbe1d18987b2..e0a048223b7ff 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/_loading_indicator.scss +++ b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/_loading_indicator.scss @@ -1,22 +1,5 @@ // SASSTODO: This needs to be replaced with EuiLoadingSpinner -/* angular */ -ml-loading-indicator { - .loading-indicator { - text-align: center; - font-size: 17px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - - .loading-spinner { - font-size: 24px; - } - } -} - -/* react */ .ml-loading-indicator { text-align: center; font-size: 17px; @@ -24,8 +7,4 @@ ml-loading-indicator { flex-direction: column; align-items: center; justify-content: center; - - .loading-spinner { - font-size: 24px; - } } diff --git a/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/index.js b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/index.js index ac91f0434ea10..0e4613bcdccca 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/index.js +++ b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/index.js @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ - - - -import './loading_indicator_directive'; +export { LoadingIndicator } from './loading_indicator'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.html b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.html deleted file mode 100644 index 4c56e8e2731c7..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.html +++ /dev/null @@ -1,4 +0,0 @@ -
-
-
{{label}}
-
diff --git a/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.js b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.js index ed1d0276fed2c..3d5e8583bd794 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.js +++ b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.js @@ -9,14 +9,19 @@ import PropTypes from 'prop-types'; import React from 'react'; +import { EuiLoadingChart, EuiSpacer } from '@elastic/eui'; + export function LoadingIndicator({ height, label }) { height = height ? +height : 100; return (
-
- {label && -
{label}
- } + + {label && ( + <> + +
{label}
+ + )}
); } diff --git a/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator_directive.js b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator_directive.js deleted file mode 100644 index e607b8a9af64f..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator_directive.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - -import template from './loading_indicator.html'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -module.directive('mlLoadingIndicator', function () { - return { - restrict: 'E', - template, - transclude: true, - scope: { - label: '@?', - isLoading: '<', - height: ' -
diff --git a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.test.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.test.tsx index e06c5e44323d1..b64cccc9eb9b9 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiSuperDatePicker } from '@elastic/eui'; -import { uiTimefilterMock } from '../../../contexts/ui/__mocks__/mocks'; +import { uiTimefilterMock } from '../../../contexts/ui/__mocks__/mocks_jest'; import { mlTimefilterRefresh$ } from '../../../services/timefilter_refresh_service'; import { TopNav } from './top_nav'; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks_jest.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks_jest.ts diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks_mocha.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks_mocha.ts new file mode 100644 index 0000000000000..cd3d80bed8d14 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks_mocha.ts @@ -0,0 +1,84 @@ +/* + * 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 { Subject } from 'rxjs'; + +export const uiChromeMock = { + getBasePath: () => 'basePath', + getUiSettingsClient: () => { + return { + get: (key: string) => { + switch (key) { + case 'dateFormat': + return 'MMM D, YYYY @ HH:mm:ss.SSS'; + case 'theme:darkMode': + return false; + case 'timepicker:timeDefaults': + return {}; + case 'timepicker:refreshIntervalDefaults': + return { pause: false, value: 0 }; + default: + throw new Error(`Unexpected config key: ${key}`); + } + }, + }; + }, +}; + +interface RefreshInterval { + value: number; + pause: boolean; +} + +const time = { + from: 'Thu Aug 29 2019 02:04:19 GMT+0200', + to: 'Sun Sep 29 2019 01:45:36 GMT+0200', +}; + +export const uiTimefilterMock = { + isAutoRefreshSelectorEnabled() { + return this._isAutoRefreshSelectorEnabled; + }, + isTimeRangeSelectorEnabled() { + return this._isTimeRangeSelectorEnabled; + }, + enableAutoRefreshSelector() { + this._isAutoRefreshSelectorEnabled = true; + }, + enableTimeRangeSelector() { + this._isTimeRangeSelectorEnabled = true; + }, + getActiveBounds() { + return; + }, + getEnabledUpdated$() { + return { subscribe: () => {} }; + }, + getFetch$() { + return new Subject(); + }, + getRefreshInterval() { + return this.refreshInterval; + }, + getRefreshIntervalUpdate$() { + return { subscribe: () => {} }; + }, + getTime: () => time, + getTimeUpdate$() { + return { subscribe: () => {} }; + }, + _isAutoRefreshSelectorEnabled: false, + _isTimeRangeSelectorEnabled: false, + refreshInterval: { value: 0, pause: true }, + on: (event: string, reload: () => void) => {}, + setRefreshInterval(refreshInterval: RefreshInterval) { + this.refreshInterval = refreshInterval; + }, +}; + +export const uiTimeHistoryMock = { + get: () => [time], +}; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_chrome_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_chrome_context.ts index 929a9454f9682..4964d727a0452 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_chrome_context.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_chrome_context.ts @@ -4,6 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { uiChromeMock } from './mocks'; +import { uiChromeMock } from './mocks_jest'; export const useUiChromeContext = () => uiChromeMock; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_context.ts index ec2a812e9fe78..0aaaa868c490a 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_context.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_context.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { uiChromeMock, uiTimefilterMock, uiTimeHistoryMock } from './mocks'; +import { uiChromeMock, uiTimefilterMock, uiTimeHistoryMock } from './mocks_jest'; export const useUiContext = () => ({ chrome: uiChromeMock, diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/__tests__/explorer_controller.js b/x-pack/legacy/plugins/ml/public/application/explorer/__tests__/explorer_controller.js deleted file mode 100644 index d4fe0de11ca06..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/explorer/__tests__/explorer_controller.js +++ /dev/null @@ -1,25 +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 ngMock from 'ng_mock'; -import expect from '@kbn/expect'; - -describe('ML - Explorer Controller', () => { - beforeEach(() => { - ngMock.module('kibana'); - }); - - it('Initialize Explorer Controller', () => { - ngMock.inject(function ($rootScope, $controller) { - const scope = $rootScope.$new(); - $controller('MlExplorerController', { $scope: scope }); - - expect(Array.isArray(scope.jobs)).to.be(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/__tests__/explorer_directive.js b/x-pack/legacy/plugins/ml/public/application/explorer/__tests__/explorer_directive.js new file mode 100644 index 0000000000000..9b887381768c6 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/__tests__/explorer_directive.js @@ -0,0 +1,58 @@ +/* + * 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 ngMock from 'ng_mock'; +import sinon from 'sinon'; +import expect from '@kbn/expect'; + +import { uiChromeMock, uiTimefilterMock, uiTimeHistoryMock } from '../../contexts/ui/__mocks__/mocks_mocha'; +import * as useUiContextModule from '../../contexts/ui/use_ui_context'; +import * as UiTimefilterModule from 'ui/timefilter'; + +describe('ML - Anomaly Explorer Directive', () => { + let $scope; + let $compile; + let $element; + let stubContext; + let stubTimefilterFetch; + + beforeEach(ngMock.module('kibana')); + beforeEach(() => { + ngMock.inject(function ($injector) { + stubContext = sinon.stub(useUiContextModule, 'useUiContext').callsFake(function fakeFn() { + return { + chrome: uiChromeMock, + timefilter: uiTimefilterMock, + timeHistory: uiTimeHistoryMock, + }; + }); + stubTimefilterFetch = sinon.stub(UiTimefilterModule.timefilter, 'getFetch$').callsFake(uiTimefilterMock.getFetch$); + + $compile = $injector.get('$compile'); + const $rootScope = $injector.get('$rootScope'); + $scope = $rootScope.$new(); + }); + }); + + afterEach(() => { + stubContext.restore(); + stubTimefilterFetch.restore(); + $scope.$destroy(); + }); + + it('Initialize Anomaly Explorer Directive', (done) => { + ngMock.inject(function () { + expect(() => { + $element = $compile('')($scope); + }).to.not.throwError(); + + // directive has scope: false + const scope = $element.isolateScope(); + expect(scope).to.eql(undefined); + done(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/actions/index.ts b/x-pack/legacy/plugins/ml/public/application/explorer/actions/index.ts new file mode 100644 index 0000000000000..1528a7ce7eee1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/actions/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { jobSelectionActionCreator } from './job_selection'; +export { loadExplorerData } from './load_explorer_data'; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/actions/job_selection.ts b/x-pack/legacy/plugins/ml/public/application/explorer/actions/job_selection.ts new file mode 100644 index 0000000000000..76d66bfbbf12b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/actions/job_selection.ts @@ -0,0 +1,46 @@ +/* + * 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 { from } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { mlFieldFormatService } from '../../services/field_format_service'; +import { mlJobService } from '../../services/job_service'; + +import { createJobs, RestoredAppState } from '../explorer_utils'; + +export function jobSelectionActionCreator( + actionName: string, + selectedJobIds: string[], + { filterData, selectedCells, viewBySwimlaneFieldName }: RestoredAppState +) { + return from(mlFieldFormatService.populateFormats(selectedJobIds)).pipe( + map(resp => { + if (resp.err) { + console.log('Error populating field formats:', resp.err); // eslint-disable-line no-console + return null; + } + + const jobs = createJobs(mlJobService.jobs).map(job => { + job.selected = selectedJobIds.some(id => job.id === id); + return job; + }); + + const selectedJobs = jobs.filter(job => job.selected); + + return { + type: actionName, + payload: { + loading: false, + selectedCells, + selectedJobs, + viewBySwimlaneFieldName, + filterData, + }, + }; + }) + ); +} diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/actions/load_explorer_data.ts b/x-pack/legacy/plugins/ml/public/application/explorer/actions/load_explorer_data.ts new file mode 100644 index 0000000000000..6d4edd909fa8f --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/actions/load_explorer_data.ts @@ -0,0 +1,197 @@ +/* + * 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 memoizeOne from 'memoize-one'; +import { isEqual } from 'lodash'; + +import { forkJoin, of } from 'rxjs'; +import { mergeMap, tap } from 'rxjs/operators'; + +import { explorerChartsContainerServiceFactory } from '../explorer_charts/explorer_charts_container_service'; +import { VIEW_BY_JOB_LABEL } from '../explorer_constants'; +import { explorerService } from '../explorer_dashboard_service'; +import { + getDateFormatTz, + getSelectionInfluencers, + getSelectionTimeRange, + loadAnnotationsTableData, + loadAnomaliesTableData, + loadDataForCharts, + loadFilteredTopInfluencers, + loadOverallData, + loadTopInfluencers, + loadViewBySwimlane, + loadViewByTopFieldValuesForSelectedTime, +} from '../explorer_utils'; +import { ExplorerState } from '../reducers'; + +const memoizeIsEqual = (newArgs: any[], lastArgs: any[]) => isEqual(newArgs, lastArgs); + +// Memoize the data fetching methods +// TODO: We need to track an attribute that allows refetching when the date picker +// triggers a refresh, otherwise we'll get back the stale data. Note this was also +// an issue with the previous version and the custom caching done within the component. +const memoizedLoadAnnotationsTableData = memoizeOne(loadAnnotationsTableData, memoizeIsEqual); +const memoizedLoadDataForCharts = memoizeOne(loadDataForCharts, memoizeIsEqual); +const memoizedLoadFilteredTopInfluencers = memoizeOne(loadFilteredTopInfluencers, memoizeIsEqual); +const memoizedLoadOverallData = memoizeOne(loadOverallData, memoizeIsEqual); +const memoizedLoadTopInfluencers = memoizeOne(loadTopInfluencers, memoizeIsEqual); +const memoizedLoadViewBySwimlane = memoizeOne(loadViewBySwimlane, memoizeIsEqual); +const memoizedLoadAnomaliesTableData = memoizeOne(loadAnomaliesTableData, memoizeIsEqual); + +const dateFormatTz = getDateFormatTz(); + +/** + * Fetches the data necessary for the Anomaly Explorer using observables. + * + * @param state ExplorerState + * + * @return Partial + */ +export function loadExplorerData(state: ExplorerState) { + const { + bounds, + influencersFilterQuery, + noInfluencersConfigured, + selectedCells, + selectedJobs, + swimlaneBucketInterval, + swimlaneLimit, + tableInterval, + tableSeverity, + viewBySwimlaneFieldName, + } = state; + + if (selectedJobs === null || bounds === undefined || viewBySwimlaneFieldName === undefined) { + return of({}); + } + + // TODO This factory should be refactored so we can load the charts using memoization. + const updateCharts = explorerChartsContainerServiceFactory(explorerService.setCharts); + + const selectionInfluencers = getSelectionInfluencers(selectedCells, viewBySwimlaneFieldName); + + const jobIds = + selectedCells !== null && selectedCells.viewByFieldName === VIEW_BY_JOB_LABEL + ? selectedCells.lanes + : selectedJobs.map(d => d.id); + + const timerange = getSelectionTimeRange( + selectedCells, + swimlaneBucketInterval.asSeconds(), + bounds + ); + + // First get the data where we have all necessary args at hand using forkJoin: + // annotationsData, anomalyChartRecords, influencers, overallState, tableData, topFieldValues + return forkJoin({ + annotationsData: memoizedLoadAnnotationsTableData( + selectedCells, + selectedJobs, + swimlaneBucketInterval.asSeconds(), + bounds + ), + anomalyChartRecords: memoizedLoadDataForCharts( + jobIds, + timerange.earliestMs, + timerange.latestMs, + selectionInfluencers, + selectedCells, + influencersFilterQuery + ), + influencers: + selectionInfluencers.length === 0 + ? memoizedLoadTopInfluencers( + jobIds, + timerange.earliestMs, + timerange.latestMs, + [], + noInfluencersConfigured, + influencersFilterQuery + ) + : Promise.resolve({}), + overallState: memoizedLoadOverallData(selectedJobs, swimlaneBucketInterval, bounds), + tableData: memoizedLoadAnomaliesTableData( + selectedCells, + selectedJobs, + dateFormatTz, + swimlaneBucketInterval.asSeconds(), + bounds, + viewBySwimlaneFieldName, + tableInterval, + tableSeverity, + influencersFilterQuery + ), + topFieldValues: + selectedCells !== null && selectedCells.showTopFieldValues === true + ? loadViewByTopFieldValuesForSelectedTime( + timerange.earliestMs, + timerange.latestMs, + selectedJobs, + viewBySwimlaneFieldName, + swimlaneLimit, + noInfluencersConfigured + ) + : Promise.resolve([]), + }).pipe( + // Trigger a side-effect action to reset view-by swimlane, + // show the view-by loading indicator + // and pass on the data we already fetched. + tap(explorerService.setViewBySwimlaneLoading), + // Trigger a side-effect to update the charts. + tap(({ anomalyChartRecords }) => { + if (selectedCells !== null && Array.isArray(anomalyChartRecords)) { + updateCharts(anomalyChartRecords, timerange.earliestMs, timerange.latestMs); + } else { + updateCharts([], timerange.earliestMs, timerange.latestMs); + } + }), + // Load view-by swimlane data and filtered top influencers. + // mergeMap is used to have access to the already fetched data and act on it in arg #1. + // In arg #2 of mergeMap we combine the data and pass it on in the action format + // which can be consumed by explorerReducer() later on. + mergeMap( + ({ anomalyChartRecords, influencers, overallState, topFieldValues }) => + forkJoin({ + influencers: + (selectionInfluencers.length > 0 || influencersFilterQuery !== undefined) && + anomalyChartRecords !== undefined && + anomalyChartRecords.length > 0 + ? memoizedLoadFilteredTopInfluencers( + jobIds, + timerange.earliestMs, + timerange.latestMs, + anomalyChartRecords, + selectionInfluencers, + noInfluencersConfigured, + influencersFilterQuery + ) + : Promise.resolve(influencers), + viewBySwimlaneState: memoizedLoadViewBySwimlane( + topFieldValues, + { + earliest: overallState.overallSwimlaneData.earliest, + latest: overallState.overallSwimlaneData.latest, + }, + selectedJobs, + viewBySwimlaneFieldName, + swimlaneLimit, + influencersFilterQuery, + noInfluencersConfigured + ), + }), + ({ annotationsData, overallState, tableData }, { influencers, viewBySwimlaneState }) => { + return { + annotationsData, + influencers, + ...overallState, + ...viewBySwimlaneState, + tableData, + }; + } + ) + ); +} diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.js b/x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.ts similarity index 90% rename from x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.js rename to x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.ts index 243adecaec78f..c0dcd9e249b3b 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.ts @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ - -import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../../breadcrumbs'; import { i18n } from '@kbn/i18n'; - +import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../../breadcrumbs'; export function getAnomalyExplorerBreadcrumbs() { // Whilst top level nav menu with tabs remains, @@ -17,10 +15,9 @@ export function getAnomalyExplorerBreadcrumbs() { ANOMALY_DETECTION_BREADCRUMB, { text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', { - defaultMessage: 'Anomaly Explorer' + defaultMessage: 'Anomaly Explorer', }), - href: '' - } + href: '', + }, ]; } - diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap index 77821663783cf..3ba4ebb2acdea 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap @@ -5,11 +5,11 @@ exports[`ExplorerNoInfluencersFound snapshot 1`] = ` title={

diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.js index 2170c20e3b5ea..b17ca7577b2bd 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.js @@ -14,7 +14,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiEmptyPrompt } from '@elastic/eui'; -export const ExplorerNoInfluencersFound = ({ swimlaneViewByFieldName, showFilterMessage = false }) => ( +export const ExplorerNoInfluencersFound = ({ viewBySwimlaneFieldName, showFilterMessage = false }) => ( )} {showFilterMessage === true && ( )}

@@ -38,6 +38,6 @@ export const ExplorerNoInfluencersFound = ({ swimlaneViewByFieldName, showFilter />); ExplorerNoInfluencersFound.propTypes = { - swimlaneViewByFieldName: PropTypes.string.isRequired, + viewBySwimlaneFieldName: PropTypes.string.isRequired, showFilterMessage: PropTypes.bool }; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.test.js index 2ec17e614f235..fddeeb4ed6bd9 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.test.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.test.js @@ -11,7 +11,7 @@ import { ExplorerNoInfluencersFound } from './explorer_no_influencers_found'; describe('ExplorerNoInfluencersFound', () => { test('snapshot', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer.d.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.d.ts new file mode 100644 index 0000000000000..de58b9228c076 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.d.ts @@ -0,0 +1,18 @@ +/* + * 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 { FC } from 'react'; + +import { State } from 'ui/state_management/state'; + +import { JobSelectService$ } from '../components/job_selector/job_select_service_utils'; + +declare interface ExplorerProps { + globalState: State; + jobSelectService$: JobSelectService$; +} + +export const Explorer: FC; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js index 985282df18f6a..50a57f634fd1b 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js @@ -8,13 +8,12 @@ * React component for rendering Explorer dashboard swimlanes. */ -import _ from 'lodash'; import PropTypes from 'prop-types'; -import React, { Fragment } from 'react'; +import React, { createRef } from 'react'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import DragSelect from 'dragselect/dist/ds.min.js'; +import { merge, Subject } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; -import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, @@ -36,12 +35,14 @@ import { import { ChartTooltip } from '../components/chart_tooltip'; import { ExplorerSwimlane } from './explorer_swimlane'; import { KqlFilterBar } from '../components/kql_filter_bar'; -import { formatHumanReadableDateTime } from '../util/date_utils'; -import { getBoundsRoundedToInterval } from '../util/time_buckets'; +import { TimeBuckets } from '../util/time_buckets'; import { getSelectedJobIds } from '../components/job_selector/job_select_service_utils'; import { InfluencersList } from '../components/influencers_list'; -import { ALLOW_CELL_RANGE_SELECTION, dragSelect$, explorer$ } from './explorer_dashboard_service'; -import { mlResultsService } from '../services/results_service'; +import { + ALLOW_CELL_RANGE_SELECTION, + dragSelect$, + explorerService, +} from './explorer_dashboard_service'; import { LoadingIndicator } from '../components/loading_indicator/loading_indicator'; import { NavigationMenu } from '../components/navigation_menu'; import { CheckboxShowCharts, showCharts$ } from '../components/controls/checkbox_showcharts'; @@ -57,80 +58,32 @@ import { escapeParens, escapeDoubleQuotes } from '../components/kql_filter_bar/utils'; +import { mlJobService } from '../services/job_service'; import { - getClearedSelectedAnomaliesState, - getDefaultViewBySwimlaneData, - getFilteredTopInfluencers, - getSelectionInfluencers, - getSelectionTimeRange, - getViewBySwimlaneOptions, - loadAnnotationsTableData, - loadAnomaliesTableData, - loadDataForCharts, - loadTopInfluencers, - processOverallResults, - processViewByResults, - getInfluencers, + getDateFormatTz, + restoreAppState, } from './explorer_utils'; -import { - explorerChartsContainerServiceFactory, - getDefaultChartsData -} from './explorer_charts/explorer_charts_container_service'; -import { - getSwimlaneContainerWidth -} from './legacy_utils'; +import { getSwimlaneContainerWidth } from './legacy_utils'; import { DRAG_SELECT_ACTION, - APP_STATE_ACTION, - EXPLORER_ACTION, FILTER_ACTION, SWIMLANE_TYPE, VIEW_BY_JOB_LABEL, } from './explorer_constants'; -import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; // Explorer Charts import { ExplorerChartsContainer } from './explorer_charts/explorer_charts_container'; // Anomalies Table import { AnomaliesTable } from '../components/anomalies_table/anomalies_table'; + +import { ResizeChecker } from '../../../../../../../src/plugins/kibana_utils/public'; import { timefilter } from 'ui/timefilter'; import { toastNotifications } from 'ui/notify'; import { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service'; -import { Subject } from 'rxjs'; - -function getExplorerDefaultState() { - return { - annotationsData: [], - anomalyChartRecords: [], - chartsData: getDefaultChartsData(), - filterActive: false, - filteredFields: [], - filterPlaceHolder: undefined, - indexPattern: { title: ML_RESULTS_INDEX_PATTERN, fields: [] }, - influencersFilterQuery: undefined, - hasResults: false, - influencers: {}, - isAndOperator: false, - loading: true, - noInfluencersConfigured: true, - noJobsFound: true, - overallSwimlaneData: [], - queryString: '', - selectedCells: null, - selectedJobs: null, - swimlaneViewByFieldName: undefined, - tableData: {}, - tableQueryString: '', - viewByLoadedForTimeFormatted: null, - viewBySwimlaneData: getDefaultViewBySwimlaneData(), - viewBySwimlaneDataLoading: false, - viewBySwimlaneOptions: [], - }; -} function mapSwimlaneOptionsToEuiOptions(options) { return options.map(option => ({ @@ -139,44 +92,34 @@ function mapSwimlaneOptionsToEuiOptions(options) { })); } -const ExplorerPage = ({ children, jobSelectorProps }) => ( - +const ExplorerPage = ({ children, jobSelectorProps, resizeRef }) => ( +
{children} - +
); export const Explorer = injectI18n(injectObservablesAsProps( { annotationsRefresh: annotationsRefresh$, - explorer: explorer$, + explorerState: explorerService.state$, showCharts: showCharts$, - swimlaneLimit: limit$.pipe(map(d => d.val)), - tableInterval: interval$.pipe(map(d => d.val)), - tableSeverity: severity$.pipe(map(d => d.val)), }, class Explorer extends React.Component { static propTypes = { - appStateHandler: PropTypes.func.isRequired, - config: PropTypes.object.isRequired, - dateFormatTz: PropTypes.string.isRequired, + annotationsRefresh: PropTypes.bool, + explorerState: PropTypes.object.isRequired, + explorer: PropTypes.object, globalState: PropTypes.object.isRequired, - jobSelectService: PropTypes.object.isRequired, - TimeBuckets: PropTypes.func.isRequired, + jobSelectService$: PropTypes.object.isRequired, + showCharts: PropTypes.bool.isRequired, }; _unsubscribeAll = new Subject(); - state = getExplorerDefaultState(); - // make sure dragSelect is only available if the mouse pointer is actually over a swimlane disableDragSelectOnMouseLeave = true; - // skip listening to clicks on swimlanes while they are loading to avoid race conditions - skipCellClicks = true; - - // initialize an empty callback, this will be set in componentDidMount() - updateCharts = () => {}; dragSelect = new DragSelect({ selectables: document.getElementsByClassName('sl-cell'), @@ -217,719 +160,84 @@ export const Explorer = injectI18n(injectObservablesAsProps( this.dragSelect.setSelectables(document.getElementsByClassName('sl-cell')); }; + resizeRef = createRef(); + resizeChecker = undefined; + resizeHandler = () => { + explorerService.setSwimlaneContainerWidth(getSwimlaneContainerWidth()); + } + componentDidMount() { - this.updateCharts = explorerChartsContainerServiceFactory((data) => { - this.setState({ - chartsData: { - ...getDefaultChartsData(), - chartsPerRow: data.chartsPerRow, - seriesToPlot: data.seriesToPlot, - // convert truthy/falsy value to Boolean - tooManyBuckets: !!data.tooManyBuckets, - } - }); - }); + timefilter.enableTimeRangeSelector(); + timefilter.enableAutoRefreshSelector(); + + explorerService.setBounds(timefilter.getActiveBounds()); - mlTimefilterRefresh$.pipe(takeUntil(this._unsubscribeAll)).subscribe(() => { - this.resetCache(); - this.updateExplorer(); + // Refresh all the data when the time range is altered. + merge( + mlTimefilterRefresh$, + timefilter.getFetch$() + ).pipe(takeUntil(this._unsubscribeAll)).subscribe(() => { + explorerService.setBounds(timefilter.getActiveBounds()); }); + + + limit$.pipe( + takeUntil(this._unsubscribeAll), + map(d => d.val), + ).subscribe(explorerService.setSwimlaneLimit); + + interval$.pipe( + takeUntil(this._unsubscribeAll), + map(d => ({ tableInterval: d.val })), + ).subscribe(explorerService.setState); + + severity$.pipe( + takeUntil(this._unsubscribeAll), + map(d => ({ tableSeverity: d.val })), + ).subscribe(explorerService.setState); + + // Required to redraw the time series chart when the container is resized. + this.resizeChecker = new ResizeChecker(this.resizeRef.current); + this.resizeChecker.on('resize', this.resizeHandler); + + // restore state stored in URL via AppState and subscribe to + // job updates via job selector. + if (mlJobService.jobs.length > 0) { + let initialized = false; + + this.props.jobSelectService$.pipe(takeUntil(this._unsubscribeAll)).subscribe(({ selection }) => { + if (selection !== undefined) { + if (!initialized) { + explorerService.initialize(selection, restoreAppState(this.props.explorerState.appState)); + initialized = true; + } else { + explorerService.updateJobSelection(selection, restoreAppState(this.props.explorerState.appState)); + } + } + }); + } else { + explorerService.clearJobs(); + } } componentWillUnmount() { this._unsubscribeAll.next(); this._unsubscribeAll.complete(); + this.resizeChecker.destroy(); } resetCache() { - this.loadOverallDataPreviousArgs = null; - this.loadViewBySwimlanePreviousArgs = null; - this.topFieldsPreviousArgs = null; - this.annotationsTablePreviousArgs = null; this.anomaliesTablePreviousArgs = null; } - // based on the pattern described here: - // https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html#fetching-external-data-when-props-change - // instead of our previous approach using custom listeners, here we react to prop changes - // and trigger corresponding updates to the component's state via updateExplorer() - previousSwimlaneLimit = limit$.getValue().val; - previousTableInterval = interval$.getValue().val; - previousTableSeverity = severity$.getValue().val; - async componentDidUpdate() { - if (this.props.explorer !== undefined && this.props.explorer.action !== EXPLORER_ACTION.IDLE) { - explorer$.next({ action: EXPLORER_ACTION.IDLE }); - - const { action, payload } = this.props.explorer; - - if (action === EXPLORER_ACTION.INITIALIZE) { - const { noJobsFound, selectedCells, selectedJobs, swimlaneViewByFieldName, filterData } = payload; - let currentSelectedCells = this.state.selectedCells; - let currentSwimlaneViewByFieldName = this.state.swimlaneViewByFieldName; - - if (swimlaneViewByFieldName !== undefined) { - currentSwimlaneViewByFieldName = swimlaneViewByFieldName; - } - - if (selectedCells !== undefined && currentSelectedCells === null) { - currentSelectedCells = selectedCells; - } - - const stateUpdate = { - noInfluencersConfigured: (getInfluencers(selectedJobs).length === 0), - noJobsFound, - selectedCells: currentSelectedCells, - selectedJobs, - swimlaneViewByFieldName: currentSwimlaneViewByFieldName - }; - - if (filterData.influencersFilterQuery !== undefined) { - Object.assign(stateUpdate, { ...filterData }); - } - - const indexPattern = await this.getIndexPattern(selectedJobs); - stateUpdate.indexPattern = indexPattern; - - this.updateExplorer(stateUpdate, true); - return; - } - - // Listen for changes to job selection. - if (action === EXPLORER_ACTION.JOB_SELECTION_CHANGE) { - const { selectedJobs } = payload; - const stateUpdate = { - noInfluencersConfigured: (getInfluencers(selectedJobs).length === 0), - selectedJobs, - }; - - this.props.appStateHandler(APP_STATE_ACTION.CLEAR_SELECTION); - Object.assign(stateUpdate, getClearedSelectedAnomaliesState()); - // clear filter if selected jobs have no influencers - if (stateUpdate.noInfluencersConfigured === true) { - this.props.appStateHandler(APP_STATE_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS); - const noFilterState = { - filterActive: false, - filteredFields: [], - influencersFilterQuery: undefined, - maskAll: false, - queryString: '', - tableQueryString: '' - }; - - Object.assign(stateUpdate, noFilterState); - } else { - // indexPattern will not be used if there are no influencers so set up can be skipped - // indexPattern is passed to KqlFilterBar which is only shown if (noInfluencersConfigured === false) - const indexPattern = await this.getIndexPattern(selectedJobs); - stateUpdate.indexPattern = indexPattern; - } - - if (selectedJobs.length > 1) { - this.props.appStateHandler( - APP_STATE_ACTION.SAVE_SWIMLANE_VIEW_BY_FIELD_NAME, - { swimlaneViewByFieldName: VIEW_BY_JOB_LABEL }, - ); - stateUpdate.swimlaneViewByFieldName = VIEW_BY_JOB_LABEL; - // enforce a state update for swimlaneViewByFieldName - this.setState({ swimlaneViewByFieldName: VIEW_BY_JOB_LABEL }, () => { - this.updateExplorer(stateUpdate, true); - }); - return; - } - - this.updateExplorer(stateUpdate, true); - return; - } - - // RELOAD reloads full Anomaly Explorer and clears the selection. - if (action === EXPLORER_ACTION.RELOAD) { - this.props.appStateHandler(APP_STATE_ACTION.CLEAR_SELECTION); - this.updateExplorer({ ...payload, ...getClearedSelectedAnomaliesState() }, true); - return; - } - - // REDRAW reloads Anomaly Explorer and tries to retain the selection. - if (action === EXPLORER_ACTION.REDRAW) { - this.updateExplorer({}, false); - return; - } - } else if (this.previousSwimlaneLimit !== this.props.swimlaneLimit) { - this.previousSwimlaneLimit = this.props.swimlaneLimit; - this.props.appStateHandler(APP_STATE_ACTION.CLEAR_SELECTION); - this.updateExplorer(getClearedSelectedAnomaliesState(), false); - } else if (this.previousTableInterval !== this.props.tableInterval) { - this.previousTableInterval = this.props.tableInterval; - this.updateExplorer(); - } else if (this.previousTableSeverity !== this.props.tableSeverity) { - this.previousTableSeverity = this.props.tableSeverity; - this.updateExplorer(); - } else if (this.props.annotationsRefresh === true) { + componentDidUpdate() { + // TODO migrate annotations update + if (this.props.annotationsRefresh === true) { annotationsRefresh$.next(false); - // clear the annotations cache and trigger an update - this.annotationsTablePreviousArgs = null; - this.annotationsTablePreviousData = null; - this.updateExplorer(); } } - // Creates index pattern in the format expected by the kuery bar/kuery autocomplete provider - // Field objects required fields: name, type, aggregatable, searchable - async getIndexPattern(selectedJobs) { - const { indexPattern } = this.state; - const influencers = getInfluencers(selectedJobs); - - indexPattern.fields = influencers.map((influencer) => ({ - name: influencer, - type: 'string', - aggregatable: true, - searchable: true - })); - - return indexPattern; - } - - getSwimlaneBucketInterval(selectedJobs) { - const { TimeBuckets } = this.props; - - const swimlaneWidth = getSwimlaneContainerWidth(this.state.noInfluencersConfigured); - // Bucketing interval should be the maximum of the chart related interval (i.e. time range related) - // and the max bucket span for the jobs shown in the chart. - const bounds = timefilter.getActiveBounds(); - const buckets = new TimeBuckets(); - buckets.setInterval('auto'); - buckets.setBounds(bounds); - - const intervalSeconds = buckets.getInterval().asSeconds(); - - // if the swimlane cell widths are too small they will not be visible - // calculate how many buckets will be drawn before the swimlanes are actually rendered - // and increase the interval to widen the cells if they're going to be smaller than 8px - // this has to be done at this stage so all searches use the same interval - const timerangeSeconds = (bounds.max.valueOf() - bounds.min.valueOf()) / 1000; - const numBuckets = parseInt(timerangeSeconds / intervalSeconds); - const cellWidth = Math.floor(swimlaneWidth / numBuckets * 100) / 100; - - // if the cell width is going to be less than 8px, double the interval - if (cellWidth < 8) { - buckets.setInterval((intervalSeconds * 2) + 's'); - } - - const maxBucketSpanSeconds = selectedJobs.reduce((memo, job) => Math.max(memo, job.bucketSpanSeconds), 0); - if (maxBucketSpanSeconds > intervalSeconds) { - buckets.setInterval(maxBucketSpanSeconds + 's'); - buckets.setBounds(bounds); - } - - return buckets.getInterval(); - } - - loadOverallDataPreviousArgs = null; - loadOverallDataPreviousData = null; - loadOverallData(selectedJobs, interval, bounds, showLoadingIndicator = true) { - return new Promise((resolve) => { - // Loads the overall data components i.e. the overall swimlane and influencers list. - if (selectedJobs === null) { - resolve({ - loading: false, - hasResuts: false - }); - return; - } - - // check if we can just return existing cached data - const compareArgs = { - selectedJobs, - intervalAsSeconds: interval.asSeconds(), - boundsMin: bounds.min.valueOf(), - boundsMax: bounds.max.valueOf(), - }; - - if (_.isEqual(compareArgs, this.loadOverallDataPreviousArgs)) { - const overallSwimlaneData = this.loadOverallDataPreviousData; - const hasResults = (overallSwimlaneData.points && overallSwimlaneData.points.length > 0); - resolve({ - hasResults, - loading: false, - overallSwimlaneData, - }); - return; - } - - if (showLoadingIndicator) { - this.setState({ hasResults: false, loading: true }); - } - - // Ensure the search bounds align to the bucketing interval used in the swimlane so - // that the first and last buckets are complete. - const searchBounds = getBoundsRoundedToInterval( - bounds, - interval, - false - ); - const selectedJobIds = selectedJobs.map(d => d.id); - - // Load the overall bucket scores by time. - // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets - // which wouldn't be the case if e.g. '1M' was used. - // Pass 'true' when obtaining bucket bounds due to the way the overall_buckets endpoint works - // to ensure the search is inclusive of end time. - const overallBucketsBounds = getBoundsRoundedToInterval( - bounds, - interval, - true - ); - mlResultsService.getOverallBucketScores( - selectedJobIds, - // Note there is an optimization for when top_n == 1. - // If top_n > 1, we should test what happens when the request takes long - // and refactor the loading calls, if necessary, to avoid delays in loading other components. - 1, - overallBucketsBounds.min.valueOf(), - overallBucketsBounds.max.valueOf(), - interval.asSeconds() + 's' - ).then((resp) => { - this.skipCellClicks = false; - const overallSwimlaneData = processOverallResults( - resp.results, - searchBounds, - interval.asSeconds(), - ); - this.loadOverallDataPreviousArgs = compareArgs; - this.loadOverallDataPreviousData = overallSwimlaneData; - - console.log('Explorer overall swimlane data set:', overallSwimlaneData); - const hasResults = (overallSwimlaneData.points && overallSwimlaneData.points.length > 0); - resolve({ - hasResults, - loading: false, - overallSwimlaneData, - }); - }); - }); - } - - loadViewBySwimlanePreviousArgs = null; - loadViewBySwimlanePreviousData = null; - loadViewBySwimlane(fieldValues, overallSwimlaneData, selectedJobs, swimlaneViewByFieldName, influencersFilterQuery) { - const { swimlaneLimit } = this.props; - - const compareArgs = { - fieldValues, - overallSwimlaneData, - selectedJobs, - swimlaneLimit, - swimlaneViewByFieldName, - interval: this.getSwimlaneBucketInterval(selectedJobs).asSeconds(), - influencersFilterQuery - }; - - return new Promise((resolve) => { - this.skipCellClicks = true; - - // check if we can just return existing cached data - if (_.isEqual(compareArgs, this.loadViewBySwimlanePreviousArgs)) { - this.skipCellClicks = false; - - resolve({ - viewBySwimlaneData: this.loadViewBySwimlanePreviousData, - viewBySwimlaneDataLoading: false - }); - return; - } - - this.setState({ - viewBySwimlaneData: getDefaultViewBySwimlaneData(), - viewBySwimlaneDataLoading: true - }); - - const finish = (resp) => { - this.skipCellClicks = false; - if (resp !== undefined) { - const viewBySwimlaneData = processViewByResults( - resp.results, - fieldValues, - overallSwimlaneData, - swimlaneViewByFieldName, - this.getSwimlaneBucketInterval(selectedJobs).asSeconds(), - ); - this.loadViewBySwimlanePreviousArgs = compareArgs; - this.loadViewBySwimlanePreviousData = viewBySwimlaneData; - console.log('Explorer view by swimlane data set:', viewBySwimlaneData); - - resolve({ - viewBySwimlaneData, - viewBySwimlaneDataLoading: false - }); - } else { - resolve({ viewBySwimlaneDataLoading: false }); - } - }; - - if ( - selectedJobs === undefined || - swimlaneViewByFieldName === undefined - ) { - finish(); - return; - } else { - // Ensure the search bounds align to the bucketing interval used in the swimlane so - // that the first and last buckets are complete. - const bounds = timefilter.getActiveBounds(); - const searchBounds = getBoundsRoundedToInterval( - bounds, - this.getSwimlaneBucketInterval(selectedJobs), - false, - ); - const selectedJobIds = selectedJobs.map(d => d.id); - - // load scores by influencer/jobId value and time. - // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets - // which wouldn't be the case if e.g. '1M' was used. - const interval = `${this.getSwimlaneBucketInterval(selectedJobs).asSeconds()}s`; - if (swimlaneViewByFieldName !== VIEW_BY_JOB_LABEL) { - mlResultsService.getInfluencerValueMaxScoreByTime( - selectedJobIds, - swimlaneViewByFieldName, - fieldValues, - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - interval, - swimlaneLimit, - influencersFilterQuery - ).then(finish); - } else { - const jobIds = (fieldValues !== undefined && fieldValues.length > 0) ? fieldValues : selectedJobIds; - mlResultsService.getScoresByBucket( - jobIds, - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - interval, - swimlaneLimit - ).then(finish); - } - } - }); - } - - topFieldsPreviousArgs = null; - topFieldsPreviousData = null; - loadViewByTopFieldValuesForSelectedTime(earliestMs, latestMs, selectedJobs, swimlaneViewByFieldName) { - const selectedJobIds = selectedJobs.map(d => d.id); - const { swimlaneLimit } = this.props; - - const compareArgs = { - earliestMs, latestMs, selectedJobIds, swimlaneLimit, swimlaneViewByFieldName, - interval: this.getSwimlaneBucketInterval(selectedJobs).asSeconds() - }; - - // Find the top field values for the selected time, and then load the 'view by' - // swimlane over the full time range for those specific field values. - return new Promise((resolve) => { - if (_.isEqual(compareArgs, this.topFieldsPreviousArgs)) { - resolve(this.topFieldsPreviousData); - return; - } - this.topFieldsPreviousArgs = compareArgs; - - if (swimlaneViewByFieldName !== VIEW_BY_JOB_LABEL) { - mlResultsService.getTopInfluencers( - selectedJobIds, - earliestMs, - latestMs, - swimlaneLimit - ).then((resp) => { - if (resp.influencers[swimlaneViewByFieldName] === undefined) { - this.topFieldsPreviousData = []; - resolve([]); - } - - const topFieldValues = []; - const topInfluencers = resp.influencers[swimlaneViewByFieldName]; - topInfluencers.forEach((influencerData) => { - if (influencerData.maxAnomalyScore > 0) { - topFieldValues.push(influencerData.influencerFieldValue); - } - }); - this.topFieldsPreviousData = topFieldValues; - resolve(topFieldValues); - }); - } else { - mlResultsService.getScoresByBucket( - selectedJobIds, - earliestMs, - latestMs, - this.getSwimlaneBucketInterval(selectedJobs).asSeconds() + 's', - swimlaneLimit - ).then((resp) => { - const topFieldValues = Object.keys(resp.results); - this.topFieldsPreviousData = topFieldValues; - resolve(topFieldValues); - }); - } - }); - } - - anomaliesTablePreviousArgs = null; - anomaliesTablePreviousData = null; - annotationsTablePreviousArgs = null; - annotationsTablePreviousData = null; - async updateExplorer(stateUpdate = {}, showOverallLoadingIndicator = true) { - const { - filterActive, - filteredFields, - influencersFilterQuery, - isAndOperator, - noInfluencersConfigured, - noJobsFound, - selectedCells, - selectedJobs, - swimlaneViewByFieldName, - } = { - ...this.state, - ...stateUpdate - }; - - this.skipCellClicks = false; - - if (noJobsFound) { - this.setState(stateUpdate); - return; - } - - if (this.swimlaneCellClickQueue.length > 0) { - this.setState(stateUpdate); - - const latestSelectedCells = this.swimlaneCellClickQueue.pop(); - this.swimlaneCellClickQueue.length = 0; - this.swimlaneCellClick(latestSelectedCells); - return; - } - - const { dateFormatTz } = this.props; - - const jobIds = (selectedCells !== null && selectedCells.viewByFieldName === VIEW_BY_JOB_LABEL) - ? selectedCells.lanes - : selectedJobs.map(d => d.id); - - const bounds = timefilter.getActiveBounds(); - const timerange = getSelectionTimeRange( - selectedCells, - this.getSwimlaneBucketInterval(selectedJobs).asSeconds(), - bounds, - ); - - // Load the overall data - if the FieldFormats failed to populate - // the default formatting will be used for metric values. - Object.assign( - stateUpdate, - await this.loadOverallData( - selectedJobs, - this.getSwimlaneBucketInterval(selectedJobs), - bounds, - showOverallLoadingIndicator, - ) - ); - - const { overallSwimlaneData } = stateUpdate; - - const annotationsTableCompareArgs = { - selectedCells, - selectedJobs, - interval: this.getSwimlaneBucketInterval(selectedJobs).asSeconds(), - boundsMin: bounds.min.valueOf(), - boundsMax: bounds.max.valueOf(), - }; - - if (_.isEqual(annotationsTableCompareArgs, this.annotationsTablePreviousArgs)) { - stateUpdate.annotationsData = this.annotationsTablePreviousData; - } else { - this.annotationsTablePreviousArgs = annotationsTableCompareArgs; - stateUpdate.annotationsData = this.annotationsTablePreviousData = await loadAnnotationsTableData( - selectedCells, - selectedJobs, - this.getSwimlaneBucketInterval(selectedJobs).asSeconds(), - bounds, - ); - } - - const viewBySwimlaneOptions = getViewBySwimlaneOptions({ - currentSwimlaneViewByFieldName: swimlaneViewByFieldName, - filterActive, - filteredFields, - isAndOperator, - selectedJobs, - selectedCells - }); - - Object.assign(stateUpdate, viewBySwimlaneOptions); - if (selectedCells !== null && selectedCells.showTopFieldValues === true) { - // this.setState({ viewBySwimlaneData: getDefaultViewBySwimlaneData(), viewBySwimlaneDataLoading: true }); - // Click is in one of the cells in the Overall swimlane - reload the 'view by' swimlane - // to show the top 'view by' values for the selected time. - const topFieldValues = await this.loadViewByTopFieldValuesForSelectedTime( - timerange.earliestMs, - timerange.latestMs, - selectedJobs, - viewBySwimlaneOptions.swimlaneViewByFieldName - ); - Object.assign( - stateUpdate, - await this.loadViewBySwimlane( - topFieldValues, - overallSwimlaneData, - selectedJobs, - viewBySwimlaneOptions.swimlaneViewByFieldName, - influencersFilterQuery - ), - { viewByLoadedForTimeFormatted: formatHumanReadableDateTime(timerange.earliestMs) } - ); - } else { - Object.assign( - stateUpdate, - viewBySwimlaneOptions, - await this.loadViewBySwimlane( - [], - overallSwimlaneData, - selectedJobs, - viewBySwimlaneOptions.swimlaneViewByFieldName, - influencersFilterQuery - ), - ); - } - - const { viewBySwimlaneData } = stateUpdate; - - // do a sanity check against selectedCells. It can happen that a previously - // selected lane loaded via URL/AppState is not available anymore. - // If filter is active - selectedCell may not be available due to swimlane view by change to filter fieldName - // Ok to keep cellSelection in this case - let clearSelection = false; - if ( - selectedCells !== null && - selectedCells.type === SWIMLANE_TYPE.VIEW_BY - ) { - clearSelection = (filterActive === false) && !selectedCells.lanes.some((lane) => { - return viewBySwimlaneData.points.some((point) => { - return ( - point.laneLabel === lane && - (point.time >= selectedCells.times[0] && point.time <= selectedCells.times[1]) - ); - }); - }); - } - - if (clearSelection === true) { - this.props.appStateHandler(APP_STATE_ACTION.CLEAR_SELECTION); - Object.assign(stateUpdate, getClearedSelectedAnomaliesState()); - } - - const selectionInfluencers = getSelectionInfluencers(selectedCells, viewBySwimlaneOptions.swimlaneViewByFieldName); - - if (selectionInfluencers.length === 0) { - stateUpdate.influencers = await loadTopInfluencers(jobIds, timerange.earliestMs, timerange.latestMs, noInfluencersConfigured); - } - - if (stateUpdate.influencers !== undefined && !noInfluencersConfigured) { - for (const influencerName in stateUpdate.influencers) { - if (stateUpdate.influencers[influencerName][0] && stateUpdate.influencers[influencerName][0].influencerFieldValue) { - stateUpdate.filterPlaceHolder = - (i18n.translate( - 'xpack.ml.explorer.kueryBar.filterPlaceholder', - { - defaultMessage: - 'Filter by influencer fields… ({queryExample})', - values: { - queryExample: - `${influencerName} : ${stateUpdate.influencers[influencerName][0].influencerFieldValue}` - } - } - )); - break; - } - } - } - - const updatedAnomalyChartRecords = await loadDataForCharts( - jobIds, timerange.earliestMs, timerange.latestMs, selectionInfluencers, selectedCells, influencersFilterQuery - ); - - if ((selectionInfluencers.length > 0 || influencersFilterQuery !== undefined) && updatedAnomalyChartRecords !== undefined) { - stateUpdate.influencers = await getFilteredTopInfluencers( - jobIds, - timerange.earliestMs, - timerange.latestMs, - updatedAnomalyChartRecords, - selectionInfluencers, - noInfluencersConfigured, - influencersFilterQuery - ); - } - - stateUpdate.anomalyChartRecords = updatedAnomalyChartRecords || []; - - this.setState(stateUpdate); - - if (selectedCells !== null) { - this.updateCharts( - stateUpdate.anomalyChartRecords, timerange.earliestMs, timerange.latestMs - ); - } else { - this.updateCharts( - [], timerange.earliestMs, timerange.latestMs - ); - } - - const { tableInterval, tableSeverity } = this.props; - const anomaliesTableCompareArgs = { - selectedCells, - selectedJobs, - dateFormatTz, - interval: this.getSwimlaneBucketInterval(selectedJobs).asSeconds(), - boundsMin: bounds.min.valueOf(), - boundsMax: bounds.max.valueOf(), - swimlaneViewByFieldName: viewBySwimlaneOptions.swimlaneViewByFieldName, - tableInterval, - tableSeverity, - influencersFilterQuery - }; - - if (_.isEqual(anomaliesTableCompareArgs, this.anomaliesTablePreviousArgs)) { - this.setState(this.anomaliesTablePreviousData); - } else { - this.anomaliesTablePreviousArgs = anomaliesTableCompareArgs; - const tableData = this.anomaliesTablePreviousData = await loadAnomaliesTableData( - selectedCells, - selectedJobs, - dateFormatTz, - this.getSwimlaneBucketInterval(selectedJobs).asSeconds(), - bounds, - viewBySwimlaneOptions.swimlaneViewByFieldName, - tableInterval, - tableSeverity, - influencersFilterQuery - ); - this.setState({ tableData }); - } - } - - viewByChangeHandler = e => this.setSwimlaneViewBy(e.target.value); - setSwimlaneViewBy = (swimlaneViewByFieldName) => { - let maskAll = false; - - if (this.state.influencersFilterQuery !== undefined) { - maskAll = (swimlaneViewByFieldName === VIEW_BY_JOB_LABEL || - this.state.filteredFields.includes(swimlaneViewByFieldName) === false); - } - - this.props.appStateHandler(APP_STATE_ACTION.CLEAR_SELECTION); - this.props.appStateHandler(APP_STATE_ACTION.SAVE_SWIMLANE_VIEW_BY_FIELD_NAME, { swimlaneViewByFieldName }); - this.setState({ swimlaneViewByFieldName, maskAll }, () => { - this.updateExplorer({ - swimlaneViewByFieldName, - ...getClearedSelectedAnomaliesState(), - }, false); - }); - }; + viewByChangeHandler = e => explorerService.setViewBySwimlaneFieldName(e.target.value); isSwimlaneSelectActive = false; onSwimlaneEnterHandler = () => this.setSwimlaneSelectActive(true); @@ -948,53 +256,22 @@ export const Explorer = injectI18n(injectObservablesAsProps( } }; - // This queue tracks click events while the swimlanes are loading. - // To avoid race conditions we keep the click events selectedCells in this queue - // and trigger another event only after the current loading is done. - // The queue is necessary since a click in the overall swimlane triggers - // an update of the viewby swimlanes. If we'd just ignored click events - // during the loading, we could miss programmatically triggered events like - // those coming via AppState when a selection is part of the URL. - swimlaneCellClickQueue = []; - // Listener for click events in the swimlane to load corresponding anomaly data. - swimlaneCellClick = (swimlaneSelectedCells) => { - if (this.skipCellClicks === true) { - this.swimlaneCellClickQueue.push(swimlaneSelectedCells); - return; - } - + swimlaneCellClick = (selectedCells) => { // If selectedCells is an empty object we clear any existing selection, // otherwise we save the new selection in AppState and update the Explorer. - if (Object.keys(swimlaneSelectedCells).length === 0) { - this.props.appStateHandler(APP_STATE_ACTION.CLEAR_SELECTION); - - const stateUpdate = getClearedSelectedAnomaliesState(); - this.updateExplorer(stateUpdate, false); + if (Object.keys(selectedCells).length === 0) { + explorerService.clearSelection(); } else { - swimlaneSelectedCells.showTopFieldValues = false; - - const currentSwimlaneType = _.get(this.state, 'selectedCells.type'); - const currentShowTopFieldValues = _.get(this.state, 'selectedCells.showTopFieldValues', false); - const newSwimlaneType = _.get(swimlaneSelectedCells, 'type'); - - if ( - (currentSwimlaneType === SWIMLANE_TYPE.OVERALL && newSwimlaneType === SWIMLANE_TYPE.VIEW_BY) || - newSwimlaneType === SWIMLANE_TYPE.OVERALL || - currentShowTopFieldValues === true - ) { - swimlaneSelectedCells.showTopFieldValues = true; - } - - this.props.appStateHandler(APP_STATE_ACTION.SAVE_SELECTION, { swimlaneSelectedCells }); - this.updateExplorer({ selectedCells: swimlaneSelectedCells }, false); + explorerService.setSelectedCells(selectedCells); } } // Escape regular parens from fieldName as that portion of the query is not wrapped in double quotes // and will cause a syntax error when called with getKqlQueryValues applyFilter = (fieldName, fieldValue, action) => { + const { filterActive, indexPattern, queryString } = this.props.explorerState; + let newQueryString = ''; - const { queryString } = this.state; const operator = 'and '; const sanitizedFieldName = escapeParens(fieldName); const sanitizedFieldValue = escapeDoubleQuotes(fieldValue); @@ -1007,15 +284,15 @@ export const Explorer = injectI18n(injectObservablesAsProps( } newQueryString = `${queryString ? `${queryString} ${operator}` : ''}${sanitizedFieldName}:"${sanitizedFieldValue}"`; } else if (action === FILTER_ACTION.REMOVE) { - if (this.state.filterActive === false) { + if (filterActive === false) { return; } else { - newQueryString = removeFilterFromQueryString(this.state.queryString, sanitizedFieldName, sanitizedFieldValue); + newQueryString = removeFilterFromQueryString(queryString, sanitizedFieldName, sanitizedFieldValue); } } try { - const queryValues = getKqlQueryValues(`${newQueryString}`, this.state.indexPattern); + const queryValues = getKqlQueryValues(`${newQueryString}`, indexPattern); this.applyInfluencersFilterQuery(queryValues); } catch(e) { console.log('Invalid kuery syntax', e); // eslint-disable-line no-console @@ -1027,78 +304,22 @@ export const Explorer = injectI18n(injectObservablesAsProps( } } - applyInfluencersFilterQuery = ({ - filterQuery: influencersFilterQuery, - isAndOperator, - filteredFields, - queryString, - tableQueryString }) => { - const { selectedCells, swimlaneViewByFieldName, viewBySwimlaneOptions } = this.state; - let selectedViewByFieldName = swimlaneViewByFieldName; + applyInfluencersFilterQuery = (payload) => { + const { filterQuery: influencersFilterQuery } = payload; if (influencersFilterQuery.match_all && Object.keys(influencersFilterQuery.match_all).length === 0) { - this.props.appStateHandler(APP_STATE_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS); - this.props.appStateHandler(APP_STATE_ACTION.CLEAR_SELECTION); - const stateUpdate = { - filterActive: false, - filteredFields: [], - influencersFilterQuery: undefined, - isAndOperator: false, - maskAll: false, - queryString: '', - tableQueryString: '', - ...getClearedSelectedAnomaliesState() - }; - - this.updateExplorer(stateUpdate, false); + explorerService.clearInfluencerFilterSettings(); } else { - // if it's an AND filter set view by swimlane to job ID as the others will have no results - if (isAndOperator && selectedCells === null) { - selectedViewByFieldName = VIEW_BY_JOB_LABEL; - this.props.appStateHandler( - APP_STATE_ACTION.SAVE_SWIMLANE_VIEW_BY_FIELD_NAME, - { swimlaneViewByFieldName: selectedViewByFieldName }, - ); - } else { - // Set View by dropdown to first relevant fieldName based on incoming filter if there's no cell selection already - // or if selected cell is from overall swimlane as this won't include an additional influencer filter - for (let i = 0; i < filteredFields.length; i++) { - if (viewBySwimlaneOptions.includes(filteredFields[i]) && - ((selectedCells === null || (selectedCells && selectedCells.type === 'overall')))) { - selectedViewByFieldName = filteredFields[i]; - this.props.appStateHandler( - APP_STATE_ACTION.SAVE_SWIMLANE_VIEW_BY_FIELD_NAME, - { swimlaneViewByFieldName: selectedViewByFieldName }, - ); - break; - } - } - } - - this.props.appStateHandler(APP_STATE_ACTION.SAVE_INFLUENCER_FILTER_SETTINGS, - { influencersFilterQuery, filterActive: true, filteredFields, queryString, tableQueryString, isAndOperator }); - - this.updateExplorer({ - filterActive: true, - filteredFields, - influencersFilterQuery, - isAndOperator, - queryString, - tableQueryString, - maskAll: (selectedViewByFieldName === VIEW_BY_JOB_LABEL || - filteredFields.includes(selectedViewByFieldName) === false), - swimlaneViewByFieldName: selectedViewByFieldName - }, false); + explorerService.setInfluencerFilterSettings(payload); } } render() { const { - dateFormatTz, globalState, intl, - jobSelectService, - TimeBuckets, + jobSelectService$, + showCharts, } = this.props; const { @@ -1108,38 +329,39 @@ export const Explorer = injectI18n(injectObservablesAsProps( filterActive, filterPlaceHolder, indexPattern, - maskAll, influencers, - hasResults, + loading, + maskAll, noInfluencersConfigured, - noJobsFound, overallSwimlaneData, queryString, selectedCells, - swimlaneViewByFieldName, + selectedJobs, + swimlaneContainerWidth, tableData, tableQueryString, viewByLoadedForTimeFormatted, viewBySwimlaneData, viewBySwimlaneDataLoading, + viewBySwimlaneFieldName, viewBySwimlaneOptions, - } = this.state; - const loading = this.props.loading || this.state.loading; - - const swimlaneWidth = getSwimlaneContainerWidth(noInfluencersConfigured); + } = this.props.explorerState; const { jobIds: selectedJobIds, selectedGroups } = getSelectedJobIds(globalState); const jobSelectorProps = { - dateFormatTz, + dateFormatTz: getDateFormatTz(), globalState, - jobSelectService, + jobSelectService$, selectedJobIds, selectedGroups, }; + const noJobsFound = selectedJobs === null || selectedJobs.length === 0; + const hasResults = (overallSwimlaneData.points && overallSwimlaneData.points.length > 0); + if (loading === true) { return ( - + ; + return ; } if (noJobsFound && hasResults === false) { - return ; + return ; } const mainColumnWidthClassName = noInfluencersConfigured === true ? 'col-xs-12' : 'col-xs-10'; const mainColumnClasses = `column ${mainColumnWidthClassName}`; + const showOverallSwimlane = ( + overallSwimlaneData !== null && + overallSwimlaneData.laneLabels && + overallSwimlaneData.laneLabels.length > 0 + ); const showViewBySwimlane = ( viewBySwimlaneData !== null && viewBySwimlaneData.laneLabels && @@ -1168,7 +395,7 @@ export const Explorer = injectI18n(injectObservablesAsProps( ); return ( - +
{/* Make sure ChartTooltip is inside this plain wrapping div so positioning can be infered correctly. */} @@ -1228,21 +455,23 @@ export const Explorer = injectI18n(injectObservablesAsProps( onMouseLeave={this.onSwimlaneLeaveHandler} data-test-subj="mlAnomalyExplorerSwimlaneOverall" > - + {showOverallSwimlane && ( + + )}
{viewBySwimlaneOptions.length > 0 && ( - + <> @@ -1286,7 +515,7 @@ export const Explorer = injectI18n(injectObservablesAsProps( /> )} {filterActive === true && - swimlaneViewByFieldName === 'job ID' && ( + viewBySwimlaneFieldName === VIEW_BY_JOB_LABEL && ( {showViewBySwimlane && ( - + <>
-
+ )} {viewBySwimlaneDataLoading && ( )} - {!showViewBySwimlane && !viewBySwimlaneDataLoading && swimlaneViewByFieldName !== null && ( + {!showViewBySwimlane && !viewBySwimlaneDataLoading && viewBySwimlaneFieldName !== null && ( )} -
+ )} {annotationsData.length > 0 && ( - + <> - + )} @@ -1394,7 +623,7 @@ export const Explorer = injectI18n(injectObservablesAsProps(
- {this.props.showCharts && } + {showCharts && }
{ // the directive just ends up being empty. expect(wrapper.isEmptyRender()).toBeTruthy(); expect(wrapper.find('.content-wrapper')).toHaveLength(0); - expect(wrapper.find('.ml-loading-indicator .loading-spinner')).toHaveLength(0); + expect(wrapper.find('.ml-loading-indicator .euiLoadingChart')).toHaveLength(0); }); test('Loading status active, no chart', () => { @@ -69,7 +69,7 @@ describe('ExplorerChart', () => { ); // test if the loading indicator is shown - expect(wrapper.find('.ml-loading-indicator .loading-spinner')).toHaveLength(1); + expect(wrapper.find('.ml-loading-indicator .euiLoadingChart')).toHaveLength(1); }); // For the following tests the directive needs to be rendered in the actual DOM, @@ -97,7 +97,7 @@ describe('ExplorerChart', () => { const wrapper = init(mockChartData); // the loading indicator should not be shown - expect(wrapper.find('.ml-loading-indicator .loading-spinner')).toHaveLength(0); + expect(wrapper.find('.ml-loading-indicator .euiLoadingChart')).toHaveLength(0); // test if all expected elements are present // need to use getDOMNode() because the chart is not rendered via react itself diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js index 83d4fda0858a2..e3d7dcca490d6 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js @@ -56,7 +56,7 @@ describe('ExplorerChart', () => { // the directive just ends up being empty. expect(wrapper.isEmptyRender()).toBeTruthy(); expect(wrapper.find('.content-wrapper')).toHaveLength(0); - expect(wrapper.find('.ml-loading-indicator .loading-spinner')).toHaveLength(0); + expect(wrapper.find('.ml-loading-indicator .euiLoadingChart')).toHaveLength(0); }); test('Loading status active, no chart', () => { @@ -69,7 +69,7 @@ describe('ExplorerChart', () => { ); // test if the loading indicator is shown - expect(wrapper.find('.ml-loading-indicator .loading-spinner')).toHaveLength(1); + expect(wrapper.find('.ml-loading-indicator .euiLoadingChart')).toHaveLength(1); }); // For the following tests the directive needs to be rendered in the actual DOM, @@ -97,7 +97,7 @@ describe('ExplorerChart', () => { const wrapper = init(mockChartData); // the loading indicator should not be shown - expect(wrapper.find('.ml-loading-indicator .loading-spinner')).toHaveLength(0); + expect(wrapper.find('.ml-loading-indicator .euiLoadingChart')).toHaveLength(0); // test if all expected elements are present // need to use getDOMNode() because the chart is not rendered via react itself diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.d.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.d.ts new file mode 100644 index 0000000000000..ccd52a26f2abc --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.d.ts @@ -0,0 +1,18 @@ +/* + * 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 declare interface ExplorerChartsData { + chartsPerRow: number; + seriesToPlot: any[]; + tooManyBuckets: boolean; + timeFieldName: string; +} + +export declare const getDefaultChartsData: () => ExplorerChartsData; + +export declare const explorerChartsContainerServiceFactory: ( + callback: (data: ExplorerChartsData) => void +) => (anomalyRecords: any[], earliestMs: number, latestMs: number) => void; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index 01afd9ffb602f..4b8c5030634f0 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -48,10 +48,7 @@ export function explorerChartsContainerServiceFactory(callback) { callback(getDefaultChartsData()); - let requestCount = 0; const anomalyDataChange = function (anomalyRecords, earliestMs, latestMs) { - const newRequestCount = ++requestCount; - requestCount = newRequestCount; const data = getDefaultChartsData(); @@ -380,11 +377,6 @@ export function explorerChartsContainerServiceFactory(callback) { Promise.all(seriesPromises) .then(response => { - // TODO: Add test to prevent this regression. - // Ignore this response if it's returned by an out of date promise - if (newRequestCount < requestCount) { - return; - } // calculate an overall min/max for all series const processedData = response.map(processChartData); const allDataPoints = _.reduce(processedData, (datapoints, series) => { diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_constants.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_constants.ts similarity index 54% rename from x-pack/legacy/plugins/ml/public/application/explorer/explorer_constants.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_constants.ts index 27073ca250d11..66cd98f7ebe29 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_constants.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_constants.ts @@ -13,16 +13,32 @@ import { i18n } from '@kbn/i18n'; export const DRAG_SELECT_ACTION = { NEW_SELECTION: 'newSelection', ELEMENT_SELECT: 'elementSelect', - DRAG_START: 'dragStart' + DRAG_START: 'dragStart', }; export const EXPLORER_ACTION = { - IDLE: 'idle', + APP_STATE_SET: 'appStateSet', + APP_STATE_CLEAR_INFLUENCER_FILTER_SETTINGS: 'appStateClearInfluencerFilterSettings', + APP_STATE_CLEAR_SELECTION: 'appStateClearSelection', + APP_STATE_SAVE_SELECTION: 'appStateSaveSelection', + APP_STATE_SAVE_VIEW_BY_SWIMLANE_FIELD_NAME: 'appStateSaveViewBySwimlaneFieldName', + APP_STATE_SAVE_INFLUENCER_FILTER_SETTINGS: 'appStateSaveInfluencerFilterSettings', + CLEAR_INFLUENCER_FILTER_SETTINGS: 'clearInfluencerFilterSettings', + CLEAR_JOBS: 'clearJobs', + CLEAR_SELECTION: 'clearSelection', INITIALIZE: 'initialize', JOB_SELECTION_CHANGE: 'jobSelectionChange', LOAD_JOBS: 'loadJobs', - REDRAW: 'redraw', - RELOAD: 'reload', + RESET: 'reset', + SET_BOUNDS: 'setBounds', + SET_CHARTS: 'setCharts', + SET_INFLUENCER_FILTER_SETTINGS: 'setInfluencerFilterSettings', + SET_SELECTED_CELLS: 'setSelectedCells', + SET_STATE: 'setState', + SET_SWIMLANE_CONTAINER_WIDTH: 'setSwimlaneContainerWidth', + SET_SWIMLANE_LIMIT: 'setSwimlaneLimit', + SET_VIEW_BY_SWIMLANE_FIELD_NAME: 'setViewBySwimlaneFieldName', + SET_VIEW_BY_SWIMLANE_LOADING: 'setViewBySwimlaneLoading', }; export const FILTER_ACTION = { @@ -30,17 +46,9 @@ export const FILTER_ACTION = { REMOVE: '-', }; -export const APP_STATE_ACTION = { - CLEAR_INFLUENCER_FILTER_SETTINGS: 'clearInfluencerFilterSettings', - CLEAR_SELECTION: 'clearSelection', - SAVE_SELECTION: 'saveSelection', - SAVE_SWIMLANE_VIEW_BY_FIELD_NAME: 'saveSwimlaneViewByFieldName', - SAVE_INFLUENCER_FILTER_SETTINGS: 'saveInfluencerFilterSettings' -}; - export const SWIMLANE_TYPE = { OVERALL: 'overall', - VIEW_BY: 'viewBy' + VIEW_BY: 'viewBy', }; export const CHART_TYPE = { @@ -53,4 +61,6 @@ export const MAX_CATEGORY_EXAMPLES = 10; export const MAX_INFLUENCER_FIELD_VALUES = 10; export const MAX_INFLUENCER_FIELD_NAMES = 50; -export const VIEW_BY_JOB_LABEL = i18n.translate('xpack.ml.explorer.jobIdLabel', { defaultMessage: 'job ID' }); +export const VIEW_BY_JOB_LABEL = i18n.translate('xpack.ml.explorer.jobIdLabel', { + defaultMessage: 'job ID', +}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_controller.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_controller.js deleted file mode 100644 index c33b86bacf942..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_controller.js +++ /dev/null @@ -1,289 +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. - */ - - -/* - * Angular controller for the Machine Learning Explorer dashboard. The controller makes - * multiple queries to Elasticsearch to obtain the data to populate all the components - * in the view. - */ - -import $ from 'jquery'; -import { Subscription } from 'rxjs'; - -import '../components/controls'; - -import uiRoutes from 'ui/routes'; -import { - createJobs, -} from './explorer_utils'; -import { getAnomalyExplorerBreadcrumbs } from './breadcrumbs'; -import { checkFullLicense } from '../license/check_license'; -import { checkGetJobsPrivilege } from '../privilege/check_privilege'; -import { loadIndexPatterns } from '../util/index_utils'; -import { TimeBuckets } from '../util/time_buckets'; -import { explorer$ } from './explorer_dashboard_service'; -import { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service'; -import { mlFieldFormatService } from '../services/field_format_service'; -import { mlJobService } from '../services/job_service'; -import { getSelectedJobIds, jobSelectServiceFactory } from '../components/job_selector/job_select_service_utils'; -import { timefilter } from 'ui/timefilter'; - -import { interval$ } from '../components/controls/select_interval'; -import { severity$ } from '../components/controls/select_severity'; -import { showCharts$ } from '../components/controls/checkbox_showcharts'; -import { subscribeAppStateToObservable } from '../util/app_state_utils'; - -import { APP_STATE_ACTION, EXPLORER_ACTION } from './explorer_constants'; - -const template = ``; - -uiRoutes - .when('/explorer/?', { - controller: 'MlExplorerController', - template, - k7Breadcrumbs: getAnomalyExplorerBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - indexPatterns: loadIndexPatterns, - jobs: mlJobService.loadJobsWrapper - }, - }); - -import { uiModules } from 'ui/modules'; - -const module = uiModules.get('apps/ml'); - -module.controller('MlExplorerController', function ( - $scope, - $timeout, - $rootScope, - AppState, - globalState, -) { - const { jobSelectService, unsubscribeFromGlobalState } = jobSelectServiceFactory(globalState); - const subscriptions = new Subscription(); - - // $scope should only contain what's actually still necessary for the angular part. - // For the moment that's the job selector and the (hidden) filter bar. - $scope.jobs = []; - timefilter.enableTimeRangeSelector(); - timefilter.enableAutoRefreshSelector(); - - $scope.TimeBuckets = TimeBuckets; - - let resizeTimeout = null; - - function jobSelectionUpdate(action, { - fullJobs, - filterData, - selectedCells, - selectedJobIds, - swimlaneViewByFieldName - }) { - const jobs = createJobs(fullJobs).map((job) => { - job.selected = selectedJobIds.some((id) => job.id === id); - return job; - }); - - const selectedJobs = jobs.filter(job => job.selected); - - function fieldFormatServiceCallback() { - $scope.jobs = jobs; - $scope.$applyAsync(); - - const noJobsFound = ($scope.jobs.length === 0); - - explorer$.next({ - action, - payload: { - loading: false, - noJobsFound, - selectedCells, - selectedJobs, - swimlaneViewByFieldName, - filterData - } - }); - $scope.jobSelectionUpdateInProgress = false; - $scope.$applyAsync(); - } - - // Populate the map of jobs / detectors / field formatters for the selected IDs. - mlFieldFormatService.populateFormats(selectedJobIds) - .catch((err) => { - console.log('Error populating field formats:', err); - }) - .then(() => { - fieldFormatServiceCallback(); - }); - } - - // Initialize the AppState in which to store swimlane settings. - // AppState is used to store state in the URL. - $scope.appState = new AppState({ - mlExplorerSwimlane: {}, - mlExplorerFilter: {} - }); - - // Load the job info needed by the dashboard, then do the first load. - // Calling loadJobs() ensures the full datafeed config is available for building the charts. - // Using this listener ensures the jobs will only be loaded and passed on after - // and have been initialized. - function loadJobsListener({ action }) { - if (action === EXPLORER_ACTION.LOAD_JOBS) { - // Jobs load via route resolver - if (mlJobService.jobs.length > 0) { - // Select any jobs set in the global state (i.e. passed in the URL). - const { jobIds: selectedJobIds } = getSelectedJobIds(globalState); - let selectedCells; - let filterData = {}; - - // keep swimlane selection, restore selectedCells from AppState - if ($scope.appState.mlExplorerSwimlane.selectedType !== undefined) { - selectedCells = { - type: $scope.appState.mlExplorerSwimlane.selectedType, - lanes: $scope.appState.mlExplorerSwimlane.selectedLanes, - times: $scope.appState.mlExplorerSwimlane.selectedTimes, - showTopFieldValues: $scope.appState.mlExplorerSwimlane.showTopFieldValues, - viewByFieldName: $scope.appState.mlExplorerSwimlane.viewByFieldName, - }; - } - - // keep influencers filter selection, restore from AppState - if ($scope.appState.mlExplorerFilter.influencersFilterQuery !== undefined) { - filterData = { - influencersFilterQuery: $scope.appState.mlExplorerFilter.influencersFilterQuery, - filterActive: $scope.appState.mlExplorerFilter.filterActive, - filteredFields: $scope.appState.mlExplorerFilter.filteredFields, - queryString: $scope.appState.mlExplorerFilter.queryString, - }; - } - - jobSelectionUpdate(EXPLORER_ACTION.INITIALIZE, { - filterData, - fullJobs: mlJobService.jobs, - selectedCells, - selectedJobIds, - swimlaneViewByFieldName: $scope.appState.mlExplorerSwimlane.viewByFieldName, - }); - - subscriptions.add(jobSelectService.subscribe(({ selection }) => { - if (selection !== undefined) { - $scope.jobSelectionUpdateInProgress = true; - jobSelectionUpdate(EXPLORER_ACTION.JOB_SELECTION_CHANGE, { fullJobs: mlJobService.jobs, selectedJobIds: selection }); - } - })); - - } else { - explorer$.next({ - action: EXPLORER_ACTION.RELOAD, - payload: { - loading: false, - noJobsFound: true, - } - }); - } - } - } - - subscriptions.add(explorer$.subscribe(loadJobsListener)); - - // Listen for changes to job selection. - $scope.jobSelectionUpdateInProgress = false; - - subscriptions.add(mlTimefilterRefresh$.subscribe(() => { - if ($scope.jobSelectionUpdateInProgress === false) { - explorer$.next({ action: EXPLORER_ACTION.REDRAW }); - } - })); - - // Refresh all the data when the time range is altered. - subscriptions.add(timefilter.getFetch$().subscribe(() => { - if ($scope.jobSelectionUpdateInProgress === false) { - explorer$.next({ action: EXPLORER_ACTION.RELOAD }); - } - })); - - subscriptions.add(subscribeAppStateToObservable(AppState, 'mlShowCharts', showCharts$, () => $rootScope.$applyAsync())); - subscriptions.add(subscribeAppStateToObservable(AppState, 'mlSelectInterval', interval$, () => $rootScope.$applyAsync())); - subscriptions.add(subscribeAppStateToObservable(AppState, 'mlSelectSeverity', severity$, () => $rootScope.$applyAsync())); - - // Redraw the swimlane when the window resizes or the global nav is toggled. - function jqueryRedrawOnResize() { - if (resizeTimeout !== null) { - $timeout.cancel(resizeTimeout); - } - // Only redraw 100ms after last resize event. - resizeTimeout = $timeout(redrawOnResize, 100); - } - - $(window).resize(jqueryRedrawOnResize); - - const navListener = $scope.$on('globalNav:update', () => { - // Run in timeout so that content pane has resized after global nav has updated. - $timeout(() => { - redrawOnResize(); - }, 300); - }); - - function redrawOnResize() { - if ($scope.jobSelectionUpdateInProgress === false) { - explorer$.next({ action: EXPLORER_ACTION.REDRAW }); - } - } - - $scope.appStateHandler = ((action, payload) => { - $scope.appState.fetch(); - - if (action === APP_STATE_ACTION.CLEAR_SELECTION) { - delete $scope.appState.mlExplorerSwimlane.selectedType; - delete $scope.appState.mlExplorerSwimlane.selectedLanes; - delete $scope.appState.mlExplorerSwimlane.selectedTimes; - delete $scope.appState.mlExplorerSwimlane.showTopFieldValues; - } - - if (action === APP_STATE_ACTION.SAVE_SELECTION) { - const swimlaneSelectedCells = payload.swimlaneSelectedCells; - $scope.appState.mlExplorerSwimlane.selectedType = swimlaneSelectedCells.type; - $scope.appState.mlExplorerSwimlane.selectedLanes = swimlaneSelectedCells.lanes; - $scope.appState.mlExplorerSwimlane.selectedTimes = swimlaneSelectedCells.times; - $scope.appState.mlExplorerSwimlane.showTopFieldValues = swimlaneSelectedCells.showTopFieldValues; - $scope.appState.mlExplorerSwimlane.viewByFieldName = swimlaneSelectedCells.viewByFieldName; - - } - - if (action === APP_STATE_ACTION.SAVE_SWIMLANE_VIEW_BY_FIELD_NAME) { - $scope.appState.mlExplorerSwimlane.viewByFieldName = payload.swimlaneViewByFieldName; - } - - if (action === APP_STATE_ACTION.SAVE_INFLUENCER_FILTER_SETTINGS) { - $scope.appState.mlExplorerFilter.influencersFilterQuery = payload.influencersFilterQuery; - $scope.appState.mlExplorerFilter.filterActive = payload.filterActive; - $scope.appState.mlExplorerFilter.filteredFields = payload.filteredFields; - $scope.appState.mlExplorerFilter.queryString = payload.queryString; - } - - if (action === APP_STATE_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS) { - delete $scope.appState.mlExplorerFilter.influencersFilterQuery; - delete $scope.appState.mlExplorerFilter.filterActive; - delete $scope.appState.mlExplorerFilter.filteredFields; - delete $scope.appState.mlExplorerFilter.queryString; - } - - $scope.appState.save(); - $scope.$applyAsync(); - }); - - $scope.$on('$destroy', () => { - subscriptions.unsubscribe(); - $(window).off('resize', jqueryRedrawOnResize); - // Cancel listening for updates to the global nav state. - navListener(); - unsubscribeFromGlobalState(); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_dashboard_service.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_dashboard_service.js deleted file mode 100644 index a017784292337..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_dashboard_service.js +++ /dev/null @@ -1,19 +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. - */ - - - -/* - * Service for firing and registering for events across the different - * components in the Explorer dashboard. - */ - -import { Subject } from 'rxjs'; - -export const ALLOW_CELL_RANGE_SELECTION = true; - -export const dragSelect$ = new Subject(); -export const explorer$ = new Subject(); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_dashboard_service.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_dashboard_service.ts new file mode 100644 index 0000000000000..713857835b3b9 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_dashboard_service.ts @@ -0,0 +1,179 @@ +/* + * 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. + */ + +/* + * Service for firing and registering for events across the different + * components in the Explorer dashboard. + */ + +import { isEqual, pick } from 'lodash'; + +import { from, isObservable, BehaviorSubject, Observable, Subject } from 'rxjs'; +import { distinctUntilChanged, flatMap, map, pairwise, scan } from 'rxjs/operators'; + +import { DeepPartial } from '../../../common/types/common'; + +import { jobSelectionActionCreator, loadExplorerData } from './actions'; +import { ExplorerChartsData } from './explorer_charts/explorer_charts_container_service'; +import { EXPLORER_ACTION } from './explorer_constants'; +import { RestoredAppState, SelectedCells, TimeRangeBounds } from './explorer_utils'; +import { + explorerReducer, + getExplorerDefaultState, + ExplorerAppState, + ExplorerState, +} from './reducers'; + +export const ALLOW_CELL_RANGE_SELECTION = true; + +export const dragSelect$ = new Subject(); + +type ExplorerAction = Action | Observable; +const explorerAction$ = new BehaviorSubject({ type: EXPLORER_ACTION.RESET }); + +export type ActionPayload = any; + +export interface Action { + type: string; + payload?: ActionPayload; +} + +const explorerFilteredAction$ = explorerAction$.pipe( + // consider observables as side-effects + flatMap((action: ExplorerAction) => + isObservable(action) ? action : (from([action]) as Observable) + ), + distinctUntilChanged(isEqual) +); + +// applies action and returns state +const explorerState$: Observable = explorerFilteredAction$.pipe( + scan(explorerReducer, getExplorerDefaultState()), + pairwise(), + map(([prev, curr]) => { + if ( + curr.selectedJobs !== null && + curr.bounds !== undefined && + !isEqual(getCompareState(prev), getCompareState(curr)) + ) { + explorerAction$.next(loadExplorerData(curr).pipe(map(d => setStateActionCreator(d)))); + } + return curr; + }) +); + +const explorerAppState$: Observable = explorerState$.pipe( + map((state: ExplorerState) => state.appState), + distinctUntilChanged(isEqual) +); + +function getCompareState(state: ExplorerState) { + return pick(state, [ + 'bounds', + 'filterActive', + 'filteredFields', + 'influencersFilterQuery', + 'isAndOperator', + 'noInfluencersConfigured', + 'selectedCells', + 'selectedJobs', + 'swimlaneContainerWidth', + 'swimlaneLimit', + 'tableInterval', + 'tableSeverity', + 'viewBySwimlaneFieldName', + ]); +} + +export const setStateActionCreator = (payload: DeepPartial) => ({ + type: EXPLORER_ACTION.SET_STATE, + payload, +}); + +interface AppStateSelection { + type: string; + lanes: string[]; + times: number[]; + showTopFieldValues: boolean; + viewByFieldName: string; +} + +// Export observable state and action dispatchers as service +export const explorerService = { + appState$: explorerAppState$, + state$: explorerState$, + appStateClearSelection: () => { + explorerAction$.next({ type: EXPLORER_ACTION.APP_STATE_CLEAR_SELECTION }); + }, + appStateSaveSelection: (payload: AppStateSelection) => { + explorerAction$.next({ type: EXPLORER_ACTION.APP_STATE_SAVE_SELECTION, payload }); + }, + clearInfluencerFilterSettings: () => { + explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS }); + }, + clearJobs: () => { + explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_JOBS }); + }, + clearSelection: () => { + explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_SELECTION }); + }, + updateJobSelection: (selectedJobIds: string[], restoredAppState: RestoredAppState) => { + explorerAction$.next( + jobSelectionActionCreator( + EXPLORER_ACTION.JOB_SELECTION_CHANGE, + selectedJobIds, + restoredAppState + ) + ); + }, + initialize: (selectedJobIds: string[], restoredAppState: RestoredAppState) => { + explorerAction$.next( + jobSelectionActionCreator(EXPLORER_ACTION.INITIALIZE, selectedJobIds, restoredAppState) + ); + }, + reset: () => { + explorerAction$.next({ type: EXPLORER_ACTION.RESET }); + }, + setAppState: (payload: DeepPartial) => { + explorerAction$.next({ type: EXPLORER_ACTION.APP_STATE_SET, payload }); + }, + setBounds: (payload: TimeRangeBounds) => { + explorerAction$.next({ type: EXPLORER_ACTION.SET_BOUNDS, payload }); + }, + setCharts: (payload: ExplorerChartsData) => { + explorerAction$.next({ type: EXPLORER_ACTION.SET_CHARTS, payload }); + }, + setInfluencerFilterSettings: (payload: any) => { + explorerAction$.next({ + type: EXPLORER_ACTION.SET_INFLUENCER_FILTER_SETTINGS, + payload, + }); + }, + setSelectedCells: (payload: SelectedCells) => { + explorerAction$.next({ + type: EXPLORER_ACTION.SET_SELECTED_CELLS, + payload, + }); + }, + setState: (payload: DeepPartial) => { + explorerAction$.next(setStateActionCreator(payload)); + }, + setSwimlaneContainerWidth: (payload: number) => { + explorerAction$.next({ + type: EXPLORER_ACTION.SET_SWIMLANE_CONTAINER_WIDTH, + payload, + }); + }, + setSwimlaneLimit: (payload: number) => { + explorerAction$.next({ type: EXPLORER_ACTION.SET_SWIMLANE_LIMIT, payload }); + }, + setViewBySwimlaneFieldName: (payload: string) => { + explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_FIELD_NAME, payload }); + }, + setViewBySwimlaneLoading: (payload: any) => { + explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_LOADING, payload }); + }, +}; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_directive.tsx b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_directive.tsx new file mode 100644 index 0000000000000..b5d65fbf937e4 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_directive.tsx @@ -0,0 +1,110 @@ +/* + * 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. + */ + +/* + * AngularJS directive wrapper for rendering Anomaly Explorer's React component. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; + +import { Subscription } from 'rxjs'; + +import { IRootElementService, IRootScopeService, IScope } from 'angular'; + +// @ts-ignore +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml'); + +import { I18nContext } from 'ui/i18n'; +import { State } from 'ui/state_management/state'; +import { AppState as IAppState, AppStateClass } from 'ui/state_management/app_state'; + +import { jobSelectServiceFactory } from '../components/job_selector/job_select_service_utils'; + +import { interval$ } from '../components/controls/select_interval'; +import { severity$ } from '../components/controls/select_severity'; +import { showCharts$ } from '../components/controls/checkbox_showcharts'; +import { subscribeAppStateToObservable } from '../util/app_state_utils'; + +import { Explorer } from './explorer'; +import { explorerService } from './explorer_dashboard_service'; +import { getExplorerDefaultAppState, ExplorerAppState } from './reducers'; + +interface ExplorerScope extends IScope { + appState: IAppState; +} + +module.directive('mlAnomalyExplorer', function( + globalState: State, + $rootScope: IRootScopeService, + AppState: AppStateClass +) { + function link($scope: ExplorerScope, element: IRootElementService) { + const subscriptions = new Subscription(); + + const { jobSelectService$, unsubscribeFromGlobalState } = jobSelectServiceFactory(globalState); + + ReactDOM.render( + + + , + element[0] + ); + + // Initialize the AppState in which to store swimlane and filter settings. + // AppState is used to store state in the URL. + $scope.appState = new AppState(getExplorerDefaultAppState()); + const { mlExplorerFilter, mlExplorerSwimlane } = $scope.appState; + + // Pass the current URL AppState on to anomaly explorer's reactive state. + // After this hand-off, the appState stored in explorerState$ is the single + // source of truth. + explorerService.setAppState({ mlExplorerSwimlane, mlExplorerFilter }); + + // Now that appState in explorerState$ is the single source of truth, + // subscribe to it and update the actual URL appState on changes. + subscriptions.add( + explorerService.appState$.subscribe((appState: ExplorerAppState) => { + $scope.appState.fetch(); + $scope.appState.mlExplorerFilter = appState.mlExplorerFilter; + $scope.appState.mlExplorerSwimlane = appState.mlExplorerSwimlane; + $scope.appState.save(); + $scope.$applyAsync(); + }) + ); + + subscriptions.add( + subscribeAppStateToObservable(AppState, 'mlShowCharts', showCharts$, () => + $rootScope.$applyAsync() + ) + ); + subscriptions.add( + subscribeAppStateToObservable(AppState, 'mlSelectInterval', interval$, () => + $rootScope.$applyAsync() + ) + ); + subscriptions.add( + subscribeAppStateToObservable(AppState, 'mlSelectSeverity', severity$, () => + $rootScope.$applyAsync() + ) + ); + + element.on('$destroy', () => { + ReactDOM.unmountComponentAtNode(element[0]); + $scope.$destroy(); + subscriptions.unsubscribe(); + unsubscribeFromGlobalState(); + }); + } + + return { link }; +}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_react_wrapper_directive.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_react_wrapper_directive.js deleted file mode 100644 index 40213a0649667..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_react_wrapper_directive.js +++ /dev/null @@ -1,62 +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. - */ - -/* - * AngularJS directive wrapper for rendering Anomaly Explorer's React component. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -import moment from 'moment-timezone'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -import { I18nContext } from 'ui/i18n'; - -import { jobSelectServiceFactory } from '../components/job_selector/job_select_service_utils'; - -import { Explorer } from './explorer'; -import { EXPLORER_ACTION } from './explorer_constants'; -import { explorer$ } from './explorer_dashboard_service'; - -module.directive('mlExplorerReactWrapper', function (config, globalState) { - function link(scope, element) { - const { jobSelectService, unsubscribeFromGlobalState } = jobSelectServiceFactory(globalState); - // Pass the timezone to the server for use when aggregating anomalies (by day / hour) for the table. - const tzConfig = config.get('dateFormat:tz'); - const dateFormatTz = (tzConfig !== 'Browser') ? tzConfig : moment.tz.guess(); - - ReactDOM.render( - - - , - element[0] - ); - - explorer$.next({ action: EXPLORER_ACTION.LOAD_JOBS }); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - unsubscribeFromGlobalState(); - }); - } - - return { - scope: false, - link, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_route.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_route.ts new file mode 100644 index 0000000000000..a061176a5ef5b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_route.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uiRoutes from 'ui/routes'; + +import '../components/controls'; + +import { checkFullLicense } from '../license/check_license'; +import { checkGetJobsPrivilege } from '../privilege/check_privilege'; +import { mlJobService } from '../services/job_service'; +import { loadIndexPatterns } from '../util/index_utils'; + +import { getAnomalyExplorerBreadcrumbs } from './breadcrumbs'; + +uiRoutes.when('/explorer/?', { + template: ``, + k7Breadcrumbs: getAnomalyExplorerBreadcrumbs, + resolve: { + CheckLicense: checkFullLicense, + privileges: checkGetJobsPrivilege, + indexPatterns: loadIndexPatterns, + jobs: mlJobService.loadJobsWrapper, + }, +}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.d.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.d.ts new file mode 100644 index 0000000000000..d7873e6d52d78 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.d.ts @@ -0,0 +1,202 @@ +/* + * 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 { Moment } from 'moment'; + +import { CombinedJob } from '../jobs/new_job/common/job_creator/configs'; + +import { TimeBucketsInterval } from '../util/time_buckets'; + +interface ClearedSelectedAnomaliesState { + anomalyChartRecords: []; + selectedCells: null; + viewByLoadedForTimeFormatted: null; +} + +export declare const getClearedSelectedAnomaliesState: () => ClearedSelectedAnomaliesState; + +export declare interface SwimlaneData { + fieldName: string; + laneLabels: string[]; + points: any[]; + interval: number; +} + +export declare interface OverallSwimlaneData extends SwimlaneData { + earliest: number; + latest: number; +} + +export declare const getDateFormatTz: () => any; + +export declare const getDefaultSwimlaneData: () => SwimlaneData; + +export declare const getInfluencers: (selectedJobs: any[]) => string[]; + +export declare const getSelectionInfluencers: ( + selectedCells: SelectedCells, + fieldName: string +) => any[]; + +interface SelectionTimeRange { + earliestMs: number; + latestMs: number; +} + +export declare const getSelectionTimeRange: ( + selectedCells: SelectedCells, + interval: number, + bounds: TimeRangeBounds +) => SelectionTimeRange; + +export declare const getSwimlaneBucketInterval: ( + selectedJobs: ExplorerJob[], + swimlaneContainerWidth: number +) => any; + +interface ViewBySwimlaneOptionsArgs { + currentViewBySwimlaneFieldName: string | undefined; + filterActive: boolean; + filteredFields: any[]; + isAndOperator: boolean; + selectedCells: SelectedCells; + selectedJobs: ExplorerJob[]; +} + +interface ViewBySwimlaneOptions { + viewBySwimlaneFieldName: string; + viewBySwimlaneOptions: string[]; +} + +export declare const getViewBySwimlaneOptions: ( + arg: ViewBySwimlaneOptionsArgs +) => ViewBySwimlaneOptions; + +export declare interface ExplorerJob { + id: string; + selected: boolean; + bucketSpanSeconds: number; +} + +export declare const createJobs: (jobs: CombinedJob[]) => ExplorerJob[]; + +export declare interface TimeRangeBounds { + min: Moment | undefined; + max: Moment | undefined; +} + +declare interface SwimlaneBounds { + earliest: number; + latest: number; +} + +export declare const loadAnnotationsTableData: ( + selectedCells: SelectedCells, + selectedJobs: ExplorerJob[], + interval: number, + bounds: TimeRangeBounds +) => Promise; + +export declare interface AnomaliesTableData { + anomalies: any[]; + interval: number; + examplesByJobId: string[]; + showViewSeriesLink: boolean; + jobIds: string[]; +} + +export declare const loadAnomaliesTableData: ( + selectedCells: SelectedCells, + selectedJobs: ExplorerJob[], + dateFormatTz: any, + interval: number, + bounds: TimeRangeBounds, + fieldName: string, + tableInterval: string, + tableSeverity: number, + influencersFilterQuery: any +) => Promise; + +export declare const loadDataForCharts: ( + jobIds: string[], + earliestMs: number, + latestMs: number, + influencers: any[], + selectedCells: SelectedCells, + influencersFilterQuery: any +) => Promise; + +export declare const loadFilteredTopInfluencers: ( + jobIds: string[], + earliestMs: number, + latestMs: number, + records: any[], + influencers: any[], + noInfluencersConfigured: boolean, + influencersFilterQuery: any +) => Promise; + +export declare const loadTopInfluencers: ( + selectedJobIds: string[], + earliestMs: number, + latestMs: number, + influencers: any[], + noInfluencersConfigured?: boolean, + influencersFilterQuery?: any +) => Promise; + +declare interface LoadOverallDataResponse { + loading: boolean; + overallSwimlaneData: OverallSwimlaneData; +} + +export declare const loadOverallData: ( + selectedJobs: ExplorerJob[], + interval: TimeBucketsInterval, + bounds: TimeRangeBounds +) => Promise; + +export declare const loadViewBySwimlane: ( + fieldValues: string[], + bounds: SwimlaneBounds, + selectedJobs: ExplorerJob[], + viewBySwimlaneFieldName: string, + swimlaneLimit: number, + influencersFilterQuery: any, + noInfluencersConfigured: boolean +) => Promise; + +export declare const loadViewByTopFieldValuesForSelectedTime: ( + earliestMs: number, + latestMs: number, + selectedJobs: ExplorerJob[], + viewBySwimlaneFieldName: string, + swimlaneLimit: number, + noInfluencersConfigured: boolean +) => Promise; + +declare interface FilterData { + influencersFilterQuery: any; + filterActive: boolean; + filteredFields: string[]; + queryString: string; +} + +declare interface SelectedCells { + type: string; + lanes: string[]; + times: number[]; + showTopFieldValues: boolean; + viewByFieldName: string; +} + +export declare interface RestoredAppState { + selectedCells?: SelectedCells; + filterData: {} | FilterData; + viewBySwimlaneFieldName: string; +} + +export declare const restoreAppState: (appState: any) => RestoredAppState; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js index 5ca8681d16749..38b088eed9b81 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js @@ -9,13 +9,25 @@ */ import { chain, each, get, union, uniq } from 'lodash'; +import moment from 'moment-timezone'; +import { i18n } from '@kbn/i18n'; +import chrome from 'ui/chrome'; + +import { npStart } from 'ui/new_platform'; +import { timefilter } from 'ui/timefilter'; + +import { + ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, + ANOMALIES_TABLE_DEFAULT_QUERY_SIZE +} from '../../../common/constants/search'; import { getEntityFieldList } from '../../../common/util/anomaly_utils'; import { isSourceDataChartableForDetector, isModelPlotEnabled } from '../../../common/util/job_utils'; import { parseInterval } from '../../../common/util/parse_interval'; import { ml } from '../services/ml_api_service'; import { mlJobService } from '../services/job_service'; import { mlResultsService } from '../services/results_service'; +import { getBoundsRoundedToInterval, TimeBuckets } from '../util/time_buckets'; import { MAX_CATEGORY_EXAMPLES, @@ -23,13 +35,8 @@ import { SWIMLANE_TYPE, VIEW_BY_JOB_LABEL, } from './explorer_constants'; -import { - ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, - ANOMALIES_TABLE_DEFAULT_QUERY_SIZE -} from '../../../common/constants/search'; +import { getSwimlaneContainerWidth } from './legacy_utils'; -import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); // create new job objects based on standard job config objects @@ -49,7 +56,7 @@ export function getClearedSelectedAnomaliesState() { }; } -export function getDefaultViewBySwimlaneData() { +export function getDefaultSwimlaneData() { return { fieldName: '', laneLabels: [], @@ -58,7 +65,7 @@ export function getDefaultViewBySwimlaneData() { }; } -export async function getFilteredTopInfluencers( +export async function loadFilteredTopInfluencers( jobIds, earliestMs, latestMs, @@ -131,6 +138,14 @@ export function getInfluencers(selectedJobs = []) { return influencers; } +export function getDateFormatTz() { + const config = npStart.core.uiSettings; + // Pass the timezone to the server for use when aggregating anomalies (by day / hour) for the table. + const tzConfig = config.get('dateFormat:tz'); + const dateFormatTz = (tzConfig !== 'Browser') ? tzConfig : moment.tz.guess(); + return dateFormatTz; +} + export function getFieldsByJob() { return mlJobService.jobs.reduce((reducedFieldsByJob, job) => { // Add the list of distinct by, over, partition and influencer fields for each job. @@ -193,9 +208,89 @@ export function getSelectionInfluencers(selectedCells, fieldName) { return []; } -// Obtain the list of 'View by' fields per job and swimlaneViewByFieldName +export function getSwimlaneBucketInterval(selectedJobs, swimlaneContainerWidth) { + // Bucketing interval should be the maximum of the chart related interval (i.e. time range related) + // and the max bucket span for the jobs shown in the chart. + const bounds = timefilter.getActiveBounds(); + const buckets = new TimeBuckets(); + buckets.setInterval('auto'); + buckets.setBounds(bounds); + + const intervalSeconds = buckets.getInterval().asSeconds(); + + // if the swimlane cell widths are too small they will not be visible + // calculate how many buckets will be drawn before the swimlanes are actually rendered + // and increase the interval to widen the cells if they're going to be smaller than 8px + // this has to be done at this stage so all searches use the same interval + const timerangeSeconds = (bounds.max.valueOf() - bounds.min.valueOf()) / 1000; + const numBuckets = parseInt(timerangeSeconds / intervalSeconds); + const cellWidth = Math.floor(swimlaneContainerWidth / numBuckets * 100) / 100; + + // if the cell width is going to be less than 8px, double the interval + if (cellWidth < 8) { + buckets.setInterval((intervalSeconds * 2) + 's'); + } + + const maxBucketSpanSeconds = selectedJobs.reduce((memo, job) => Math.max(memo, job.bucketSpanSeconds), 0); + if (maxBucketSpanSeconds > intervalSeconds) { + buckets.setInterval(maxBucketSpanSeconds + 's'); + buckets.setBounds(bounds); + } + + return buckets.getInterval(); +} + +export function loadViewByTopFieldValuesForSelectedTime( + earliestMs, + latestMs, + selectedJobs, + viewBySwimlaneFieldName, + swimlaneLimit, + noInfluencersConfigured +) { + const selectedJobIds = selectedJobs.map(d => d.id); + + // Find the top field values for the selected time, and then load the 'view by' + // swimlane over the full time range for those specific field values. + return new Promise((resolve) => { + if (viewBySwimlaneFieldName !== VIEW_BY_JOB_LABEL) { + mlResultsService.getTopInfluencers( + selectedJobIds, + earliestMs, + latestMs, + swimlaneLimit + ).then((resp) => { + if (resp.influencers[viewBySwimlaneFieldName] === undefined) { + resolve([]); + } + + const topFieldValues = []; + const topInfluencers = resp.influencers[viewBySwimlaneFieldName]; + topInfluencers.forEach((influencerData) => { + if (influencerData.maxAnomalyScore > 0) { + topFieldValues.push(influencerData.influencerFieldValue); + } + }); + resolve(topFieldValues); + }); + } else { + mlResultsService.getScoresByBucket( + selectedJobIds, + earliestMs, + latestMs, + getSwimlaneBucketInterval(selectedJobs, getSwimlaneContainerWidth(noInfluencersConfigured)).asSeconds() + 's', + swimlaneLimit + ).then((resp) => { + const topFieldValues = Object.keys(resp.results); + resolve(topFieldValues); + }); + } + }); +} + +// Obtain the list of 'View by' fields per job and viewBySwimlaneFieldName export function getViewBySwimlaneOptions({ - currentSwimlaneViewByFieldName, + currentViewBySwimlaneFieldName, filterActive, filteredFields, isAndOperator, @@ -219,20 +314,20 @@ export function getViewBySwimlaneOptions({ viewByOptions.push(VIEW_BY_JOB_LABEL); let viewBySwimlaneOptions = viewByOptions; - let swimlaneViewByFieldName = undefined; + let viewBySwimlaneFieldName = undefined; if ( - viewBySwimlaneOptions.indexOf(currentSwimlaneViewByFieldName) !== -1 + viewBySwimlaneOptions.indexOf(currentViewBySwimlaneFieldName) !== -1 ) { // Set the swimlane viewBy to that stored in the state (URL) if set. // This means we reset it to the current state because it was set by the listener // on initialization. - swimlaneViewByFieldName = currentSwimlaneViewByFieldName; + viewBySwimlaneFieldName = currentViewBySwimlaneFieldName; } else { if (selectedJobIds.length > 1) { // If more than one job selected, default to job ID. - swimlaneViewByFieldName = VIEW_BY_JOB_LABEL; - } else { + viewBySwimlaneFieldName = VIEW_BY_JOB_LABEL; + } else if (mlJobService.jobs.length > 0) { // For a single job, default to the first partition, over, // by or influencer field of the first selected job. const firstSelectedJob = mlJobService.jobs.find((job) => { @@ -245,7 +340,7 @@ export function getViewBySwimlaneOptions({ detector.partition_field_name !== undefined && firstJobInfluencers.indexOf(detector.partition_field_name) !== -1 ) { - swimlaneViewByFieldName = detector.partition_field_name; + viewBySwimlaneFieldName = detector.partition_field_name; return false; } @@ -253,7 +348,7 @@ export function getViewBySwimlaneOptions({ detector.over_field_name !== undefined && firstJobInfluencers.indexOf(detector.over_field_name) !== -1 ) { - swimlaneViewByFieldName = detector.over_field_name; + viewBySwimlaneFieldName = detector.over_field_name; return false; } @@ -265,17 +360,17 @@ export function getViewBySwimlaneOptions({ detector.over_field_name === undefined && firstJobInfluencers.indexOf(detector.by_field_name) !== -1 ) { - swimlaneViewByFieldName = detector.by_field_name; + viewBySwimlaneFieldName = detector.by_field_name; return false; } }); - if (swimlaneViewByFieldName === undefined) { + if (viewBySwimlaneFieldName === undefined) { if (firstJobInfluencers.length > 0) { - swimlaneViewByFieldName = firstJobInfluencers[0]; + viewBySwimlaneFieldName = firstJobInfluencers[0]; } else { // No influencers for first selected job - set to first available option. - swimlaneViewByFieldName = viewBySwimlaneOptions.length > 0 + viewBySwimlaneFieldName = viewBySwimlaneOptions.length > 0 ? viewBySwimlaneOptions[0] : undefined; } @@ -301,7 +396,7 @@ export function getViewBySwimlaneOptions({ } return { - swimlaneViewByFieldName, + viewBySwimlaneFieldName, viewBySwimlaneOptions, }; } @@ -339,8 +434,8 @@ export function processOverallResults(scoresByTime, searchBounds, interval) { export function processViewByResults( scoresByInfluencerAndTime, sortedLaneValues, - overallSwimlaneData, - swimlaneViewByFieldName, + bounds, + viewBySwimlaneFieldName, interval, ) { // Processes the scores for the 'view by' swimlane. @@ -348,14 +443,14 @@ export function processViewByResults( // values in the order in which they should be displayed, // or pass an empty array to sort lanes according to max score over all time. const dataset = { - fieldName: swimlaneViewByFieldName, + fieldName: viewBySwimlaneFieldName, points: [], interval }; // Set the earliest and latest to be the same as the overall swimlane. - dataset.earliest = overallSwimlaneData.earliest; - dataset.latest = overallSwimlaneData.latest; + dataset.earliest = bounds.earliest; + dataset.latest = bounds.latest; const laneLabels = []; const maxScoreByLaneLabel = {}; @@ -548,7 +643,7 @@ export async function loadDataForCharts(jobIds, earliestMs, latestMs, influencer .then((resp) => { // Ignore this response if it's returned by an out of date promise if (newRequestCount < requestCount) { - resolve(undefined); + resolve([]); } if ((selectedCells !== null && Object.keys(selectedCells).length > 0) || @@ -557,11 +652,147 @@ export async function loadDataForCharts(jobIds, earliestMs, latestMs, influencer resolve(resp.records); } - resolve(undefined); + resolve([]); }); }); } +export function loadOverallData(selectedJobs, interval, bounds) { + return new Promise((resolve) => { + // Loads the overall data components i.e. the overall swimlane and influencers list. + if (selectedJobs === null) { + resolve({ + loading: false, + hasResuts: false + }); + return; + } + + // Ensure the search bounds align to the bucketing interval used in the swimlane so + // that the first and last buckets are complete. + const searchBounds = getBoundsRoundedToInterval( + bounds, + interval, + false + ); + const selectedJobIds = selectedJobs.map(d => d.id); + + // Load the overall bucket scores by time. + // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets + // which wouldn't be the case if e.g. '1M' was used. + // Pass 'true' when obtaining bucket bounds due to the way the overall_buckets endpoint works + // to ensure the search is inclusive of end time. + const overallBucketsBounds = getBoundsRoundedToInterval( + bounds, + interval, + true + ); + mlResultsService.getOverallBucketScores( + selectedJobIds, + // Note there is an optimization for when top_n == 1. + // If top_n > 1, we should test what happens when the request takes long + // and refactor the loading calls, if necessary, to avoid delays in loading other components. + 1, + overallBucketsBounds.min.valueOf(), + overallBucketsBounds.max.valueOf(), + interval.asSeconds() + 's' + ).then((resp) => { + const overallSwimlaneData = processOverallResults( + resp.results, + searchBounds, + interval.asSeconds(), + ); + + console.log('Explorer overall swimlane data set:', overallSwimlaneData); + resolve({ + loading: false, + overallSwimlaneData, + }); + }); + }); +} + +export function loadViewBySwimlane( + fieldValues, + bounds, + selectedJobs, + viewBySwimlaneFieldName, + swimlaneLimit, + influencersFilterQuery, + noInfluencersConfigured +) { + + return new Promise((resolve) => { + + const finish = (resp) => { + if (resp !== undefined) { + const viewBySwimlaneData = processViewByResults( + resp.results, + fieldValues, + bounds, + viewBySwimlaneFieldName, + getSwimlaneBucketInterval(selectedJobs, getSwimlaneContainerWidth(noInfluencersConfigured)).asSeconds(), + ); + console.log('Explorer view by swimlane data set:', viewBySwimlaneData); + + resolve({ + viewBySwimlaneData, + viewBySwimlaneDataLoading: false + }); + } else { + resolve({ viewBySwimlaneDataLoading: false }); + } + }; + + if ( + selectedJobs === undefined || + viewBySwimlaneFieldName === undefined + ) { + finish(); + return; + } else { + // Ensure the search bounds align to the bucketing interval used in the swimlane so + // that the first and last buckets are complete. + const timefilterBounds = timefilter.getActiveBounds(); + const searchBounds = getBoundsRoundedToInterval( + timefilterBounds, + getSwimlaneBucketInterval(selectedJobs, getSwimlaneContainerWidth(noInfluencersConfigured)), + false, + ); + const selectedJobIds = selectedJobs.map(d => d.id); + + // load scores by influencer/jobId value and time. + // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets + // which wouldn't be the case if e.g. '1M' was used. + const interval = `${getSwimlaneBucketInterval( + selectedJobs, + getSwimlaneContainerWidth(noInfluencersConfigured) + ).asSeconds()}s`; + if (viewBySwimlaneFieldName !== VIEW_BY_JOB_LABEL) { + mlResultsService.getInfluencerValueMaxScoreByTime( + selectedJobIds, + viewBySwimlaneFieldName, + fieldValues, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + interval, + swimlaneLimit, + influencersFilterQuery + ).then(finish); + } else { + const jobIds = (fieldValues !== undefined && fieldValues.length > 0) ? fieldValues : selectedJobIds; + mlResultsService.getScoresByBucket( + jobIds, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + interval, + swimlaneLimit + ).then(finish); + } + } + }); +} + export async function loadTopInfluencers( selectedJobIds, earliestMs, @@ -589,3 +820,32 @@ export async function loadTopInfluencers( } }); } + +export function restoreAppState(appState) { + // Select any jobs set in the global state (i.e. passed in the URL). + let selectedCells; + let filterData = {}; + + // keep swimlane selection, restore selectedCells from AppState + if (appState.mlExplorerSwimlane.selectedType !== undefined) { + selectedCells = { + type: appState.mlExplorerSwimlane.selectedType, + lanes: appState.mlExplorerSwimlane.selectedLanes, + times: appState.mlExplorerSwimlane.selectedTimes, + showTopFieldValues: appState.mlExplorerSwimlane.showTopFieldValues, + viewByFieldName: appState.mlExplorerSwimlane.viewByFieldName, + }; + } + + // keep influencers filter selection, restore from AppState + if (appState.mlExplorerFilter.influencersFilterQuery !== undefined) { + filterData = { + influencersFilterQuery: appState.mlExplorerFilter.influencersFilterQuery, + filterActive: appState.mlExplorerFilter.filterActive, + filteredFields: appState.mlExplorerFilter.filteredFields, + queryString: appState.mlExplorerFilter.queryString, + }; + } + + return { filterData, selectedCells, viewBySwimlaneFieldName: appState.mlExplorerSwimlane.viewByFieldName }; +} diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/index.ts similarity index 80% rename from x-pack/legacy/plugins/ml/public/application/explorer/index.js rename to x-pack/legacy/plugins/ml/public/application/explorer/index.ts index ebd3eb9c12662..edc25565daa9f 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/index.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/index.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ - - -import '../explorer/explorer_controller'; import '../explorer/explorer_dashboard_service'; -import '../explorer/explorer_react_wrapper_directive'; +import '../explorer/explorer_directive'; +import '../explorer/explorer_route'; import '../explorer/explorer_charts'; import '../explorer/select_limit'; import '../components/job_selector'; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/legacy_utils.js b/x-pack/legacy/plugins/ml/public/application/explorer/legacy_utils.js deleted file mode 100644 index 62feabdf1e141..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/explorer/legacy_utils.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// This file includes utils which should eventuelly become obsolete once Anomaly Explorer -// is fully migrated to React. Their purpose is to retain functionality while we migrate step by step. - -export function getChartContainerWidth() { - const chartContainer = document.querySelector('.explorer-charts'); - return Math.floor(chartContainer && chartContainer.clientWidth || 0); -} - -export function getSwimlaneContainerWidth(noInfluencersConfigured = true) { - const explorerContainer = document.querySelector('.ml-explorer'); - const explorerContainerWidth = explorerContainer && explorerContainer.clientWidth || 0; - if (noInfluencersConfigured === true) { - // swimlane is full width, minus 30 for the 'no influencers' info icon, - // minus 170 for the lane labels, minus 50 padding - return explorerContainerWidth - 250; - } else { - // swimlane width is 5 sixths of the window, - // minus 170 for the lane labels, minus 50 padding - return ((explorerContainerWidth / 6) * 5) - 220; - } -} diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/legacy_utils.ts b/x-pack/legacy/plugins/ml/public/application/explorer/legacy_utils.ts new file mode 100644 index 0000000000000..3b92ee3fa37f6 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/legacy_utils.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// This file includes utils which should eventuelly become obsolete once Anomaly Explorer +// is fully migrated to React. Their purpose is to retain functionality while we migrate step by step. + +export function getChartContainerWidth() { + const chartContainer = document.querySelector('.explorer-charts'); + return Math.floor((chartContainer && chartContainer.clientWidth) || 0); +} + +export function getSwimlaneContainerWidth() { + const explorerContainer = document.querySelector('.ml-explorer'); + return (explorerContainer && explorerContainer.clientWidth) || 0; +} diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/reducers/app_state_reducer.ts b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/app_state_reducer.ts new file mode 100644 index 0000000000000..66e00a41a3f31 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/app_state_reducer.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { cloneDeep } from 'lodash'; + +import { EXPLORER_ACTION } from '../explorer_constants'; +import { Action } from '../explorer_dashboard_service'; + +export interface ExplorerAppState { + mlExplorerSwimlane: { + selectedType?: string; + selectedLanes?: string[]; + selectedTimes?: number[]; + showTopFieldValues?: boolean; + viewByFieldName?: string; + }; + mlExplorerFilter: { + influencersFilterQuery?: unknown; + filterActive?: boolean; + filteredFields?: string[]; + queryString?: string; + }; +} + +export function getExplorerDefaultAppState(): ExplorerAppState { + return { + mlExplorerSwimlane: {}, + mlExplorerFilter: {}, + }; +} + +export const appStateReducer = (state: ExplorerAppState, nextAction: Action) => { + const { type, payload } = nextAction; + + const appState = cloneDeep(state); + + if (appState.mlExplorerSwimlane === undefined) { + appState.mlExplorerSwimlane = {}; + } + if (appState.mlExplorerFilter === undefined) { + appState.mlExplorerFilter = {}; + } + + switch (type) { + case EXPLORER_ACTION.APP_STATE_SET: + return { ...appState, ...payload }; + + case EXPLORER_ACTION.APP_STATE_CLEAR_SELECTION: + delete appState.mlExplorerSwimlane.selectedType; + delete appState.mlExplorerSwimlane.selectedLanes; + delete appState.mlExplorerSwimlane.selectedTimes; + delete appState.mlExplorerSwimlane.showTopFieldValues; + break; + + case EXPLORER_ACTION.APP_STATE_SAVE_SELECTION: + const swimlaneSelectedCells = payload; + appState.mlExplorerSwimlane.selectedType = swimlaneSelectedCells.type; + appState.mlExplorerSwimlane.selectedLanes = swimlaneSelectedCells.lanes; + appState.mlExplorerSwimlane.selectedTimes = swimlaneSelectedCells.times; + appState.mlExplorerSwimlane.showTopFieldValues = swimlaneSelectedCells.showTopFieldValues; + appState.mlExplorerSwimlane.viewByFieldName = swimlaneSelectedCells.viewByFieldName; + break; + + case EXPLORER_ACTION.APP_STATE_SAVE_VIEW_BY_SWIMLANE_FIELD_NAME: + appState.mlExplorerSwimlane.viewByFieldName = payload.viewBySwimlaneFieldName; + break; + + case EXPLORER_ACTION.APP_STATE_SAVE_INFLUENCER_FILTER_SETTINGS: + appState.mlExplorerFilter.influencersFilterQuery = payload.influencersFilterQuery; + appState.mlExplorerFilter.filterActive = payload.filterActive; + appState.mlExplorerFilter.filteredFields = payload.filteredFields; + appState.mlExplorerFilter.queryString = payload.queryString; + break; + + case EXPLORER_ACTION.APP_STATE_CLEAR_INFLUENCER_FILTER_SETTINGS: + delete appState.mlExplorerFilter.influencersFilterQuery; + delete appState.mlExplorerFilter.filterActive; + delete appState.mlExplorerFilter.filteredFields; + delete appState.mlExplorerFilter.queryString; + break; + + default: + } + + return appState; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/check_selected_cells.ts b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/check_selected_cells.ts new file mode 100644 index 0000000000000..28f04bf65634a --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/check_selected_cells.ts @@ -0,0 +1,60 @@ +/* + * 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 { EXPLORER_ACTION, SWIMLANE_TYPE } from '../../explorer_constants'; +import { getClearedSelectedAnomaliesState } from '../../explorer_utils'; + +import { appStateReducer } from '../app_state_reducer'; + +import { ExplorerState } from './state'; + +interface SwimlanePoint { + laneLabel: string; + time: number; +} + +// do a sanity check against selectedCells. It can happen that a previously +// selected lane loaded via URL/AppState is not available anymore. +// If filter is active - selectedCell may not be available due to swimlane view by change to filter fieldName +// Ok to keep cellSelection in this case +export const checkSelectedCells = (state: ExplorerState) => { + const { filterActive, selectedCells, viewBySwimlaneData, viewBySwimlaneDataLoading } = state; + + if (viewBySwimlaneDataLoading) { + return {}; + } + + let clearSelection = false; + if ( + selectedCells !== null && + selectedCells.type === SWIMLANE_TYPE.VIEW_BY && + viewBySwimlaneData !== undefined && + viewBySwimlaneData.points !== undefined + ) { + clearSelection = + filterActive === false && + !selectedCells.lanes.some((lane: string) => { + return viewBySwimlaneData.points.some((point: SwimlanePoint) => { + return ( + point.laneLabel === lane && + point.time >= selectedCells.times[0] && + point.time <= selectedCells.times[1] + ); + }); + }); + } + + if (clearSelection === true) { + return { + appState: appStateReducer(state.appState, { + type: EXPLORER_ACTION.APP_STATE_CLEAR_SELECTION, + }), + ...getClearedSelectedAnomaliesState(), + }; + } + + return {}; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts new file mode 100644 index 0000000000000..29c077a5cba43 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EXPLORER_ACTION } from '../../explorer_constants'; +import { getClearedSelectedAnomaliesState } from '../../explorer_utils'; + +import { appStateReducer } from '../app_state_reducer'; + +import { ExplorerState } from './state'; + +export function clearInfluencerFilterSettings(state: ExplorerState): ExplorerState { + const appStateClearInfluencer = appStateReducer(state.appState, { + type: EXPLORER_ACTION.APP_STATE_CLEAR_INFLUENCER_FILTER_SETTINGS, + }); + const appStateClearSelection = appStateReducer(appStateClearInfluencer, { + type: EXPLORER_ACTION.APP_STATE_CLEAR_SELECTION, + }); + + return { + ...state, + appState: appStateClearSelection, + filterActive: false, + filteredFields: [], + influencersFilterQuery: undefined, + isAndOperator: false, + maskAll: false, + queryString: '', + tableQueryString: '', + ...getClearedSelectedAnomaliesState(), + }; +} diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/get_index_pattern.ts b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/get_index_pattern.ts new file mode 100644 index 0000000000000..9b6c7e4fb99bc --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/get_index_pattern.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; + +import { getInfluencers, ExplorerJob } from '../../explorer_utils'; + +// Creates index pattern in the format expected by the kuery bar/kuery autocomplete provider +// Field objects required fields: name, type, aggregatable, searchable +export function getIndexPattern(selectedJobs: ExplorerJob[]) { + return { + title: ML_RESULTS_INDEX_PATTERN, + fields: getInfluencers(selectedJobs).map(influencer => ({ + name: influencer, + type: 'string', + aggregatable: true, + searchable: true, + })), + }; +} diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/index.ts b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/index.ts new file mode 100644 index 0000000000000..7f2281454a4ea --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/index.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 { getIndexPattern } from './get_index_pattern'; +export { explorerReducer } from './reducer'; +export { getExplorerDefaultState, ExplorerState } from './state'; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/initialize.ts b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/initialize.ts new file mode 100644 index 0000000000000..8536c8f3e542e --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/initialize.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ActionPayload } from '../../explorer_dashboard_service'; +import { getInfluencers } from '../../explorer_utils'; + +import { getIndexPattern } from './get_index_pattern'; +import { ExplorerState } from './state'; + +export const initialize = (state: ExplorerState, payload: ActionPayload): ExplorerState => { + const { selectedCells, selectedJobs, viewBySwimlaneFieldName, filterData } = payload; + let currentSelectedCells = state.selectedCells; + let currentviewBySwimlaneFieldName = state.viewBySwimlaneFieldName; + + if (viewBySwimlaneFieldName !== undefined) { + currentviewBySwimlaneFieldName = viewBySwimlaneFieldName; + } + + if (selectedCells !== undefined && currentSelectedCells === null) { + currentSelectedCells = selectedCells; + } + + return { + ...state, + indexPattern: getIndexPattern(selectedJobs), + noInfluencersConfigured: getInfluencers(selectedJobs).length === 0, + selectedCells: currentSelectedCells, + selectedJobs, + viewBySwimlaneFieldName: currentviewBySwimlaneFieldName, + ...(filterData.influencersFilterQuery !== undefined ? { ...filterData } : {}), + }; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts new file mode 100644 index 0000000000000..9fe8ebbb2c481 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts @@ -0,0 +1,61 @@ +/* + * 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 { EXPLORER_ACTION, VIEW_BY_JOB_LABEL } from '../../explorer_constants'; +import { ActionPayload } from '../../explorer_dashboard_service'; +import { + getClearedSelectedAnomaliesState, + getDefaultSwimlaneData, + getInfluencers, +} from '../../explorer_utils'; + +import { appStateReducer } from '../app_state_reducer'; + +import { getIndexPattern } from './get_index_pattern'; +import { getExplorerDefaultState, ExplorerState } from './state'; + +export const jobSelectionChange = (state: ExplorerState, payload: ActionPayload): ExplorerState => { + const { selectedJobs } = payload; + const stateUpdate: ExplorerState = { + ...state, + appState: appStateReducer(getExplorerDefaultState().appState, { + type: EXPLORER_ACTION.APP_STATE_CLEAR_SELECTION, + }), + ...getClearedSelectedAnomaliesState(), + noInfluencersConfigured: getInfluencers(selectedJobs).length === 0, + overallSwimlaneData: getDefaultSwimlaneData(), + selectedJobs, + }; + + // clear filter if selected jobs have no influencers + if (stateUpdate.noInfluencersConfigured === true) { + stateUpdate.appState = appStateReducer(stateUpdate.appState, { + type: EXPLORER_ACTION.APP_STATE_CLEAR_INFLUENCER_FILTER_SETTINGS, + }); + const noFilterState = { + filterActive: false, + filteredFields: [], + influencersFilterQuery: undefined, + maskAll: false, + queryString: '', + tableQueryString: '', + }; + + Object.assign(stateUpdate, noFilterState); + } else { + // indexPattern will not be used if there are no influencers so set up can be skipped + // indexPattern is passed to KqlFilterBar which is only shown if (noInfluencersConfigured === false) + stateUpdate.indexPattern = getIndexPattern(selectedJobs); + } + + if (selectedJobs.length > 1) { + stateUpdate.viewBySwimlaneFieldName = VIEW_BY_JOB_LABEL; + return stateUpdate; + } + + stateUpdate.loading = true; + return stateUpdate; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts new file mode 100644 index 0000000000000..1919ce949683f --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts @@ -0,0 +1,249 @@ +/* + * 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 { formatHumanReadableDateTime } from '../../../util/date_utils'; + +import { getDefaultChartsData } from '../../explorer_charts/explorer_charts_container_service'; +import { EXPLORER_ACTION, SWIMLANE_TYPE, VIEW_BY_JOB_LABEL } from '../../explorer_constants'; +import { Action } from '../../explorer_dashboard_service'; +import { + getClearedSelectedAnomaliesState, + getDefaultSwimlaneData, + getSelectionTimeRange, + getSwimlaneBucketInterval, + getViewBySwimlaneOptions, +} from '../../explorer_utils'; +import { appStateReducer } from '../app_state_reducer'; + +import { checkSelectedCells } from './check_selected_cells'; +import { clearInfluencerFilterSettings } from './clear_influencer_filter_settings'; +import { initialize } from './initialize'; +import { jobSelectionChange } from './job_selection_change'; +import { getExplorerDefaultState, ExplorerState } from './state'; +import { setInfluencerFilterSettings } from './set_influencer_filter_settings'; +import { setKqlQueryBarPlaceholder } from './set_kql_query_bar_placeholder'; + +export const explorerReducer = (state: ExplorerState, nextAction: Action): ExplorerState => { + const { type, payload } = nextAction; + + let nextState; + + switch (type) { + case EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS: + nextState = clearInfluencerFilterSettings(state); + break; + + case EXPLORER_ACTION.CLEAR_JOBS: + nextState = { + ...state, + ...getClearedSelectedAnomaliesState(), + appState: appStateReducer(state.appState, { + type: EXPLORER_ACTION.APP_STATE_CLEAR_SELECTION, + }), + loading: false, + selectedJobs: [], + }; + break; + + case EXPLORER_ACTION.CLEAR_SELECTION: + nextState = { + ...state, + ...getClearedSelectedAnomaliesState(), + appState: appStateReducer(state.appState, { + type: EXPLORER_ACTION.APP_STATE_CLEAR_SELECTION, + }), + }; + break; + + case EXPLORER_ACTION.INITIALIZE: + nextState = initialize(state, payload); + break; + + case EXPLORER_ACTION.JOB_SELECTION_CHANGE: + nextState = jobSelectionChange(state, payload); + break; + + case EXPLORER_ACTION.APP_STATE_SET: + case EXPLORER_ACTION.APP_STATE_CLEAR_SELECTION: + case EXPLORER_ACTION.APP_STATE_SAVE_SELECTION: + case EXPLORER_ACTION.APP_STATE_SAVE_VIEW_BY_SWIMLANE_FIELD_NAME: + case EXPLORER_ACTION.APP_STATE_SAVE_INFLUENCER_FILTER_SETTINGS: + case EXPLORER_ACTION.APP_STATE_CLEAR_INFLUENCER_FILTER_SETTINGS: + nextState = { ...state, appState: appStateReducer(state.appState, nextAction) }; + break; + + case EXPLORER_ACTION.RESET: + nextState = getExplorerDefaultState(); + break; + + case EXPLORER_ACTION.SET_BOUNDS: + nextState = { ...state, bounds: payload }; + break; + + case EXPLORER_ACTION.SET_CHARTS: + nextState = { + ...state, + chartsData: { + ...getDefaultChartsData(), + chartsPerRow: payload.chartsPerRow, + seriesToPlot: payload.seriesToPlot, + // convert truthy/falsy value to Boolean + tooManyBuckets: !!payload.tooManyBuckets, + }, + }; + break; + + case EXPLORER_ACTION.SET_INFLUENCER_FILTER_SETTINGS: + nextState = setInfluencerFilterSettings(state, payload); + break; + + case EXPLORER_ACTION.SET_SELECTED_CELLS: + const selectedCells = payload; + selectedCells.showTopFieldValues = false; + + const currentSwimlaneType = state.selectedCells?.type; + const currentShowTopFieldValues = state.selectedCells?.showTopFieldValues; + const newSwimlaneType = selectedCells?.type; + + if ( + (currentSwimlaneType === SWIMLANE_TYPE.OVERALL && + newSwimlaneType === SWIMLANE_TYPE.VIEW_BY) || + newSwimlaneType === SWIMLANE_TYPE.OVERALL || + currentShowTopFieldValues === true + ) { + selectedCells.showTopFieldValues = true; + } + + nextState = { + ...state, + appState: appStateReducer(state.appState, { + type: EXPLORER_ACTION.APP_STATE_SAVE_SELECTION, + payload, + }), + selectedCells, + }; + break; + + case EXPLORER_ACTION.SET_STATE: + if (payload.viewBySwimlaneFieldName) { + nextState = { + ...state, + ...payload, + appState: appStateReducer(state.appState, { + type: EXPLORER_ACTION.APP_STATE_SAVE_VIEW_BY_SWIMLANE_FIELD_NAME, + payload: { viewBySwimlaneFieldName: payload.viewBySwimlaneFieldName }, + }), + }; + } else { + nextState = { ...state, ...payload }; + } + break; + + case EXPLORER_ACTION.SET_SWIMLANE_CONTAINER_WIDTH: + if (state.noInfluencersConfigured === true) { + // swimlane is full width, minus 30 for the 'no influencers' info icon, + // minus 170 for the lane labels, minus 50 padding + nextState = { ...state, swimlaneContainerWidth: payload - 250 }; + } else { + // swimlane width is 5 sixths of the window, + // minus 170 for the lane labels, minus 50 padding + nextState = { ...state, swimlaneContainerWidth: (payload / 6) * 5 - 220 }; + } + break; + + case EXPLORER_ACTION.SET_SWIMLANE_LIMIT: + nextState = { + ...state, + appState: appStateReducer(state.appState, { + type: EXPLORER_ACTION.APP_STATE_CLEAR_SELECTION, + }), + ...getClearedSelectedAnomaliesState(), + swimlaneLimit: payload, + }; + break; + + case EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_FIELD_NAME: + const { filteredFields, influencersFilterQuery } = state; + const viewBySwimlaneFieldName = payload; + + let maskAll = false; + + if (influencersFilterQuery !== undefined) { + maskAll = + viewBySwimlaneFieldName === VIEW_BY_JOB_LABEL || + filteredFields.includes(viewBySwimlaneFieldName) === false; + } + + nextState = { + ...state, + ...getClearedSelectedAnomaliesState(), + appState: appStateReducer(state.appState, { + type: EXPLORER_ACTION.APP_STATE_CLEAR_SELECTION, + }), + maskAll, + viewBySwimlaneFieldName, + }; + break; + + case EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_LOADING: + const { annotationsData, overallState, tableData } = payload; + nextState = { + ...state, + annotationsData, + ...overallState, + tableData, + viewBySwimlaneData: { + ...getDefaultSwimlaneData(), + }, + viewBySwimlaneDataLoading: true, + }; + break; + + default: + nextState = state; + } + + if (nextState.selectedJobs === null || nextState.bounds === undefined) { + return nextState; + } + + const swimlaneBucketInterval = getSwimlaneBucketInterval( + nextState.selectedJobs, + nextState.swimlaneContainerWidth + ); + + // Does a sanity check on the selected `viewBySwimlaneFieldName` + // and return the available `viewBySwimlaneOptions`. + const { viewBySwimlaneFieldName, viewBySwimlaneOptions } = getViewBySwimlaneOptions({ + currentViewBySwimlaneFieldName: nextState.viewBySwimlaneFieldName, + filterActive: nextState.filterActive, + filteredFields: nextState.filteredFields, + isAndOperator: nextState.isAndOperator, + selectedJobs: nextState.selectedJobs, + selectedCells: nextState.selectedCells, + }); + + const { bounds, selectedCells } = nextState; + + const timerange = getSelectionTimeRange( + selectedCells, + swimlaneBucketInterval.asSeconds(), + bounds + ); + + return { + ...nextState, + swimlaneBucketInterval, + viewByLoadedForTimeFormatted: + selectedCells !== null && selectedCells.showTopFieldValues === true + ? formatHumanReadableDateTime(timerange.earliestMs) + : null, + viewBySwimlaneFieldName, + viewBySwimlaneOptions, + ...checkSelectedCells(nextState), + ...setKqlQueryBarPlaceholder(nextState), + }; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_influencer_filter_settings.ts b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_influencer_filter_settings.ts new file mode 100644 index 0000000000000..76577ae557fe3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_influencer_filter_settings.ts @@ -0,0 +1,72 @@ +/* + * 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 { EXPLORER_ACTION, VIEW_BY_JOB_LABEL } from '../../explorer_constants'; +import { ActionPayload } from '../../explorer_dashboard_service'; + +import { appStateReducer } from '../app_state_reducer'; + +import { ExplorerState } from './state'; + +export function setInfluencerFilterSettings( + state: ExplorerState, + payload: ActionPayload +): ExplorerState { + const { + filterQuery: influencersFilterQuery, + isAndOperator, + filteredFields, + queryString, + tableQueryString, + } = payload; + + const { selectedCells, viewBySwimlaneOptions } = state; + let selectedViewByFieldName = state.viewBySwimlaneFieldName; + + // if it's an AND filter set view by swimlane to job ID as the others will have no results + if (isAndOperator && selectedCells === null) { + selectedViewByFieldName = VIEW_BY_JOB_LABEL; + } else { + // Set View by dropdown to first relevant fieldName based on incoming filter if there's no cell selection already + // or if selected cell is from overall swimlane as this won't include an additional influencer filter + for (let i = 0; i < filteredFields.length; i++) { + if ( + viewBySwimlaneOptions.includes(filteredFields[i]) && + (selectedCells === null || (selectedCells && selectedCells.type === 'overall')) + ) { + selectedViewByFieldName = filteredFields[i]; + break; + } + } + } + + const appState = appStateReducer(state.appState, { + type: EXPLORER_ACTION.APP_STATE_SAVE_INFLUENCER_FILTER_SETTINGS, + payload: { + influencersFilterQuery, + filterActive: true, + filteredFields, + queryString, + tableQueryString, + isAndOperator, + }, + }); + + return { + ...state, + appState, + filterActive: true, + filteredFields, + influencersFilterQuery, + isAndOperator, + queryString, + tableQueryString, + maskAll: + selectedViewByFieldName === VIEW_BY_JOB_LABEL || + filteredFields.includes(selectedViewByFieldName) === false, + viewBySwimlaneFieldName: selectedViewByFieldName, + }; +} diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_kql_query_bar_placeholder.ts b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_kql_query_bar_placeholder.ts new file mode 100644 index 0000000000000..f0f3767974fdb --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_kql_query_bar_placeholder.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { ExplorerState } from './state'; + +// Set the KQL query bar placeholder value +export const setKqlQueryBarPlaceholder = (state: ExplorerState) => { + const { influencers, noInfluencersConfigured } = state; + + if (influencers !== undefined && !noInfluencersConfigured) { + for (const influencerName in influencers) { + if (influencers[influencerName][0] && influencers[influencerName][0].influencerFieldValue) { + return { + filterPlaceHolder: i18n.translate('xpack.ml.explorer.kueryBar.filterPlaceholder', { + defaultMessage: 'Filter by influencer fields… ({queryExample})', + values: { + queryExample: `${influencerName} : ${influencers[influencerName][0].influencerFieldValue}`, + }, + }), + }; + } + } + } + + return {}; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts new file mode 100644 index 0000000000000..ce37605c3a926 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts @@ -0,0 +1,104 @@ +/* + * 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 { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; +import { Dictionary } from '../../../../../common/types/common'; + +import { + getDefaultChartsData, + ExplorerChartsData, +} from '../../explorer_charts/explorer_charts_container_service'; +import { + getDefaultSwimlaneData, + AnomaliesTableData, + ExplorerJob, + SwimlaneData, + TimeRangeBounds, +} from '../../explorer_utils'; + +import { getExplorerDefaultAppState, ExplorerAppState } from '../app_state_reducer'; + +export interface ExplorerState { + annotationsData: any[]; + anomalyChartRecords: any[]; + appState: ExplorerAppState; + bounds: TimeRangeBounds | undefined; + chartsData: ExplorerChartsData; + fieldFormatsLoading: boolean; + filterActive: boolean; + filteredFields: any[]; + filterPlaceHolder: any; + indexPattern: { title: string; fields: any[] }; + influencersFilterQuery: any; + influencers: Dictionary; + isAndOperator: boolean; + loading: boolean; + maskAll: boolean; + noInfluencersConfigured: boolean; + overallSwimlaneData: SwimlaneData; + queryString: string; + selectedCells: any; + selectedJobs: ExplorerJob[] | null; + swimlaneBucketInterval: any; + swimlaneContainerWidth: number; + swimlaneLimit: number; + tableData: AnomaliesTableData; + tableInterval: string; + tableQueryString: string; + tableSeverity: number; + viewByLoadedForTimeFormatted: string | null; + viewBySwimlaneData: SwimlaneData; + viewBySwimlaneDataLoading: boolean; + viewBySwimlaneFieldName?: string; + viewBySwimlaneOptions: string[]; +} + +function getDefaultIndexPattern() { + return { title: ML_RESULTS_INDEX_PATTERN, fields: [] }; +} + +export function getExplorerDefaultState(): ExplorerState { + return { + annotationsData: [], + anomalyChartRecords: [], + appState: getExplorerDefaultAppState(), + bounds: undefined, + chartsData: getDefaultChartsData(), + fieldFormatsLoading: false, + filterActive: false, + filteredFields: [], + filterPlaceHolder: undefined, + indexPattern: getDefaultIndexPattern(), + influencersFilterQuery: undefined, + influencers: {}, + isAndOperator: false, + loading: true, + maskAll: false, + noInfluencersConfigured: true, + overallSwimlaneData: getDefaultSwimlaneData(), + queryString: '', + selectedCells: null, + selectedJobs: null, + swimlaneBucketInterval: undefined, + swimlaneContainerWidth: 0, + swimlaneLimit: 10, + tableData: { + anomalies: [], + examplesByJobId: [''], + interval: 0, + jobIds: [], + showViewSeriesLink: false, + }, + tableInterval: 'auto', + tableQueryString: '', + tableSeverity: 0, + viewByLoadedForTimeFormatted: null, + viewBySwimlaneData: getDefaultSwimlaneData(), + viewBySwimlaneDataLoading: false, + viewBySwimlaneFieldName: undefined, + viewBySwimlaneOptions: [], + }; +} diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/reducers/index.ts b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/index.ts new file mode 100644 index 0000000000000..98cc07e8f9449 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/explorer/reducers/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { appStateReducer, getExplorerDefaultAppState, ExplorerAppState } from './app_state_reducer'; +export { + explorerReducer, + getExplorerDefaultState, + getIndexPattern, + ExplorerState, +} from './explorer_reducer'; diff --git a/x-pack/legacy/plugins/ml/public/application/services/annotations_service.test.tsx b/x-pack/legacy/plugins/ml/public/application/services/annotations_service.test.tsx index eed9e46a47745..d74c3802c2ed2 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/annotations_service.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/services/annotations_service.test.tsx @@ -32,10 +32,10 @@ describe('annotations_service', () => { annotationsRefresh$.subscribe(subscriber); - expect(subscriber.mock.calls).toHaveLength(0); + expect(subscriber.mock.calls).toHaveLength(1); annotationsRefresh$.next(true); - expect(subscriber.mock.calls).toHaveLength(1); + expect(subscriber.mock.calls).toHaveLength(2); }); }); diff --git a/x-pack/legacy/plugins/ml/public/application/services/annotations_service.tsx b/x-pack/legacy/plugins/ml/public/application/services/annotations_service.tsx index 051c6ab445102..6953232f0cc6c 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/annotations_service.tsx +++ b/x-pack/legacy/plugins/ml/public/application/services/annotations_service.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BehaviorSubject, Subject } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; import { Annotation } from '../../../common/types/annotations'; @@ -74,4 +74,4 @@ export const annotation$ = new BehaviorSubject(null); Instead of passing around callbacks or deeply nested props, it can be imported for both angularjs controllers/directives and React components. */ -export const annotationsRefresh$ = new Subject(); +export const annotationsRefresh$ = new BehaviorSubject(false); diff --git a/x-pack/legacy/plugins/ml/public/application/services/field_format_service.ts b/x-pack/legacy/plugins/ml/public/application/services/field_format_service.ts index ce6bc7896c44c..a9ecf56c58ea7 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/field_format_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/field_format_service.ts @@ -24,7 +24,7 @@ class FieldFormatService { // configured in the datafeed of each job. // Builds a map of Kibana FieldFormats (plugins/data/common/field_formats) // against detector index by job ID. - populateFormats(jobIds: string[]) { + populateFormats(jobIds: string[]): Promise { return new Promise((resolve, reject) => { // Populate a map of index pattern IDs against job ID, by finding the ID of the index // pattern with a title attribute which matches the index configured in the datafeed. diff --git a/x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts b/x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts index 436d13589adcc..b1d3d338e22c4 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts @@ -13,6 +13,7 @@ export interface ExistingJobsAndGroups { } declare interface JobService { + jobs: CombinedJob[]; createResultsUrlForJobs: (jobs: any[], target: string) => string; tempJobCloningObjects: { job: any; @@ -35,6 +36,7 @@ declare interface JobService { getJobAndGroupIds(): ExistingJobsAndGroups; searchPreview(job: CombinedJob): Promise>; getJob(jobId: string): CombinedJob; + loadJobsWrapper(): Promise; } export const mlJobService: JobService; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 02e29c1117ffc..a70e1d38784e9 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -166,8 +166,8 @@ export class TimeSeriesExplorer extends React.Component { constructor(props) { super(props); - const { jobSelectService, unsubscribeFromGlobalState } = jobSelectServiceFactory(props.globalState); - this.jobSelectService = jobSelectService; + const { jobSelectService$, unsubscribeFromGlobalState } = jobSelectServiceFactory(props.globalState); + this.jobSelectService$ = jobSelectService$; this.unsubscribeFromGlobalState = unsubscribeFromGlobalState; } @@ -864,7 +864,7 @@ export class TimeSeriesExplorer extends React.Component { })); // Listen for changes to job selection. - this.subscriptions.add(this.jobSelectService.subscribe(({ selection: selectedJobIds }) => { + this.subscriptions.add(this.jobSelectService$.subscribe(({ selection: selectedJobIds }) => { const jobs = createTimeSeriesJobData(mlJobService.jobs); this.contextChartSelectedInitCallDone = false; @@ -903,7 +903,7 @@ export class TimeSeriesExplorer extends React.Component { ); setGlobalState(globalState, { selectedIds: [selectedJobIds[0]] }); - this.jobSelectService.next({ selection: [selectedJobIds[0]], resetSelection: true }); + this.jobSelectService$.next({ selection: [selectedJobIds[0]], resetSelection: true }); } else { // if a group has been loaded if (selectedJobIds.length > 0) { @@ -915,12 +915,12 @@ export class TimeSeriesExplorer extends React.Component { ); setGlobalState(globalState, { selectedIds: [selectedJobIds[0]] }); - this.jobSelectService.next({ selection: [selectedJobIds[0]], resetSelection: true }); + this.jobSelectService$.next({ selection: [selectedJobIds[0]], resetSelection: true }); } else if (jobs.length > 0) { // if there are no valid jobs in the group but there are valid jobs // in the list of all jobs, select the first setGlobalState(globalState, { selectedIds: [jobs[0].id] }); - this.jobSelectService.next({ selection: [jobs[0].id], resetSelection: true }); + this.jobSelectService$.next({ selection: [jobs[0].id], resetSelection: true }); } else { // if there are no valid jobs left. this.setState({ loading: false }); @@ -930,7 +930,7 @@ export class TimeSeriesExplorer extends React.Component { // if some ids have been filtered out because they were invalid. // refresh the URL with the first valid id setGlobalState(globalState, { selectedIds: [selectedJobIds[0]] }); - this.jobSelectService.next({ selection: [selectedJobIds[0]], resetSelection: true }); + this.jobSelectService$.next({ selection: [selectedJobIds[0]], resetSelection: true }); } else if (selectedJobIds.length > 0) { // normal behavior. a job ID has been loaded from the URL if (this.state.selectedJob !== undefined && selectedJobIds[0] !== this.state.selectedJob.job_id) { @@ -943,7 +943,7 @@ export class TimeSeriesExplorer extends React.Component { // no jobs were loaded from the URL, so add the first job // from the full jobs list. setGlobalState(globalState, { selectedIds: [jobs[0].id] }); - this.jobSelectService.next({ selection: [jobs[0].id], resetSelection: true }); + this.jobSelectService$.next({ selection: [jobs[0].id], resetSelection: true }); } else { // Jobs exist, but no time series jobs. this.setState({ loading: false }); @@ -1132,7 +1132,7 @@ export class TimeSeriesExplorer extends React.Component { const jobSelectorProps = { dateFormatTz, globalState, - jobSelectService: this.jobSelectService, + jobSelectService$: this.jobSelectService$, selectedJobIds, selectedGroups, singleSelection: true, diff --git a/x-pack/legacy/plugins/ml/public/application/util/app_state_utils.d.ts b/x-pack/legacy/plugins/ml/public/application/util/app_state_utils.d.ts new file mode 100644 index 0000000000000..454ea55210dcc --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/util/app_state_utils.d.ts @@ -0,0 +1,16 @@ +/* + * 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 { Observable } from 'rxjs'; + +export const initializeAppState: (AppState: any, stateName: any, defaultState: any) => any; + +export const subscribeAppStateToObservable: ( + AppState: any, + appStateName: string, + o$: Observable, + callback: (payload: any) => void +) => any; diff --git a/x-pack/legacy/plugins/ml/public/application/util/observable_utils.tsx b/x-pack/legacy/plugins/ml/public/application/util/observable_utils.tsx index 7f1fc366bc5bb..4b8027260ab9a 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/observable_utils.tsx +++ b/x-pack/legacy/plugins/ml/public/application/util/observable_utils.tsx @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEqual } from 'lodash'; import React, { Component, ComponentType } from 'react'; - import { BehaviorSubject, Subscription } from 'rxjs'; +import { distinctUntilChanged } from 'rxjs/operators'; import { Dictionary } from '../../../common/types/common'; // Sets up a ObservableComponent which subscribes to given observable updates and @@ -30,7 +31,9 @@ export function injectObservablesAsProps( public componentDidMount() { observableKeys.forEach(k => { - this.subscriptions[k] = observables[k].subscribe(v => this.setState({ [k]: v })); + this.subscriptions[k] = observables[k] + .pipe(distinctUntilChanged(isEqual)) + .subscribe(v => this.setState({ [k]: v })); }); } @@ -41,6 +44,17 @@ export function injectObservablesAsProps( } public render() { + // All injected observables are expected to provide initial state. + // If an observable has undefined as its current value, rendering + // the wrapped component will be skipped. + if ( + Object.keys(this.state) + .map(k => this.state[k]) + .some(v => v === undefined) + ) { + return null; + } + return ( {this.props.children} diff --git a/x-pack/legacy/plugins/ml/public/application/util/time_buckets.d.ts b/x-pack/legacy/plugins/ml/public/application/util/time_buckets.d.ts index 17773b66e7456..96a4653d0026a 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/time_buckets.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/time_buckets.d.ts @@ -6,19 +6,22 @@ import { Moment } from 'moment'; -declare interface TimeFilterBounds { - min: Moment; - max: Moment; +declare interface TimeRangeBounds { + min: Moment | undefined; + max: Moment | undefined; +} + +export declare interface TimeBucketsInterval { + asMilliseconds: () => number; + asSeconds: () => number; + expression: string; } export class TimeBuckets { setBarTarget: (barTarget: number) => void; setMaxBars: (maxBars: number) => void; setInterval: (interval: string) => void; - setBounds: (bounds: TimeFilterBounds) => void; + setBounds: (bounds: TimeRangeBounds) => void; getBounds: () => { min: any; max: any }; - getInterval: () => { - asMilliseconds: () => number; - expression: string; - }; + getInterval: () => TimeBucketsInterval; } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 23ebdf0ad6aad..ad3fc5544bc3b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6931,8 +6931,8 @@ "xpack.ml.explorer.limitLabel": "制限", "xpack.ml.explorer.loadingLabel": "読み込み中", "xpack.ml.explorer.noConfiguredInfluencersTooltip": "選択されたジョブに影響因子が構成されていないため、トップインフルエンスリストは非表示になっています。", - "xpack.ml.explorer.noInfluencersFoundTitle": "{swimlaneViewByFieldName}影響因子が見つかりません", - "xpack.ml.explorer.noInfluencersFoundTitleFilterMessage": "指定されたフィルターの {swimlaneViewByFieldName} 影響因子が見つかりません", + "xpack.ml.explorer.noInfluencersFoundTitle": "{viewBySwimlaneFieldName}影響因子が見つかりません", + "xpack.ml.explorer.noInfluencersFoundTitleFilterMessage": "指定されたフィルターの {viewBySwimlaneFieldName} 影響因子が見つかりません", "xpack.ml.explorer.noJobsFoundLabel": "ジョブが見つかりません", "xpack.ml.explorer.noResultsFoundLabel": "結果が見つかりませんでした", "xpack.ml.explorer.overallLabel": "全体", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 28f6c2857d51d..14daaa7f78fad 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6933,8 +6933,8 @@ "xpack.ml.explorer.limitLabel": "限制", "xpack.ml.explorer.loadingLabel": "正在加载", "xpack.ml.explorer.noConfiguredInfluencersTooltip": "“顶级影响因素”列表被隐藏,因为没有为所选作业配置影响因素。", - "xpack.ml.explorer.noInfluencersFoundTitle": "找不到 {swimlaneViewByFieldName} 影响因素", - "xpack.ml.explorer.noInfluencersFoundTitleFilterMessage": "找不到指定筛选的 {swimlaneViewByFieldName} 影响因素", + "xpack.ml.explorer.noInfluencersFoundTitle": "找不到 {viewBySwimlaneFieldName} 影响因素", + "xpack.ml.explorer.noInfluencersFoundTitleFilterMessage": "找不到指定筛选的 {viewBySwimlaneFieldName} 影响因素", "xpack.ml.explorer.noJobsFoundLabel": "找不到作业", "xpack.ml.explorer.noResultsFoundLabel": "找不到结果", "xpack.ml.explorer.overallLabel": "总体",