From c3f6af3ae3ff3be624e0e15a687d04cb8d355274 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 28 Oct 2019 20:32:51 -0400 Subject: [PATCH] Provide uiSettings service in NP (#48413) (#49531) * provide ui settins client via context * update mocks * update types and expose setDefaults to plugins * move ui settings routes to NP * add typings fro test kbn server * move integration test & improve typings * hide client private methods, update tests * add unit tests for get_upgradable_config * inline writeErrors into createOrUpgradeConfig to simplify testing * regen docs * add functional tests for ui_settings service * unify test suites * add types for sipertest in core_plugin tests * tsify core_plugins tests * add test for empty saved config * update renovate * rename get/setDefaults to reguster * regen docs * regen docs * Update src/core/MIGRATION.md Co-Authored-By: Josh Dover --- ...ugin-public.ianonymouspaths.isanonymous.md | 2 +- .../kibana-plugin-public.ianonymouspaths.md | 4 +- ...-plugin-public.ianonymouspaths.register.md | 2 +- .../core/public/kibana-plugin-public.md | 2 +- ...a-plugin-public.uisettingsclient.getall.md | 4 +- ...-plugin-public.uisettingsclientcontract.md | 2 +- .../server/kibana-plugin-server.coresetup.md | 1 + ...bana-plugin-server.coresetup.uisettings.md | 13 ++ ...in-server.iuisettingsclient.getdefaults.md | 13 -- ...-server.iuisettingsclient.getregistered.md | 13 ++ ...erver.iuisettingsclient.getuserprovided.md | 5 +- .../kibana-plugin-server.iuisettingsclient.md | 6 +- .../core/server/kibana-plugin-server.md | 4 +- ...lugin-server.requesthandlercontext.core.md | 3 + ...ana-plugin-server.requesthandlercontext.md | 2 +- ...plugin-server.uisettingsparams.category.md | 2 +- ...gin-server.uisettingsparams.description.md | 2 +- .../kibana-plugin-server.uisettingsparams.md | 2 +- ...ana-plugin-server.uisettingsparams.name.md | 2 +- ...-plugin-server.uisettingsparams.options.md | 2 +- ...na-plugin-server.uisettingsparams.value.md | 2 +- ...na-plugin-server.uisettingsservicesetup.md | 19 +++ ...-server.uisettingsservicesetup.register.md | 28 ++++ ...-server.userprovidedvalues.isoverridden.md | 11 ++ ...kibana-plugin-server.userprovidedvalues.md | 21 +++ ...gin-server.userprovidedvalues.uservalue.md | 11 ++ package.json | 1 + renovate.json5 | 8 + .../injected_metadata_service.ts | 16 +- src/core/public/public.api.md | 11 +- src/core/public/ui_settings/types.ts | 23 +-- .../public/ui_settings/ui_settings_client.ts | 13 +- .../server/http/router/response_adapter.ts | 6 +- src/core/server/index.ts | 13 +- src/core/server/legacy/legacy_service.test.ts | 2 +- src/core/server/legacy/legacy_service.ts | 3 + src/core/server/mocks.ts | 16 ++ src/core/server/plugins/plugin.test.ts | 10 +- src/core/server/plugins/plugin_context.ts | 3 + .../server/plugins/plugins_service.test.ts | 10 +- src/core/server/plugins/plugins_service.ts | 11 +- .../server/plugins/plugins_system.test.ts | 13 +- src/core/server/server.api.md | 37 +++-- src/core/server/server.ts | 24 +-- src/core/server/types.ts | 1 + .../create_or_upgrade_saved_config.test.ts | 122 +++++++-------- .../create_or_upgrade_saved_config.ts | 22 ++- .../get_upgradeable_config.test.ts | 86 +++++++++++ .../create_or_upgrade.test.ts | 24 ++- src/core/server/ui_settings/index.ts | 14 +- .../integration_tests/doc_exists.ts | 0 .../integration_tests/doc_missing.ts | 0 .../doc_missing_and_index_read_only.ts | 0 .../integration_tests/index.test.ts | 0 .../integration_tests/lib/assert.ts | 0 .../integration_tests/lib/chance.ts | 0 .../integration_tests/lib/index.ts | 0 .../integration_tests/lib/servers.ts | 22 +-- src/core/server/ui_settings/routes/delete.ts | 61 ++++++++ src/core/server/ui_settings/routes/get.ts | 45 ++++++ src/core/server/ui_settings/routes/index.ts | 31 ++++ src/core/server/ui_settings/routes/set.ts | 67 +++++++++ .../server/ui_settings/routes/set_many.ts | 60 ++++++++ src/core/server/ui_settings/types.ts | 141 ++++++++++++++++++ .../ui_settings/ui_settings_client.test.ts | 136 +++++++---------- .../server/ui_settings/ui_settings_client.ts | 103 +++---------- .../server/ui_settings/ui_settings_errors.ts | 31 ++++ .../ui_settings/ui_settings_service.mock.ts | 9 +- .../ui_settings/ui_settings_service.test.ts | 20 ++- .../server/ui_settings/ui_settings_service.ts | 59 ++------ .../default_route_provider.test.ts | 2 +- .../http/setup_default_route_provider.ts | 2 +- .../ui_exports_replace_injected_vars.js | 2 +- .../mixin/field_formats_mixin.ts | 6 +- src/legacy/ui/ui_render/ui_render_mixin.js | 2 +- .../ui_settings_mixin.test.ts | 6 +- src/legacy/ui/ui_settings/routes/set.ts | 55 ------- .../ui/ui_settings/ui_settings_mixin.js | 13 +- src/test_utils/kbn_server.ts | 36 ++++- .../plugins/core_plugin_b/server/plugin.ts | 2 +- .../plugins/ui_settings_plugin/kibana.json | 8 + .../plugins/ui_settings_plugin/package.json | 17 +++ .../ui_settings_plugin/public}/index.ts | 6 +- .../ui_settings_plugin/public/plugin.tsx | 30 ++-- .../ui_settings_plugin/server/index.ts} | 6 +- .../ui_settings_plugin/server/plugin.ts | 50 +++---- .../plugins/ui_settings_plugin/tsconfig.json | 15 ++ .../plugin_functional/services/index.ts | 21 +-- .../services/{supertest.js => supertest.ts} | 5 +- .../{applications.js => applications.ts} | 22 +-- .../core_plugins/{index.js => index.ts} | 5 +- .../{legacy_plugins.js => legacy_plugins.ts} | 8 +- .../{server_plugins.js => server_plugins.ts} | 18 +-- .../{ui_plugins.js => ui_plugins.ts} | 12 +- .../test_suites/core_plugins/ui_settings.ts | 52 +++++++ yarn.lock | 21 +++ 96 files changed, 1236 insertions(+), 638 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.coresetup.uisettings.md delete mode 100644 docs/development/core/server/kibana-plugin-server.iuisettingsclient.getdefaults.md create mode 100644 docs/development/core/server/kibana-plugin-server.iuisettingsclient.getregistered.md create mode 100644 docs/development/core/server/kibana-plugin-server.uisettingsservicesetup.md create mode 100644 docs/development/core/server/kibana-plugin-server.uisettingsservicesetup.register.md create mode 100644 docs/development/core/server/kibana-plugin-server.userprovidedvalues.isoverridden.md create mode 100644 docs/development/core/server/kibana-plugin-server.userprovidedvalues.md create mode 100644 docs/development/core/server/kibana-plugin-server.userprovidedvalues.uservalue.md create mode 100644 src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.test.ts rename src/{legacy/ui/ui_settings/routes => core/server/ui_settings}/integration_tests/doc_exists.ts (100%) rename src/{legacy/ui/ui_settings/routes => core/server/ui_settings}/integration_tests/doc_missing.ts (100%) rename src/{legacy/ui/ui_settings/routes => core/server/ui_settings}/integration_tests/doc_missing_and_index_read_only.ts (100%) rename src/{legacy/ui/ui_settings/routes => core/server/ui_settings}/integration_tests/index.test.ts (100%) rename src/{legacy/ui/ui_settings/routes => core/server/ui_settings}/integration_tests/lib/assert.ts (100%) rename src/{legacy/ui/ui_settings/routes => core/server/ui_settings}/integration_tests/lib/chance.ts (100%) rename src/{legacy/ui/ui_settings/routes => core/server/ui_settings}/integration_tests/lib/index.ts (100%) rename src/{legacy/ui/ui_settings/routes => core/server/ui_settings}/integration_tests/lib/servers.ts (83%) create mode 100644 src/core/server/ui_settings/routes/delete.ts create mode 100644 src/core/server/ui_settings/routes/get.ts create mode 100644 src/core/server/ui_settings/routes/index.ts create mode 100644 src/core/server/ui_settings/routes/set.ts create mode 100644 src/core/server/ui_settings/routes/set_many.ts create mode 100644 src/core/server/ui_settings/types.ts create mode 100644 src/core/server/ui_settings/ui_settings_errors.ts delete mode 100644 src/legacy/ui/ui_settings/routes/set.ts create mode 100644 test/plugin_functional/plugins/ui_settings_plugin/kibana.json create mode 100644 test/plugin_functional/plugins/ui_settings_plugin/package.json rename {src/legacy/ui/ui_settings/routes => test/plugin_functional/plugins/ui_settings_plugin/public}/index.ts (84%) rename src/legacy/ui/ui_settings/routes/delete.ts => test/plugin_functional/plugins/ui_settings_plugin/public/plugin.tsx (63%) rename test/plugin_functional/{services/index.js => plugins/ui_settings_plugin/server/index.ts} (86%) rename src/legacy/ui/ui_settings/routes/set_many.ts => test/plugin_functional/plugins/ui_settings_plugin/server/plugin.ts (53%) create mode 100644 test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json rename src/legacy/ui/ui_settings/routes/get.ts => test/plugin_functional/services/index.ts (68%) rename test/plugin_functional/services/{supertest.js => supertest.ts} (87%) rename test/plugin_functional/test_suites/core_plugins/{applications.js => applications.ts} (86%) rename test/plugin_functional/test_suites/core_plugins/{index.js => index.ts} (81%) rename test/plugin_functional/test_suites/core_plugins/{legacy_plugins.js => legacy_plugins.ts} (84%) rename test/plugin_functional/test_suites/core_plugins/{server_plugins.js => server_plugins.ts} (67%) rename test/plugin_functional/test_suites/core_plugins/{ui_plugins.js => ui_plugins.ts} (84%) create mode 100644 test/plugin_functional/test_suites/core_plugins/ui_settings.ts diff --git a/docs/development/core/public/kibana-plugin-public.ianonymouspaths.isanonymous.md b/docs/development/core/public/kibana-plugin-public.ianonymouspaths.isanonymous.md index 92a87668b6ef..d6be78e1e725 100644 --- a/docs/development/core/public/kibana-plugin-public.ianonymouspaths.isanonymous.md +++ b/docs/development/core/public/kibana-plugin-public.ianonymouspaths.isanonymous.md @@ -4,7 +4,7 @@ ## IAnonymousPaths.isAnonymous() method -Determines whether the provided path doesn't require authentication +Determines whether the provided path doesn't require authentication. `path` should include the current basePath. Signature: diff --git a/docs/development/core/public/kibana-plugin-public.ianonymouspaths.md b/docs/development/core/public/kibana-plugin-public.ianonymouspaths.md index 3e5caf49695c..1290df28780c 100644 --- a/docs/development/core/public/kibana-plugin-public.ianonymouspaths.md +++ b/docs/development/core/public/kibana-plugin-public.ianonymouspaths.md @@ -16,6 +16,6 @@ export interface IAnonymousPaths | Method | Description | | --- | --- | -| [isAnonymous(path)](./kibana-plugin-public.ianonymouspaths.isanonymous.md) | Determines whether the provided path doesn't require authentication | -| [register(path)](./kibana-plugin-public.ianonymouspaths.register.md) | Register path as not requiring authentication | +| [isAnonymous(path)](./kibana-plugin-public.ianonymouspaths.isanonymous.md) | Determines whether the provided path doesn't require authentication. path should include the current basePath. | +| [register(path)](./kibana-plugin-public.ianonymouspaths.register.md) | Register path as not requiring authentication. path should not include the current basePath. | diff --git a/docs/development/core/public/kibana-plugin-public.ianonymouspaths.register.md b/docs/development/core/public/kibana-plugin-public.ianonymouspaths.register.md index 88c615da0515..3ab9bf438aa1 100644 --- a/docs/development/core/public/kibana-plugin-public.ianonymouspaths.register.md +++ b/docs/development/core/public/kibana-plugin-public.ianonymouspaths.register.md @@ -4,7 +4,7 @@ ## IAnonymousPaths.register() method -Register `path` as not requiring authentication +Register `path` as not requiring authentication. `path` should not include the current basePath. Signature: diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 253e50f0f2c2..df0b963e2b62 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -121,5 +121,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) | [UiSettingsClient](./kibana-plugin-public.uisettingsclient.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) | diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclient.getall.md b/docs/development/core/public/kibana-plugin-public.uisettingsclient.getall.md index 5eaf06e7dd68..06daf8e8151c 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclient.getall.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclient.getall.md @@ -9,9 +9,9 @@ Gets the metadata about all uiSettings, including the type, default value, and u Signature: ```typescript -getAll(): UiSettingsState; +getAll(): Record>; ``` Returns: -`UiSettingsState` +`Record>` diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsclientcontract.md b/docs/development/core/public/kibana-plugin-public.uisettingsclientcontract.md index 6eda1fd3274c..7173386d8826 100644 --- a/docs/development/core/public/kibana-plugin-public.uisettingsclientcontract.md +++ b/docs/development/core/public/kibana-plugin-public.uisettingsclientcontract.md @@ -4,7 +4,7 @@ ## UiSettingsClientContract type -[UiSettingsClient](./kibana-plugin-public.uisettingsclient.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) Signature: diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.md b/docs/development/core/server/kibana-plugin-server.coresetup.md index a6dda69fd154..c51459bc41a4 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.md @@ -19,4 +19,5 @@ export interface CoreSetup | [context](./kibana-plugin-server.coresetup.context.md) | ContextSetup | [ContextSetup](./kibana-plugin-server.contextsetup.md) | | [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | ElasticsearchServiceSetup | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | | [http](./kibana-plugin-server.coresetup.http.md) | HttpServiceSetup | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | +| [uiSettings](./kibana-plugin-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.uisettings.md b/docs/development/core/server/kibana-plugin-server.coresetup.uisettings.md new file mode 100644 index 000000000000..54120d7c3fa8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.coresetup.uisettings.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) > [uiSettings](./kibana-plugin-server.coresetup.uisettings.md) + +## CoreSetup.uiSettings property + +[UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) + +Signature: + +```typescript +uiSettings: UiSettingsServiceSetup; +``` diff --git a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getdefaults.md b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getdefaults.md deleted file mode 100644 index 29faa6d945b4..000000000000 --- a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getdefaults.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) > [getDefaults](./kibana-plugin-server.iuisettingsclient.getdefaults.md) - -## IUiSettingsClient.getDefaults property - -Returns uiSettings default values [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) - -Signature: - -```typescript -getDefaults: () => Record; -``` diff --git a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getregistered.md b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getregistered.md new file mode 100644 index 000000000000..16ae4c3dd8b3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getregistered.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) > [getRegistered](./kibana-plugin-server.iuisettingsclient.getregistered.md) + +## IUiSettingsClient.getRegistered property + +Returns registered uiSettings values [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) + +Signature: + +```typescript +getRegistered: () => Readonly>; +``` 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 9a449b64ed5d..134039cfa91f 100644 --- a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getuserprovided.md +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getuserprovided.md @@ -9,8 +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 142f33d27c38..a4697ddbbb85 100644 --- a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.md +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.md @@ -4,7 +4,7 @@ ## IUiSettingsClient interface -Service that provides access to the UiSettings stored in elasticsearch. +Server-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. Signature: @@ -18,8 +18,8 @@ export interface IUiSettingsClient | --- | --- | --- | | [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. | -| [getDefaults](./kibana-plugin-server.iuisettingsclient.getdefaults.md) | () => Record<string, UiSettingsParams> | Returns uiSettings default values [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) | -| [getUserProvided](./kibana-plugin-server.iuisettingsclient.getuserprovided.md) | <T extends SavedObjectAttribute = any>() => Promise<Record<string, {
userValue?: T;
isOverridden?: boolean;
}>> | Retrieves a set of all uiSettings values set by the user. | +| [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. | | [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. | diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 5a8b702e0ec9..9907750b8742 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -63,7 +63,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) | A tiny abstraction for TCP socket. | | [IndexSettingsDeprecationInfo](./kibana-plugin-server.indexsettingsdeprecationinfo.md) | | | [IRouter](./kibana-plugin-server.irouter.md) | Registers route handlers for specified resource path and method. See [RouteConfig](./kibana-plugin-server.routeconfig.md) and [RequestHandler](./kibana-plugin-server.requesthandler.md) for more information about arguments to route registrations. | -| [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) | Service that provides access to the UiSettings stored in elasticsearch. | +| [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) | Server-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. | | [KibanaRequestRoute](./kibana-plugin-server.kibanarequestroute.md) | Request specific route information exposed to a handler. | | [LegacyRequest](./kibana-plugin-server.legacyrequest.md) | | | [LegacyServiceSetupDeps](./kibana-plugin-server.legacyservicesetupdeps.md) | | @@ -118,6 +118,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SessionStorageCookieOptions](./kibana-plugin-server.sessionstoragecookieoptions.md) | Configuration used to create HTTP session storage based on top of cookie mechanism. | | [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | SessionStorage factory to bind one to an incoming request | | [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) | UiSettings parameters defined by the plugins. | +| [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) | | +| [UserProvidedValues](./kibana-plugin-server.userprovidedvalues.md) | Describes the values explicitly set by user. | ## Variables diff --git a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md index e1ea358320b9..2d8b27ecb6c6 100644 --- a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md +++ b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md @@ -15,5 +15,8 @@ core: { dataClient: IScopedClusterClient; adminClient: IScopedClusterClient; }; + uiSettings: { + client: IUiSettingsClient; + }; }; ``` diff --git a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md index 37a40f98adef..c9fc80596efa 100644 --- a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md @@ -18,5 +18,5 @@ export interface RequestHandlerContext | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
};
elasticsearch: {
dataClient: IScopedClusterClient;
adminClient: IScopedClusterClient;
};
} | | +| [core](./kibana-plugin-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
};
elasticsearch: {
dataClient: IScopedClusterClient;
adminClient: IScopedClusterClient;
};
uiSettings: {
client: IUiSettingsClient;
};
} | | diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.category.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.category.md index 47aedbfbf281..6bf1b17dc947 100644 --- a/docs/development/core/server/kibana-plugin-server.uisettingsparams.category.md +++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.category.md @@ -9,5 +9,5 @@ used to group the configured setting in the UI Signature: ```typescript -category: string[]; +category?: string[]; ``` diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.description.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.description.md index 8d8887285ae2..6a203629f542 100644 --- a/docs/development/core/server/kibana-plugin-server.uisettingsparams.description.md +++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.description.md @@ -9,5 +9,5 @@ description provided to a user in UI Signature: ```typescript -description: string; +description?: string; ``` diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.md index 275111c05eff..a38499e8f37d 100644 --- a/docs/development/core/server/kibana-plugin-server.uisettingsparams.md +++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.md @@ -20,7 +20,7 @@ export interface UiSettingsParams | [description](./kibana-plugin-server.uisettingsparams.description.md) | string | description provided to a user in UI | | [name](./kibana-plugin-server.uisettingsparams.name.md) | string | title in the UI | | [optionLabels](./kibana-plugin-server.uisettingsparams.optionlabels.md) | Record<string, string> | text labels for 'select' type UI element | -| [options](./kibana-plugin-server.uisettingsparams.options.md) | string[] | a range of valid values | +| [options](./kibana-plugin-server.uisettingsparams.options.md) | string[] | array of permitted values for this setting | | [readonly](./kibana-plugin-server.uisettingsparams.readonly.md) | boolean | a flag indicating that value cannot be changed | | [requiresPageReload](./kibana-plugin-server.uisettingsparams.requirespagereload.md) | boolean | a flag indicating whether new value applying requires page reloading | | [type](./kibana-plugin-server.uisettingsparams.type.md) | UiSettingsType | defines a type of UI element [UiSettingsType](./kibana-plugin-server.uisettingstype.md) | diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.name.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.name.md index 2b414eefffed..07905ca7de20 100644 --- a/docs/development/core/server/kibana-plugin-server.uisettingsparams.name.md +++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.name.md @@ -9,5 +9,5 @@ title in the UI Signature: ```typescript -name: string; +name?: string; ``` diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.options.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.options.md index 71eecdfabc4a..2220feab74ff 100644 --- a/docs/development/core/server/kibana-plugin-server.uisettingsparams.options.md +++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.options.md @@ -4,7 +4,7 @@ ## UiSettingsParams.options property -a range of valid values +array of permitted values for this setting Signature: diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.value.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.value.md index 455756899ecf..397498ccf5c1 100644 --- a/docs/development/core/server/kibana-plugin-server.uisettingsparams.value.md +++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.value.md @@ -9,5 +9,5 @@ default value to fall back to if a user doesn't provide any Signature: ```typescript -value: SavedObjectAttribute; +value?: SavedObjectAttribute; ``` diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsservicesetup.md b/docs/development/core/server/kibana-plugin-server.uisettingsservicesetup.md new file mode 100644 index 000000000000..8dde78f633d8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.uisettingsservicesetup.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) + +## UiSettingsServiceSetup interface + + +Signature: + +```typescript +export interface UiSettingsServiceSetup +``` + +## Methods + +| Method | Description | +| --- | --- | +| [register(settings)](./kibana-plugin-server.uisettingsservicesetup.register.md) | Sets settings with default values for the uiSettings. | + diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsservicesetup.register.md b/docs/development/core/server/kibana-plugin-server.uisettingsservicesetup.register.md new file mode 100644 index 000000000000..8091a7cec44a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.uisettingsservicesetup.register.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) > [register](./kibana-plugin-server.uisettingsservicesetup.register.md) + +## UiSettingsServiceSetup.register() method + +Sets settings with default values for the uiSettings. + +Signature: + +```typescript +register(settings: Record): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| settings | Record<string, UiSettingsParams> | | + +Returns: + +`void` + +## Example + +setup(core: CoreSetup){ core.uiSettings.register(\[{ foo: { name: i18n.translate('my foo settings'), value: true, description: 'add some awesomeness', }, }\]); } + diff --git a/docs/development/core/server/kibana-plugin-server.userprovidedvalues.isoverridden.md b/docs/development/core/server/kibana-plugin-server.userprovidedvalues.isoverridden.md new file mode 100644 index 000000000000..01e04b490595 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.userprovidedvalues.isoverridden.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UserProvidedValues](./kibana-plugin-server.userprovidedvalues.md) > [isOverridden](./kibana-plugin-server.userprovidedvalues.isoverridden.md) + +## UserProvidedValues.isOverridden property + +Signature: + +```typescript +isOverridden?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.userprovidedvalues.md b/docs/development/core/server/kibana-plugin-server.userprovidedvalues.md new file mode 100644 index 000000000000..7b2114404d7f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.userprovidedvalues.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UserProvidedValues](./kibana-plugin-server.userprovidedvalues.md) + +## UserProvidedValues interface + +Describes the values explicitly set by user. + +Signature: + +```typescript +export interface UserProvidedValues +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [isOverridden](./kibana-plugin-server.userprovidedvalues.isoverridden.md) | boolean | | +| [userValue](./kibana-plugin-server.userprovidedvalues.uservalue.md) | T | | + diff --git a/docs/development/core/server/kibana-plugin-server.userprovidedvalues.uservalue.md b/docs/development/core/server/kibana-plugin-server.userprovidedvalues.uservalue.md new file mode 100644 index 000000000000..59d25651b769 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.userprovidedvalues.uservalue.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UserProvidedValues](./kibana-plugin-server.userprovidedvalues.md) > [userValue](./kibana-plugin-server.userprovidedvalues.uservalue.md) + +## UserProvidedValues.userValue property + +Signature: + +```typescript +userValue?: T; +``` diff --git a/package.json b/package.json index 28b0659074e4..38fca64abbb3 100644 --- a/package.json +++ b/package.json @@ -337,6 +337,7 @@ "@types/strip-ansi": "^3.0.0", "@types/styled-components": "^3.0.2", "@types/supertest": "^2.0.5", + "@types/supertest-as-promised": "^2.0.38", "@types/type-detect": "^4.0.1", "@types/uuid": "^3.4.4", "@types/vinyl-fs": "^2.4.11", diff --git a/renovate.json5 b/renovate.json5 index 01cda357cd98..29f98b9b3ec2 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -567,6 +567,14 @@ '@types/supertest', ], }, + { + groupSlug: 'supertest-as-promised', + groupName: 'supertest-as-promised related packages', + packageNames: [ + 'supertest-as-promised', + '@types/supertest-as-promised', + ], + }, { groupSlug: 'type-detect', groupName: 'type-detect related packages', diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 478285a70771..a5342aaa48b7 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -19,8 +19,12 @@ import { get } from 'lodash'; import { DiscoveredPlugin, PluginName } from '../../server'; -import { EnvironmentMode, PackageInfo } from '../../server/types'; -import { UiSettingsState } from '../ui_settings'; +import { + EnvironmentMode, + PackageInfo, + UiSettingsParams, + UserProvidedValues, +} from '../../server/types'; import { deepFreeze } from '../../utils/'; import { Capabilities } from '..'; @@ -69,8 +73,8 @@ export interface InjectedMetadataParams { serverName: string; devMode: boolean; uiSettings: { - defaults: UiSettingsState; - user?: UiSettingsState; + defaults: Record; + user?: Record; }; }; }; @@ -179,8 +183,8 @@ export interface InjectedMetadataSetup { serverName: string; devMode: boolean; uiSettings: { - defaults: UiSettingsState; - user?: UiSettingsState | undefined; + defaults: Record; + user?: Record | undefined; }; }; getInjectedVar: (name: string, defaultValue?: any) => unknown; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 416fb13cbb73..a596ea394abd 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -11,6 +11,8 @@ import React from 'react'; import * as Rx from 'rxjs'; import { ShallowPromise } from '@kbn/utility-types'; import { EuiGlobalToastListToast as Toast } from '@elastic/eui'; +import { UiSettingsParams as UiSettingsParams_2 } from 'src/core/server/types'; +import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/types'; // @public export interface App extends AppBase { @@ -957,7 +959,7 @@ export class UiSettingsClient { constructor(params: UiSettingsClientParams); get$(key: string, defaultOverride?: any): Rx.Observable; get(key: string, defaultOverride?: any): any; - getAll(): UiSettingsState; + getAll(): Record>; getSaved$(): Rx.Observable<{ key: string; newValue: any; @@ -979,16 +981,13 @@ export class UiSettingsClient { stop(): void; } -// @public (undocumented) +// @public export type UiSettingsClientContract = PublicMethodsOf; // @public (undocumented) export interface UiSettingsState { - // Warning: (ae-forgotten-export) The symbol "InjectedUiSettingsDefault" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "InjectedUiSettingsUser" needs to be exported by the entry point index.d.ts - // // (undocumented) - [key: string]: InjectedUiSettingsDefault & InjectedUiSettingsUser; + [key: string]: UiSettingsParams_2 & UserProvidedValues_2; } diff --git a/src/core/public/ui_settings/types.ts b/src/core/public/ui_settings/types.ts index d1cf25c3e9e6..24e87eb04f02 100644 --- a/src/core/public/ui_settings/types.ts +++ b/src/core/public/ui_settings/types.ts @@ -17,28 +17,9 @@ * under the License. */ -// properties that come from legacyInjectedMetadata.uiSettings.defaults -interface InjectedUiSettingsDefault { - name?: string; - value?: any; - description?: string; - category?: string[]; - type?: string; - readOnly?: boolean; - options?: string[] | { [key: string]: any }; - /** - * Whether a change in that setting will only take affect after a page reload. - */ - requiresPageReload?: boolean; -} - -// properties that come from legacyInjectedMetadata.uiSettings.user -interface InjectedUiSettingsUser { - userValue?: any; - isOverridden?: boolean; -} +import { UiSettingsParams, UserProvidedValues } from 'src/core/server/types'; /** @public */ export interface UiSettingsState { - [key: string]: InjectedUiSettingsDefault & InjectedUiSettingsUser; + [key: string]: UiSettingsParams & UserProvidedValues; } diff --git a/src/core/public/ui_settings/ui_settings_client.ts b/src/core/public/ui_settings/ui_settings_client.ts index 3083851dd453..c3190847130d 100644 --- a/src/core/public/ui_settings/ui_settings_client.ts +++ b/src/core/public/ui_settings/ui_settings_client.ts @@ -21,18 +21,25 @@ import { cloneDeep, defaultsDeep } from 'lodash'; import * as Rx from 'rxjs'; import { filter, map } from 'rxjs/operators'; +import { UiSettingsParams, UserProvidedValues } from 'src/core/server/types'; import { UiSettingsState } from './types'; + import { UiSettingsApi } from './ui_settings_api'; /** @public */ interface UiSettingsClientParams { api: UiSettingsApi; - defaults: UiSettingsState; + defaults: Record; initialSettings?: UiSettingsState; } /** + * 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; @@ -44,8 +51,8 @@ export class UiSettingsClient { private readonly updateErrors$ = new Rx.Subject(); private readonly api: UiSettingsApi; - private readonly defaults: UiSettingsState; - private cache: UiSettingsState; + private readonly defaults: Record; + private cache: Record; constructor(params: UiSettingsClientParams) { this.api = params.api; diff --git a/src/core/server/http/router/response_adapter.ts b/src/core/server/http/router/response_adapter.ts index fe6cb32d2177..e5dabc99f444 100644 --- a/src/core/server/http/router/response_adapter.ts +++ b/src/core/server/http/router/response_adapter.ts @@ -120,7 +120,11 @@ export class HapiResponseAdapter { }); error.output.payload.message = getErrorMessage(payload); - error.output.payload.attributes = getErrorAttributes(payload); + + const attributes = getErrorAttributes(payload); + if (attributes) { + error.output.payload.attributes = attributes; + } const headers = kibanaResponse.options.headers; if (headers) { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index c3f63c7c26e9..35e83da4ef30 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -49,7 +49,11 @@ import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plug import { ContextSetup } from './context'; import { SavedObjectsServiceStart } from './saved_objects'; -import { InternalUiSettingsServiceSetup } from './ui_settings'; +import { + InternalUiSettingsServiceSetup, + IUiSettingsClient, + UiSettingsServiceSetup, +} from './ui_settings'; import { SavedObjectsClientContract } from './saved_objects/types'; export { bootstrap } from './bootstrap'; @@ -175,6 +179,8 @@ export { UiSettingsParams, InternalUiSettingsServiceSetup, UiSettingsType, + UiSettingsServiceSetup, + UserProvidedValues, } from './ui_settings'; export { RecursiveReadonly } from '../utils'; @@ -216,6 +222,9 @@ export interface RequestHandlerContext { dataClient: IScopedClusterClient; adminClient: IScopedClusterClient; }; + uiSettings: { + client: IUiSettingsClient; + }; }; } @@ -231,6 +240,8 @@ export interface CoreSetup { elasticsearch: ElasticsearchServiceSetup; /** {@link HttpServiceSetup} */ http: HttpServiceSetup; + /** {@link UiSettingsServiceSetup} */ + uiSettings: UiSettingsServiceSetup; } /** diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 590bd192e3de..e2aefd846d97 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -72,7 +72,7 @@ beforeEach(() => { core: { context: contextServiceMock.createSetupContract(), elasticsearch: { legacy: {} } as any, - uiSettings: uiSettingsServiceMock.createSetup(), + uiSettings: uiSettingsServiceMock.createSetupContract(), http: { ...httpServiceMock.createSetupContract(), auth: { diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index dd3ee3db8915..b7c55a8af7c1 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -248,6 +248,9 @@ export class LegacyService implements CoreService { basePath: setupDeps.core.http.basePath, isTlsEnabled: setupDeps.core.http.isTlsEnabled, }, + uiSettings: { + register: setupDeps.core.uiSettings.register, + }, }; const coreStart: CoreStart = {}; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index deb5984564db..b51d5302e327 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -22,6 +22,7 @@ import { loggingServiceMock } from './logging/logging_service.mock'; import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock'; import { httpServiceMock } from './http/http_service.mock'; import { contextServiceMock } from './context/context_service.mock'; +import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export { httpServerMock } from './http/http_server.mocks'; export { sessionStorageMock } from './http/cookie_session_storage.mocks'; @@ -79,10 +80,14 @@ function createCoreSetupMock() { }; httpMock.createRouter.mockImplementation(() => httpService.createRouter('')); + const uiSettingsMock = { + register: uiSettingsServiceMock.createSetupContract().register, + }; const mock: MockedKeys = { context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createSetupContract(), http: httpMock, + uiSettings: uiSettingsMock, }; return mock; @@ -94,8 +99,19 @@ function createCoreStartMock() { return mock; } +function createInternalCoreSetupMock() { + const setupDeps = { + context: contextServiceMock.createSetupContract(), + elasticsearch: elasticsearchServiceMock.createSetupContract(), + http: httpServiceMock.createSetupContract(), + uiSettings: uiSettingsServiceMock.createSetupContract(), + }; + return setupDeps; +} + export const coreMock = { createSetup: createCoreSetupMock, createStart: createCoreStartMock, + createInternalSetup: createInternalCoreSetupMock, createPluginInitializerContext: pluginInitializerContextMock, }; diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts index 5c57a5fa2c8d..e457f01a1941 100644 --- a/src/core/server/plugins/plugin.test.ts +++ b/src/core/server/plugins/plugin.test.ts @@ -24,15 +24,13 @@ import { schema } from '@kbn/config-schema'; import { Env } from '../config'; import { getEnvOptions } from '../config/__mocks__/env'; import { CoreContext } from '../core_context'; +import { coreMock } from '../mocks'; import { configServiceMock } from '../config/config_service.mock'; -import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock'; -import { httpServiceMock } from '../http/http_service.mock'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { PluginWrapper } from './plugin'; import { PluginManifest } from './types'; import { createPluginInitializerContext, createPluginSetupContext } from './plugin_context'; -import { contextServiceMock } from '../context/context_service.mock'; const mockPluginInitializer = jest.fn(); const logger = loggingServiceMock.create(); @@ -68,11 +66,7 @@ configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true })); let coreId: symbol; let env: Env; let coreContext: CoreContext; -const setupDeps = { - context: contextServiceMock.createSetupContract(), - elasticsearch: elasticsearchServiceMock.createSetupContract(), - http: httpServiceMock.createSetupContract(), -}; +const setupDeps = coreMock.createInternalSetup(); beforeEach(() => { coreId = Symbol('core'); env = Env.createDefault(getEnvOptions()); diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index edcafbb9a3dc..9885a572ad8c 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -123,6 +123,9 @@ export function createPluginSetupContext( basePath: deps.http.basePath, isTlsEnabled: deps.http.isTlsEnabled, }, + uiSettings: { + register: deps.uiSettings.register, + }, }; } diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index d25f9087432b..0b3bc0759463 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -25,15 +25,13 @@ import { schema } from '@kbn/config-schema'; import { Config, ConfigPath, ConfigService, Env, ObjectToConfigAdapter } from '../config'; import { getEnvOptions } from '../config/__mocks__/env'; -import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock'; -import { httpServiceMock } from '../http/http_service.mock'; +import { coreMock } from '../mocks'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { PluginDiscoveryError } from './discovery'; import { PluginWrapper } from './plugin'; import { PluginsService } from './plugins_service'; import { PluginsSystem } from './plugins_system'; import { config } from './plugins_config'; -import { contextServiceMock } from '../context/context_service.mock'; const MockPluginsSystem: jest.Mock = PluginsSystem as any; @@ -42,11 +40,7 @@ let configService: ConfigService; let coreId: symbol; let env: Env; let mockPluginSystem: jest.Mocked; -const setupDeps = { - context: contextServiceMock.createSetupContract(), - elasticsearch: elasticsearchServiceMock.createSetupContract(), - http: httpServiceMock.createSetupContract(), -}; +const setupDeps = coreMock.createInternalSetup(); const logger = loggingServiceMock.create(); ['path-1', 'path-2', 'path-3', 'path-4', 'path-5'].forEach(path => { diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 92b537deae33..2964e34c370b 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -21,15 +21,14 @@ import { Observable } from 'rxjs'; import { filter, first, map, mergeMap, tap, toArray } from 'rxjs/operators'; import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; -import { InternalElasticsearchServiceSetup } from '../elasticsearch'; -import { InternalHttpServiceSetup } from '../http'; + import { Logger } from '../logging'; import { discover, PluginDiscoveryError, PluginDiscoveryErrorType } from './discovery'; import { PluginWrapper } from './plugin'; import { DiscoveredPlugin, DiscoveredPluginInternal, PluginName } from './types'; import { PluginsConfig, PluginsConfigType } from './plugins_config'; import { PluginsSystem } from './plugins_system'; -import { ContextSetup } from '../context'; +import { InternalCoreSetup } from '..'; /** @public */ export interface PluginsServiceSetup { @@ -46,11 +45,7 @@ export interface PluginsServiceStart { } /** @internal */ -export interface PluginsServiceSetupDeps { - context: ContextSetup; - elasticsearch: InternalElasticsearchServiceSetup; - http: InternalHttpServiceSetup; -} +export type PluginsServiceSetupDeps = InternalCoreSetup; /** @internal */ export interface PluginsServiceStartDeps {} // eslint-disable-line @typescript-eslint/no-empty-interface diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index f67bd371ff56..efd53681d6cd 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -28,13 +28,13 @@ import { Env } from '../config'; import { getEnvOptions } from '../config/__mocks__/env'; import { CoreContext } from '../core_context'; import { configServiceMock } from '../config/config_service.mock'; -import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock'; -import { httpServiceMock } from '../http/http_service.mock'; import { loggingServiceMock } from '../logging/logging_service.mock'; + import { PluginWrapper } from './plugin'; import { PluginName } from './types'; import { PluginsSystem } from './plugins_system'; -import { contextServiceMock } from '../context/context_service.mock'; + +import { coreMock } from '../mocks'; const logger = loggingServiceMock.create(); function createPlugin( @@ -68,11 +68,8 @@ const configService = configServiceMock.create(); configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true })); let env: Env; let coreContext: CoreContext; -const setupDeps = { - context: contextServiceMock.createSetupContract(), - elasticsearch: elasticsearchServiceMock.createSetupContract(), - http: httpServiceMock.createSetupContract(), -}; +const setupDeps = coreMock.createInternalSetup(); + beforeEach(() => { env = Env.createDefault(getEnvOptions()); diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index e7a4cdc0174b..14943fc96f26 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -511,6 +511,8 @@ export interface CoreSetup { elasticsearch: ElasticsearchServiceSetup; // (undocumented) http: HttpServiceSetup; + // (undocumented) + uiSettings: UiSettingsServiceSetup; } // @public @@ -737,7 +739,7 @@ export interface InternalCoreStart { // @internal (undocumented) export interface InternalUiSettingsServiceSetup { asScopedToClient(savedObjectsClient: SavedObjectsClientContract): IUiSettingsClient; - setDefaults(values: Record): void; + register(settings: Record): void; } // @public @@ -763,11 +765,8 @@ export type IScopedClusterClient = Pick(key: string) => Promise; getAll: () => Promise>; - getDefaults: () => Record; - getUserProvided: () => Promise>; + getRegistered: () => Readonly>; + getUserProvided: () => Promise>>; isOverridden: (key: string) => boolean; remove: (key: string) => Promise; removeMany: (keys: string[]) => Promise; @@ -1074,6 +1073,9 @@ export interface RequestHandlerContext { dataClient: IScopedClusterClient; adminClient: IScopedClusterClient; }; + uiSettings: { + client: IUiSettingsClient; + }; }; } @@ -1610,24 +1612,37 @@ export interface SessionStorageFactory { // @public export interface UiSettingsParams { - category: string[]; - description: string; - name: string; + category?: string[]; + description?: string; + name?: string; optionLabels?: Record; options?: string[]; readonly?: boolean; requiresPageReload?: boolean; type?: UiSettingsType; - value: SavedObjectAttribute; + value?: SavedObjectAttribute; +} + +// @public (undocumented) +export interface UiSettingsServiceSetup { + register(settings: Record): void; } // @public export type UiSettingsType = 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string'; +// @public +export interface UserProvidedValues { + // (undocumented) + isOverridden?: boolean; + // (undocumented) + userValue?: T; +} + // Warnings were encountered during analysis: // // src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/plugins_service.ts:39:5 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/plugins_service.ts:38:5 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 49136c8e0976..46974e204c7a 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -21,7 +21,7 @@ import { take } from 'rxjs/operators'; import { Type } from '@kbn/config-schema'; import { ConfigService, Env, Config, ConfigPath } from './config'; -import { ElasticsearchService, ElasticsearchServiceSetup } from './elasticsearch'; +import { ElasticsearchService } from './elasticsearch'; import { HttpService, InternalHttpServiceSetup } from './http'; import { LegacyService } from './legacy'; import { Logger, LoggerFactory } from './logging'; @@ -39,7 +39,7 @@ import { config as uiSettingsConfig } from './ui_settings'; import { mapToObject } from '../utils/'; import { ContextService } from './context'; import { SavedObjectsServiceSetup } from './saved_objects/saved_objects_service'; -import { RequestHandlerContext } from '.'; +import { RequestHandlerContext, InternalCoreSetup } from '.'; const coreId = Symbol('core'); @@ -102,7 +102,7 @@ export class Server { http: httpSetup, }); - const coreSetup = { + const coreSetup: InternalCoreSetup = { context: contextServiceSetup, elasticsearch: elasticsearchServiceSetup, http: httpSetup, @@ -121,7 +121,7 @@ export class Server { legacy: legacySetup, }); - this.registerCoreContext({ ...coreSetup, savedObjects: savedObjectsSetup }); + this.registerCoreContext(coreSetup, savedObjectsSetup); return coreSetup; } @@ -163,27 +163,31 @@ export class Server { ); } - private registerCoreContext(coreSetup: { - http: InternalHttpServiceSetup; - elasticsearch: ElasticsearchServiceSetup; - savedObjects: SavedObjectsServiceSetup; - }) { + private registerCoreContext( + coreSetup: InternalCoreSetup, + savedObjects: SavedObjectsServiceSetup + ) { coreSetup.http.registerRouteHandlerContext( coreId, 'core', async (context, req): Promise => { const adminClient = await coreSetup.elasticsearch.adminClient$.pipe(take(1)).toPromise(); const dataClient = await coreSetup.elasticsearch.dataClient$.pipe(take(1)).toPromise(); + const savedObjectsClient = savedObjects.clientProvider.getClient(req); + return { savedObjects: { // Note: the client provider doesn't support new ES clients // emitted from adminClient$ - client: coreSetup.savedObjects.clientProvider.getClient(req), + client: savedObjectsClient, }, elasticsearch: { adminClient: adminClient.asScoped(req), dataClient: dataClient.asScoped(req), }, + uiSettings: { + client: coreSetup.uiSettings.asScopedToClient(savedObjectsClient), + }, }; } ); diff --git a/src/core/server/types.ts b/src/core/server/types.ts index 46c70a91721b..4878fb9ccae1 100644 --- a/src/core/server/types.ts +++ b/src/core/server/types.ts @@ -20,4 +20,5 @@ /** This module is intended for consumption by public to avoid import issues with server-side code */ export { PluginOpaqueId } from './plugins/types'; export * from './saved_objects/types'; +export * from './ui_settings/types'; export { EnvironmentMode, PackageInfo } from './config/types'; diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts index 5f7e91536587..65b8792532ac 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts @@ -20,6 +20,8 @@ import sinon from 'sinon'; import Chance from 'chance'; +import { SavedObjectsErrorHelpers } from '../../saved_objects'; + import { loggingServiceMock } from '../../logging/logging_service.mock'; import * as getUpgradeableConfigNS from './get_upgradeable_config'; import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; @@ -50,6 +52,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { version, buildNum, log: logger.get(), + handleWriteErrors: false, ...options, }); @@ -173,85 +176,64 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { }); }); - describe('onWriteError()', () => { - it('is called with error and attributes when savedObjectsClient.create rejects', async () => { - const { run, savedObjectsClient } = setup(); - - const error = new Error('foo'); - savedObjectsClient.create.callsFake(async () => { - throw error; - }); - - const onWriteError = sinon.stub(); - await run({ onWriteError }); - sinon.assert.calledOnce(onWriteError); - sinon.assert.calledWithExactly(onWriteError, error, { - buildNum, - }); - }); - - it('resolves with the return value of onWriteError()', async () => { - const { run, savedObjectsClient } = setup(); - - savedObjectsClient.create.callsFake(async () => { - throw new Error('foo'); - }); - - const result = await run({ onWriteError: () => 123 }); - expect(result).toBe(123); - }); - - it('rejects with the error from onWriteError() if it rejects', async () => { - const { run, savedObjectsClient } = setup(); - - savedObjectsClient.create.callsFake(async () => { - throw new Error('foo'); - }); - - try { - await run({ - onWriteError: (error: Error) => Promise.reject(new Error(`${error.message} bar`)), + describe('handleWriteErrors', () => { + describe('handleWriteErrors: false', () => { + it('throws write errors', async () => { + const { run, savedObjectsClient } = setup(); + const error = new Error('foo'); + savedObjectsClient.create.callsFake(async () => { + throw error; }); - throw new Error('expected run() to reject'); - } catch (error) { - expect(error.message).toBe('foo bar'); - } + + await expect(run({ handleWriteErrors: false })).rejects.toThrowError(error); + }); }); + describe('handleWriteErrors:true', () => { + it('returns undefined for ConflictError', async () => { + const { run, savedObjectsClient } = setup(); + const error = new Error('foo'); + savedObjectsClient.create.throws(SavedObjectsErrorHelpers.decorateConflictError(error)); - it('rejects with the error from onWriteError() if it throws sync', async () => { - const { run, savedObjectsClient } = setup(); - - savedObjectsClient.create.callsFake(async () => { - throw new Error('foo'); + expect(await run({ handleWriteErrors: true })).toBe(undefined); }); - try { - await run({ - onWriteError: (error: Error) => { - throw new Error(`${error.message} bar`); - }, + it('returns config attributes for NotAuthorizedError', async () => { + const { run, savedObjectsClient } = setup(); + const error = new Error('foo'); + savedObjectsClient.create.throws( + SavedObjectsErrorHelpers.decorateNotAuthorizedError(error) + ); + + expect(await run({ handleWriteErrors: true })).toEqual({ + buildNum, }); - throw new Error('expected run() to reject'); - } catch (error) { - expect(error.message).toBe('foo bar'); - } - }); - - it('rejects with the writeError if onWriteError() is undefined', async () => { - const { run, savedObjectsClient } = setup(); - - savedObjectsClient.create.callsFake(async () => { - throw new Error('foo'); }); - try { - await run({ - onWriteError: undefined, + it('returns config attributes for ForbiddenError', async () => { + const { run, savedObjectsClient } = setup(); + const error = new Error('foo'); + savedObjectsClient.create.throws(SavedObjectsErrorHelpers.decorateForbiddenError(error)); + + expect(await run({ handleWriteErrors: true })).toEqual({ + buildNum, }); - throw new Error('expected run() to reject'); - } catch (error) { - expect(error.message).toBe('foo'); - } + }); + + it('throws error for other SavedObjects exceptions', async () => { + const { run, savedObjectsClient } = setup(); + const error = new Error('foo'); + savedObjectsClient.create.throws(SavedObjectsErrorHelpers.decorateGeneralError(error)); + + await expect(run({ handleWriteErrors: true })).rejects.toThrowError(error); + }); + + it('throws error for all other exceptions', async () => { + const { run, savedObjectsClient } = setup(); + const error = new Error('foo'); + savedObjectsClient.create.throws(error); + + await expect(run({ handleWriteErrors: true })).rejects.toThrowError(error); + }); }); }); }); 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 1655297adb6c..809e15248b5b 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 @@ -20,6 +20,7 @@ import { defaults } from 'lodash'; import { SavedObjectsClientContract, SavedObjectAttribute } from '../../saved_objects/types'; +import { SavedObjectsErrorHelpers } from '../../saved_objects/'; import { Logger } from '../../logging'; import { getUpgradeableConfig } from './get_upgradeable_config'; @@ -29,15 +30,13 @@ interface Options { version: string; buildNum: number; log: Logger; - onWriteError?: ( - error: Error, - attributes: Record - ) => Record | undefined; + handleWriteErrors: boolean; } + export async function createOrUpgradeSavedConfig( options: Options ): Promise | undefined> { - const { savedObjectsClient, version, buildNum, log, onWriteError } = options; + const { savedObjectsClient, version, buildNum, log, handleWriteErrors } = options; // try to find an older config we can upgrade const upgradeableConfig = await getUpgradeableConfig({ @@ -52,8 +51,17 @@ export async function createOrUpgradeSavedConfig { + it('finds saved objects with type "config"', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [{ id: '7.5.0' }], + } as any); + + await getUpgradeableConfig({ savedObjectsClient, version: '7.5.0' }); + expect(savedObjectsClient.find.mock.calls[0][0].type).toBe('config'); + }); + + it('finds saved config with version < than Kibana version', async () => { + const savedConfig = { id: '7.4.0' }; + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [savedConfig], + } as any); + + const result = await getUpgradeableConfig({ savedObjectsClient, version: '7.5.0' }); + expect(result).toBe(savedConfig); + }); + + it('finds saved config with RC version === Kibana version', async () => { + const savedConfig = { id: '7.5.0-rc1' }; + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [savedConfig], + } as any); + + const result = await getUpgradeableConfig({ savedObjectsClient, version: '7.5.0' }); + expect(result).toBe(savedConfig); + }); + + it('does not find saved config with version === Kibana version', async () => { + const savedConfig = { id: '7.5.0' }; + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [savedConfig], + } as any); + + const result = await getUpgradeableConfig({ savedObjectsClient, version: '7.5.0' }); + expect(result).toBe(undefined); + }); + + it('does not find saved config with version > Kibana version', async () => { + const savedConfig = { id: '7.6.0' }; + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [savedConfig], + } as any); + + const result = await getUpgradeableConfig({ savedObjectsClient, version: '7.5.0' }); + expect(result).toBe(undefined); + }); + + it('handles empty config', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [], + } as any); + + const result = await getUpgradeableConfig({ savedObjectsClient, version: '7.5.0' }); + expect(result).toBe(undefined); + }); +}); diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts index 83f8ce03299f..9d52a339ccf9 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts @@ -18,28 +18,31 @@ */ import expect from '@kbn/expect'; -import { UnwrapPromise } from '@kbn/utility-types'; import { SavedObjectsClientContract } from 'src/core/server'; -import KbnServer from '../../../../../legacy/server/kbn_server'; -import { createTestServers } from '../../../../../test_utils/kbn_server'; +import { + createTestServers, + TestElasticsearchUtils, + TestKibanaUtils, + TestUtils, +} from '../../../../../test_utils/kbn_server'; import { createOrUpgradeSavedConfig } from '../create_or_upgrade_saved_config'; import { loggingServiceMock } from '../../../logging/logging_service.mock'; const logger = loggingServiceMock.create().get(); describe('createOrUpgradeSavedConfig()', () => { let savedObjectsClient: SavedObjectsClientContract; - let kbnServer: KbnServer; - let servers: ReturnType; - let esServer: UnwrapPromise>; - let kbn: UnwrapPromise>; + let servers: TestUtils; + let esServer: TestElasticsearchUtils; + let kbn: TestKibanaUtils; + + let kbnServer: TestKibanaUtils['kbnServer']; beforeAll(async function() { servers = createTestServers({ adjustTimeout: t => { jest.setTimeout(t); }, - settings: {}, }); esServer = await servers.startES(); kbn = await servers.startKibana(); @@ -90,6 +93,7 @@ describe('createOrUpgradeSavedConfig()', () => { version: '5.4.0', buildNum: 54099, log: logger, + handleWriteErrors: false, }); const config540 = await savedObjectsClient.get('config', '5.4.0'); @@ -116,6 +120,7 @@ describe('createOrUpgradeSavedConfig()', () => { version: '5.4.1', buildNum: 54199, log: logger, + handleWriteErrors: false, }); const config541 = await savedObjectsClient.get('config', '5.4.1'); @@ -142,6 +147,7 @@ describe('createOrUpgradeSavedConfig()', () => { version: '7.0.0-rc1', buildNum: 70010, log: logger, + handleWriteErrors: false, }); const config700rc1 = await savedObjectsClient.get('config', '7.0.0-rc1'); @@ -169,6 +175,7 @@ describe('createOrUpgradeSavedConfig()', () => { version: '7.0.0', buildNum: 70099, log: logger, + handleWriteErrors: false, }); const config700 = await savedObjectsClient.get('config', '7.0.0'); @@ -197,6 +204,7 @@ describe('createOrUpgradeSavedConfig()', () => { version: '6.2.3-rc1', buildNum: 62310, log: logger, + handleWriteErrors: false, }); const config623rc1 = await savedObjectsClient.get('config', '6.2.3-rc1'); diff --git a/src/core/server/ui_settings/index.ts b/src/core/server/ui_settings/index.ts index edd0bfc4f3a8..fd0a21bed4e1 100644 --- a/src/core/server/ui_settings/index.ts +++ b/src/core/server/ui_settings/index.ts @@ -17,16 +17,16 @@ * under the License. */ -export { - IUiSettingsClient, - UiSettingsClient, - UiSettingsServiceOptions, -} from './ui_settings_client'; +export { UiSettingsClient, UiSettingsServiceOptions } from './ui_settings_client'; export { config } from './ui_settings_config'; +export { UiSettingsService } from './ui_settings_service'; + export { + UiSettingsServiceSetup, + IUiSettingsClient, UiSettingsParams, - UiSettingsService, InternalUiSettingsServiceSetup, UiSettingsType, -} from './ui_settings_service'; + UserProvidedValues, +} from './types'; diff --git a/src/legacy/ui/ui_settings/routes/integration_tests/doc_exists.ts b/src/core/server/ui_settings/integration_tests/doc_exists.ts similarity index 100% rename from src/legacy/ui/ui_settings/routes/integration_tests/doc_exists.ts rename to src/core/server/ui_settings/integration_tests/doc_exists.ts diff --git a/src/legacy/ui/ui_settings/routes/integration_tests/doc_missing.ts b/src/core/server/ui_settings/integration_tests/doc_missing.ts similarity index 100% rename from src/legacy/ui/ui_settings/routes/integration_tests/doc_missing.ts rename to src/core/server/ui_settings/integration_tests/doc_missing.ts diff --git a/src/legacy/ui/ui_settings/routes/integration_tests/doc_missing_and_index_read_only.ts b/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts similarity index 100% rename from src/legacy/ui/ui_settings/routes/integration_tests/doc_missing_and_index_read_only.ts rename to src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts diff --git a/src/legacy/ui/ui_settings/routes/integration_tests/index.test.ts b/src/core/server/ui_settings/integration_tests/index.test.ts similarity index 100% rename from src/legacy/ui/ui_settings/routes/integration_tests/index.test.ts rename to src/core/server/ui_settings/integration_tests/index.test.ts diff --git a/src/legacy/ui/ui_settings/routes/integration_tests/lib/assert.ts b/src/core/server/ui_settings/integration_tests/lib/assert.ts similarity index 100% rename from src/legacy/ui/ui_settings/routes/integration_tests/lib/assert.ts rename to src/core/server/ui_settings/integration_tests/lib/assert.ts diff --git a/src/legacy/ui/ui_settings/routes/integration_tests/lib/chance.ts b/src/core/server/ui_settings/integration_tests/lib/chance.ts similarity index 100% rename from src/legacy/ui/ui_settings/routes/integration_tests/lib/chance.ts rename to src/core/server/ui_settings/integration_tests/lib/chance.ts diff --git a/src/legacy/ui/ui_settings/routes/integration_tests/lib/index.ts b/src/core/server/ui_settings/integration_tests/lib/index.ts similarity index 100% rename from src/legacy/ui/ui_settings/routes/integration_tests/lib/index.ts rename to src/core/server/ui_settings/integration_tests/lib/index.ts diff --git a/src/legacy/ui/ui_settings/routes/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts similarity index 83% rename from src/legacy/ui/ui_settings/routes/integration_tests/lib/servers.ts rename to src/core/server/ui_settings/integration_tests/lib/servers.ts index ae0ef1c91411..a1be1e7e7291 100644 --- a/src/legacy/ui/ui_settings/routes/integration_tests/lib/servers.ts +++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts @@ -17,20 +17,24 @@ * under the License. */ -import { UnwrapPromise } from '@kbn/utility-types'; import { SavedObjectsClientContract, IUiSettingsClient } from 'src/core/server'; -import KbnServer from '../../../../../server/kbn_server'; -import { createTestServers } from '../../../../../../test_utils/kbn_server'; -import { CallCluster } from '../../../../../../legacy/core_plugins/elasticsearch'; +import { + createTestServers, + TestElasticsearchUtils, + TestKibanaUtils, + TestUtils, +} from '../../../../../test_utils/kbn_server'; +import { CallCluster } from '../../../../../legacy/core_plugins/elasticsearch'; -let kbnServer: KbnServer; -let servers: ReturnType; -let esServer: UnwrapPromise>; -let kbn: UnwrapPromise>; +let servers: TestUtils; +let esServer: TestElasticsearchUtils; +let kbn: TestKibanaUtils; + +let kbnServer: TestKibanaUtils['kbnServer']; interface AllServices { - kbnServer: KbnServer; + kbnServer: TestKibanaUtils['kbnServer']; savedObjectsClient: SavedObjectsClientContract; callCluster: CallCluster; uiSettings: IUiSettingsClient; diff --git a/src/core/server/ui_settings/routes/delete.ts b/src/core/server/ui_settings/routes/delete.ts new file mode 100644 index 000000000000..ee4c05325fcd --- /dev/null +++ b/src/core/server/ui_settings/routes/delete.ts @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { schema } from '@kbn/config-schema'; + +import { IRouter } from '../../http'; +import { SavedObjectsErrorHelpers } from '../../saved_objects'; +import { CannotOverrideError } from '../ui_settings_errors'; + +const validate = { + params: schema.object({ + key: schema.string(), + }), +}; + +export function registerDeleteRoute(router: IRouter) { + router.delete( + { path: '/api/kibana/settings/{key}', validate }, + async (context, request, response) => { + try { + const uiSettingsClient = context.core.uiSettings.client; + + await uiSettingsClient.remove(request.params.key); + + return response.ok({ + body: { + settings: await uiSettingsClient.getUserProvided(), + }, + }); + } catch (error) { + if (SavedObjectsErrorHelpers.isSavedObjectsClientError(error)) { + return response.customError({ + body: error, + statusCode: error.output.statusCode, + }); + } + + if (error instanceof CannotOverrideError) { + return response.badRequest({ body: error }); + } + + throw error; + } + } + ); +} diff --git a/src/core/server/ui_settings/routes/get.ts b/src/core/server/ui_settings/routes/get.ts new file mode 100644 index 000000000000..d249369a1ace --- /dev/null +++ b/src/core/server/ui_settings/routes/get.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { IRouter } from '../../http'; +import { SavedObjectsErrorHelpers } from '../../saved_objects'; + +export function registerGetRoute(router: IRouter) { + router.get( + { path: '/api/kibana/settings', validate: false }, + async (context, request, response) => { + try { + const uiSettingsClient = context.core.uiSettings.client; + return response.ok({ + body: { + settings: await uiSettingsClient.getUserProvided(), + }, + }); + } catch (error) { + if (SavedObjectsErrorHelpers.isSavedObjectsClientError(error)) { + return response.customError({ + body: error, + statusCode: error.output.statusCode, + }); + } + + throw error; + } + } + ); +} diff --git a/src/core/server/ui_settings/routes/index.ts b/src/core/server/ui_settings/routes/index.ts new file mode 100644 index 000000000000..d70d55d72593 --- /dev/null +++ b/src/core/server/ui_settings/routes/index.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { IRouter } from 'src/core/server'; + +import { registerDeleteRoute } from './delete'; +import { registerGetRoute } from './get'; +import { registerSetManyRoute } from './set_many'; +import { registerSetRoute } from './set'; + +export function registerRoutes(router: IRouter) { + registerGetRoute(router); + registerDeleteRoute(router); + registerSetRoute(router); + registerSetManyRoute(router); +} diff --git a/src/core/server/ui_settings/routes/set.ts b/src/core/server/ui_settings/routes/set.ts new file mode 100644 index 000000000000..51ad256b5133 --- /dev/null +++ b/src/core/server/ui_settings/routes/set.ts @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { schema } from '@kbn/config-schema'; + +import { IRouter } from '../../http'; +import { SavedObjectsErrorHelpers } from '../../saved_objects'; +import { CannotOverrideError } from '../ui_settings_errors'; + +const validate = { + params: schema.object({ + key: schema.string(), + }), + body: schema.object({ + value: schema.any(), + }), +}; + +export function registerSetRoute(router: IRouter) { + router.post( + { path: '/api/kibana/settings/{key}', validate }, + async (context, request, response) => { + try { + const uiSettingsClient = context.core.uiSettings.client; + + const { key } = request.params; + const { value } = request.body; + + await uiSettingsClient.set(key, value); + + return response.ok({ + body: { + settings: await uiSettingsClient.getUserProvided(), + }, + }); + } catch (error) { + if (SavedObjectsErrorHelpers.isSavedObjectsClientError(error)) { + return response.customError({ + body: error, + statusCode: error.output.statusCode, + }); + } + + if (error instanceof CannotOverrideError) { + return response.badRequest({ body: error }); + } + + throw error; + } + } + ); +} diff --git a/src/core/server/ui_settings/routes/set_many.ts b/src/core/server/ui_settings/routes/set_many.ts new file mode 100644 index 000000000000..3794eba004be --- /dev/null +++ b/src/core/server/ui_settings/routes/set_many.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { schema } from '@kbn/config-schema'; + +import { IRouter } from '../../http'; +import { SavedObjectsErrorHelpers } from '../../saved_objects'; +import { CannotOverrideError } from '../ui_settings_errors'; + +const validate = { + body: schema.object({ + changes: schema.object({}, { allowUnknowns: true }), + }), +}; + +export function registerSetManyRoute(router: IRouter) { + router.post({ path: '/api/kibana/settings', validate }, async (context, request, response) => { + try { + const uiSettingsClient = context.core.uiSettings.client; + + const { changes } = request.body; + + await uiSettingsClient.setMany(changes); + + return response.ok({ + body: { + settings: await uiSettingsClient.getUserProvided(), + }, + }); + } catch (error) { + if (SavedObjectsErrorHelpers.isSavedObjectsClientError(error)) { + return response.customError({ + body: error, + statusCode: error.output.statusCode, + }); + } + + if (error instanceof CannotOverrideError) { + return response.badRequest({ body: error }); + } + + throw error; + } + }); +} diff --git a/src/core/server/ui_settings/types.ts b/src/core/server/ui_settings/types.ts new file mode 100644 index 000000000000..0fa6b3702af2 --- /dev/null +++ b/src/core/server/ui_settings/types.ts @@ -0,0 +1,141 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { SavedObjectsClientContract, SavedObjectAttribute } from '../saved_objects/types'; +/** + * Server-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. + * + * @public + */ +export interface IUiSettingsClient { + /** + * Returns registered uiSettings values {@link UiSettingsParams} + */ + getRegistered: () => Readonly>; + /** + * Retrieves uiSettings values set by the user with fallbacks to default values if not specified. + */ + 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>; + /** + * Retrieves a set of all uiSettings values set by the user. + */ + getUserProvided: () => Promise< + Record> + >; + /** + * Writes multiple uiSettings values and marks them as set by the user. + */ + setMany: (changes: Record) => Promise; + /** + * Writes uiSettings value and marks it as set by the user. + */ + set: (key: string, value: T) => Promise; + /** + * Removes uiSettings value by key. + */ + remove: (key: string) => Promise; + /** + * Removes multiple uiSettings values by keys. + */ + removeMany: (keys: string[]) => Promise; + /** + * Shows whether the uiSettings value set by the user. + */ + isOverridden: (key: string) => boolean; +} + +/** + * Describes the values explicitly set by user. + * @public + * */ +export interface UserProvidedValues { + userValue?: T; + isOverridden?: boolean; +} + +/** + * UI element type to represent the settings. + * @public + * */ +export type UiSettingsType = 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string'; + +/** + * UiSettings parameters defined by the plugins. + * @public + * */ +export interface UiSettingsParams { + /** title in the UI */ + name?: string; + /** default value to fall back to if a user doesn't provide any */ + value?: SavedObjectAttribute; + /** description provided to a user in UI */ + description?: string; + /** used to group the configured setting in the UI */ + category?: string[]; + /** array of permitted values for this setting */ + options?: string[]; + /** text labels for 'select' type UI element */ + optionLabels?: Record; + /** a flag indicating whether new value applying requires page reloading */ + requiresPageReload?: boolean; + /** a flag indicating that value cannot be changed */ + readonly?: boolean; + /** defines a type of UI element {@link UiSettingsType} */ + type?: UiSettingsType; +} + +/** @internal */ +export interface InternalUiSettingsServiceSetup { + /** + * Sets settings with default values for the uiSettings. + * @param settings + */ + register(settings: Record): void; + /** + * Creates uiSettings client with provided *scoped* saved objects client {@link IUiSettingsClient} + * @param savedObjectsClient + */ + asScopedToClient(savedObjectsClient: SavedObjectsClientContract): IUiSettingsClient; +} + +/** @public */ +export interface UiSettingsServiceSetup { + /** + * Sets settings with default values for the uiSettings. + * @param settings + * + * @example + * setup(core: CoreSetup){ + * core.uiSettings.register([{ + * foo: { + * name: i18n.translate('my foo settings'), + * value: true, + * description: 'add some awesomeness', + * }, + * }]); + * } + */ + register(settings: Record): void; +} diff --git a/src/core/server/ui_settings/ui_settings_client.test.ts b/src/core/server/ui_settings/ui_settings_client.test.ts index 59c13fbebee7..1c99637a89fe 100644 --- a/src/core/server/ui_settings/ui_settings_client.test.ts +++ b/src/core/server/ui_settings/ui_settings_client.test.ts @@ -24,6 +24,7 @@ import sinon from 'sinon'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { UiSettingsClient } from './ui_settings_client'; +import { CannotOverrideError } from './ui_settings_errors'; import * as createOrUpgradeSavedConfigNS from './create_or_upgrade_saved_config/create_or_upgrade_saved_config'; import { createObjectsClientStub, savedObjectsClientErrors } from './create_objects_client_stub'; @@ -119,7 +120,12 @@ describe('ui settings', () => { await uiSettings.setMany({ foo: 'bar' }); sinon.assert.calledTwice(savedObjectsClient.update); + sinon.assert.calledOnce(createOrUpgradeSavedConfig); + sinon.assert.calledWith( + createOrUpgradeSavedConfig, + sinon.match({ handleWriteErrors: false }) + ); }); it('only tried to auto create once and throws NotFound', async () => { @@ -135,9 +141,14 @@ describe('ui settings', () => { sinon.assert.calledTwice(savedObjectsClient.update); sinon.assert.calledOnce(createOrUpgradeSavedConfig); + + sinon.assert.calledWith( + createOrUpgradeSavedConfig, + sinon.match({ handleWriteErrors: false }) + ); }); - it('throws an error if any key is overridden', async () => { + it('throws CannotOverrideError if the key is overridden', async () => { const { uiSettings } = setup({ overrides: { foo: 'bar', @@ -150,6 +161,7 @@ describe('ui settings', () => { foo: 'baz', }); } catch (error) { + expect(error).to.be.a(CannotOverrideError); expect(error.message).to.be('Unable to update "foo" because it is overridden'); } }); @@ -167,7 +179,7 @@ describe('ui settings', () => { assertUpdateQuery({ one: 'value' }); }); - it('throws an error if the key is overridden', async () => { + it('throws CannotOverrideError if the key is overridden', async () => { const { uiSettings } = setup({ overrides: { foo: 'bar', @@ -177,6 +189,7 @@ describe('ui settings', () => { try { await uiSettings.set('foo', 'baz'); } catch (error) { + expect(error).to.be.a(CannotOverrideError); expect(error.message).to.be('Unable to update "foo" because it is overridden'); } }); @@ -194,7 +207,7 @@ describe('ui settings', () => { assertUpdateQuery({ one: null }); }); - it('throws an error if the key is overridden', async () => { + it('throws CannotOverrideError if the key is overridden', async () => { const { uiSettings } = setup({ overrides: { foo: 'bar', @@ -204,6 +217,7 @@ describe('ui settings', () => { try { await uiSettings.remove('foo'); } catch (error) { + expect(error).to.be.a(CannotOverrideError); expect(error.message).to.be('Unable to update "foo" because it is overridden'); } }); @@ -227,7 +241,7 @@ describe('ui settings', () => { assertUpdateQuery({ one: null, two: null, three: null }); }); - it('throws an error if any key is overridden', async () => { + it('throws CannotOverrideError if any key is overridden', async () => { const { uiSettings } = setup({ overrides: { foo: 'bar', @@ -237,18 +251,18 @@ describe('ui settings', () => { try { await uiSettings.setMany({ baz: 'baz', foo: 'foo' }); } catch (error) { + expect(error).to.be.a(CannotOverrideError); expect(error.message).to.be('Unable to update "foo" because it is overridden'); } }); }); - describe('#getDefaults()', () => { - it('returns the defaults passed to the constructor', () => { + describe('#getRegistered()', () => { + it('returns the registered settings passed to the constructor', () => { const value = chance.word(); - const { uiSettings } = setup({ defaults: { key: { value } } }); - expect(uiSettings.getDefaults()).to.eql({ - key: { value }, - }); + const defaults = { key: { value } }; + const { uiSettings } = setup({ defaults }); + expect(uiSettings.getRegistered()).to.be(defaults); }); }); @@ -284,31 +298,48 @@ describe('ui settings', () => { }); }); - it.skip('returns an empty object on NotFound responses', async () => { - const { uiSettings, savedObjectsClient } = setup(); + it('automatically creates the savedConfig if it is missing and returns empty object', async () => { + const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); + savedObjectsClient.get + .onFirstCall() + .throws(savedObjectsClientErrors.createGenericNotFoundError()) + .onSecondCall() + .returns({ attributes: {} }); - const error = savedObjectsClientErrors.createGenericNotFoundError(); - savedObjectsClient.get.throws(error); + expect(await uiSettings.getUserProvided()).to.eql({}); - expect(await uiSettings.getUserProvided({})).to.eql({}); + sinon.assert.calledTwice(savedObjectsClient.get); + + sinon.assert.calledOnce(createOrUpgradeSavedConfig); + sinon.assert.calledWith(createOrUpgradeSavedConfig, sinon.match({ handleWriteErrors: true })); + }); + + it('returns result of savedConfig creation in case of notFound error', async () => { + const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); + createOrUpgradeSavedConfig.resolves({ foo: 'bar ' }); + savedObjectsClient.get.throws(savedObjectsClientErrors.createGenericNotFoundError()); + + expect(await uiSettings.getUserProvided()).to.eql({ foo: { userValue: 'bar ' } }); }); it('returns an empty object on Forbidden responses', async () => { - const { uiSettings, savedObjectsClient } = setup(); + const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); const error = savedObjectsClientErrors.decorateForbiddenError(new Error()); savedObjectsClient.get.throws(error); expect(await uiSettings.getUserProvided()).to.eql({}); + sinon.assert.notCalled(createOrUpgradeSavedConfig); }); it('returns an empty object on EsUnavailable responses', async () => { - const { uiSettings, savedObjectsClient } = setup(); + const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); const error = savedObjectsClientErrors.decorateEsUnavailableError(new Error()); savedObjectsClient.get.throws(error); expect(await uiSettings.getUserProvided()).to.eql({}); + sinon.assert.notCalled(createOrUpgradeSavedConfig); }); it('throws Unauthorized errors', async () => { @@ -346,6 +377,7 @@ describe('ui settings', () => { const overrides = { foo: 'bar', + baz: null, }; const { uiSettings } = setup({ esDocSource, overrides }); @@ -357,57 +389,7 @@ describe('ui settings', () => { userValue: 'bar', isOverridden: true, }, - }); - }); - }); - - describe('#getRaw()', () => { - it('pulls user configuration from ES', async () => { - const esDocSource = {}; - const { uiSettings, assertGetQuery } = setup({ esDocSource }); - await uiSettings.getRaw(); - assertGetQuery(); - }); - - it(`without user configuration it's equal to the defaults`, async () => { - const esDocSource = {}; - const defaults = { key: { value: chance.word() } }; - const { uiSettings } = setup({ esDocSource, defaults }); - const result = await uiSettings.getRaw(); - expect(result).to.eql(defaults); - }); - - it(`user configuration gets merged with defaults`, async () => { - const esDocSource = { foo: 'bar' }; - const defaults = { key: { value: chance.word() } }; - const { uiSettings } = setup({ esDocSource, defaults }); - const result = await uiSettings.getRaw(); - - expect(result).to.eql({ - foo: { - userValue: 'bar', - }, - key: { - value: defaults.key.value, - }, - }); - }); - - it('includes the values for overridden keys', async () => { - const esDocSource = { foo: 'bar' }; - const defaults = { key: { value: chance.word() } }; - const overrides = { foo: true }; - const { uiSettings } = setup({ esDocSource, defaults, overrides }); - const result = await uiSettings.getRaw(); - - expect(result).to.eql({ - foo: { - userValue: true, - isOverridden: true, - }, - key: { - value: defaults.key.value, - }, + baz: { isOverridden: true }, }); }); }); @@ -545,22 +527,4 @@ describe('ui settings', () => { expect(uiSettings.isOverridden('bar')).to.be(true); }); }); - - describe('#assertUpdateAllowed()', () => { - it('returns false if no overrides defined', () => { - const { uiSettings } = setup(); - expect(uiSettings.assertUpdateAllowed('foo')).to.be(undefined); - }); - it('throws 400 Boom error when keys is overridden', () => { - const { uiSettings } = setup({ overrides: { foo: true } }); - expect(() => uiSettings.assertUpdateAllowed('foo')).to.throwError(error => { - expect(error).to.have.property( - 'message', - 'Unable to update "foo" because it is overridden' - ); - expect(error).to.have.property('isBoom', true); - expect(error.output).to.have.property('statusCode', 400); - }); - }); - }); }); diff --git a/src/core/server/ui_settings/ui_settings_client.ts b/src/core/server/ui_settings/ui_settings_client.ts index c495d1b4c456..423ff2a1dfd9 100644 --- a/src/core/server/ui_settings/ui_settings_client.ts +++ b/src/core/server/ui_settings/ui_settings_client.ts @@ -17,12 +17,13 @@ * under the License. */ import { defaultsDeep } from 'lodash'; -import Boom from 'boom'; +import { SavedObjectsErrorHelpers } from '../saved_objects'; import { SavedObjectsClientContract, SavedObjectAttribute } from '../saved_objects/types'; import { Logger } from '../logging'; import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; -import { UiSettingsParams } from './ui_settings_service'; +import { IUiSettingsClient, UiSettingsParams } from './types'; +import { CannotOverrideError } from './ui_settings_errors'; export interface UiSettingsServiceOptions { type: string; @@ -49,52 +50,6 @@ type UiSettingsRawValue = UiSettingsParams & UserProvidedValue; type UserProvided = Record>; type UiSettingsRaw = Record; -/** - * Service that provides access to the UiSettings stored in elasticsearch. - * - * @public - */ -export interface IUiSettingsClient { - /** - * Returns uiSettings default values {@link UiSettingsParams} - */ - getDefaults: () => Record; - /** - * Retrieves uiSettings values set by the user with fallbacks to default values if not specified. - */ - 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>; - /** - * Retrieves a set of all uiSettings values set by the user. - */ - getUserProvided: () => Promise< - Record - >; - /** - * Writes multiple uiSettings values and marks them as set by the user. - */ - setMany: (changes: Record) => Promise; - /** - * Writes uiSettings value and marks it as set by the user. - */ - set: (key: string, value: T) => Promise; - /** - * Removes uiSettings value by key. - */ - remove: (key: string) => Promise; - /** - * Removes multiple uiSettings values by keys. - */ - removeMany: (keys: string[]) => Promise; - /** - * Shows whether the uiSettings value set by the user. - */ - isOverridden: (key: string) => boolean; -} - export class UiSettingsClient implements IUiSettingsClient { private readonly type: UiSettingsServiceOptions['type']; private readonly id: UiSettingsServiceOptions['id']; @@ -116,7 +71,7 @@ export class UiSettingsClient implements IUiSettingsClient { this.log = log; } - getDefaults() { + getRegistered() { return this.defaults; } @@ -138,19 +93,11 @@ export class UiSettingsClient implements IUiSettingsClient { ); } - // NOTE: should be a private method - async getRaw(): Promise { - const userProvided = await this.getUserProvided(); - return defaultsDeep(userProvided, this.defaults); - } - - async getUserProvided( - options: ReadOptions = {} - ): Promise> { + async getUserProvided(): Promise> { const userProvided: UserProvided = {}; // write the userValue for each key stored in the saved object that is not overridden - for (const [key, userValue] of Object.entries(await this.read(options))) { + for (const [key, userValue] of Object.entries(await this.read())) { if (userValue !== null && !this.isOverridden(key)) { userProvided[key] = { userValue, @@ -192,13 +139,17 @@ export class UiSettingsClient implements IUiSettingsClient { return this.overrides.hasOwnProperty(key); } - // NOTE: should be private method - assertUpdateAllowed(key: string) { + private assertUpdateAllowed(key: string) { if (this.isOverridden(key)) { - throw Boom.badRequest(`Unable to update "${key}" because it is overridden`); + throw new CannotOverrideError(`Unable to update "${key}" because it is overridden`); } } + private async getRaw(): Promise { + const userProvided = await this.getUserProvided(); + return defaultsDeep(userProvided, this.defaults); + } + private async write({ changes, autoCreateOrUpgradeIfMissing = true, @@ -213,8 +164,7 @@ export class UiSettingsClient implements IUiSettingsClient { try { await this.savedObjectsClient.update(this.type, this.id, changes); } catch (error) { - const { isNotFoundError } = this.savedObjectsClient.errors; - if (!isNotFoundError(error) || !autoCreateOrUpgradeIfMissing) { + if (!SavedObjectsErrorHelpers.isNotFoundError(error) || !autoCreateOrUpgradeIfMissing) { throw error; } @@ -223,6 +173,7 @@ export class UiSettingsClient implements IUiSettingsClient { version: this.id, buildNum: this.buildNum, log: this.log, + handleWriteErrors: false, }); await this.write({ @@ -236,37 +187,17 @@ export class UiSettingsClient implements IUiSettingsClient { ignore401Errors = false, autoCreateOrUpgradeIfMissing = true, }: ReadOptions = {}): Promise> { - const { - isConflictError, - isNotFoundError, - isForbiddenError, - isNotAuthorizedError, - } = this.savedObjectsClient.errors; - try { const resp = await this.savedObjectsClient.get(this.type, this.id); return resp.attributes; } catch (error) { - if (isNotFoundError(error) && autoCreateOrUpgradeIfMissing) { + if (SavedObjectsErrorHelpers.isNotFoundError(error) && autoCreateOrUpgradeIfMissing) { const failedUpgradeAttributes = await createOrUpgradeSavedConfig({ savedObjectsClient: this.savedObjectsClient, version: this.id, buildNum: this.buildNum, log: this.log, - onWriteError(writeError, attributes) { - if (isConflictError(writeError)) { - // trigger `!failedUpgradeAttributes` check below, since another - // request caused the uiSettings object to be created so we can - // just re-read - return; - } - - if (isNotAuthorizedError(writeError) || isForbiddenError(writeError)) { - return attributes; - } - - throw writeError; - }, + handleWriteErrors: true, }); if (!failedUpgradeAttributes) { diff --git a/src/core/server/ui_settings/ui_settings_errors.ts b/src/core/server/ui_settings/ui_settings_errors.ts new file mode 100644 index 000000000000..d8fc32b111e4 --- /dev/null +++ b/src/core/server/ui_settings/ui_settings_errors.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export class CannotOverrideError extends Error { + public cause?: Error; + + constructor(message: string, cause?: Error) { + super(message); + this.cause = cause; + + // Set the prototype explicitly, see: + // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + Object.setPrototypeOf(this, CannotOverrideError.prototype); + } +} diff --git a/src/core/server/ui_settings/ui_settings_service.mock.ts b/src/core/server/ui_settings/ui_settings_service.mock.ts index 2127faf0d202..bb21109a2f96 100644 --- a/src/core/server/ui_settings/ui_settings_service.mock.ts +++ b/src/core/server/ui_settings/ui_settings_service.mock.ts @@ -17,12 +17,11 @@ * under the License. */ -import { IUiSettingsClient } from './ui_settings_client'; -import { InternalUiSettingsServiceSetup } from './ui_settings_service'; +import { IUiSettingsClient, InternalUiSettingsServiceSetup } from './types'; const createClientMock = () => { const mocked: jest.Mocked = { - getDefaults: jest.fn(), + getRegistered: jest.fn(), get: jest.fn(), getAll: jest.fn(), getUserProvided: jest.fn(), @@ -38,7 +37,7 @@ const createClientMock = () => { const createSetupMock = () => { const mocked: jest.Mocked = { - setDefaults: jest.fn(), + register: jest.fn(), asScopedToClient: jest.fn(), }; @@ -48,6 +47,6 @@ const createSetupMock = () => { }; export const uiSettingsServiceMock = { - createSetup: createSetupMock, + createSetupContract: createSetupMock, createClient: createClientMock, }; diff --git a/src/core/server/ui_settings/ui_settings_service.test.ts b/src/core/server/ui_settings/ui_settings_service.test.ts index e219d0c96234..d7a085a22019 100644 --- a/src/core/server/ui_settings/ui_settings_service.test.ts +++ b/src/core/server/ui_settings/ui_settings_service.test.ts @@ -52,6 +52,14 @@ afterEach(() => { describe('uiSettings', () => { describe('#setup', () => { describe('#asScopedToClient', () => { + it('passes saved object type "config" to UiSettingsClient', async () => { + const service = new UiSettingsService(coreContext); + const setup = await service.setup(setupDeps); + setup.asScopedToClient(savedObjectsClient); + expect(MockUiSettingsClientConstructor).toBeCalledTimes(1); + expect(MockUiSettingsClientConstructor.mock.calls[0][0].type).toBe('config'); + }); + it('passes overrides to UiSettingsClient', async () => { const service = new UiSettingsService(coreContext); const setup = await service.setup(setupDeps); @@ -86,7 +94,7 @@ describe('uiSettings', () => { const service = new UiSettingsService(coreContext); const setup = await service.setup(setupDeps); - setup.setDefaults(defaults); + setup.register(defaults); setup.asScopedToClient(savedObjectsClient); expect(MockUiSettingsClientConstructor).toBeCalledTimes(1); @@ -95,13 +103,13 @@ describe('uiSettings', () => { }); }); - describe('#setDefaults', () => { - it('throws if set defaults for the same key twice', async () => { + describe('#register', () => { + it('throws if registers the same key twice', async () => { const service = new UiSettingsService(coreContext); const setup = await service.setup(setupDeps); - setup.setDefaults(defaults); - expect(() => setup.setDefaults(defaults)).toThrowErrorMatchingInlineSnapshot( - `"uiSettings defaults for key [foo] has been already set"` + setup.register(defaults); + expect(() => setup.register(defaults)).toThrowErrorMatchingInlineSnapshot( + `"uiSettings for the key [foo] has been already registered"` ); }); }); diff --git a/src/core/server/ui_settings/ui_settings_service.ts b/src/core/server/ui_settings/ui_settings_service.ts index 746fa514c5d4..a8f5663f8bd1 100644 --- a/src/core/server/ui_settings/ui_settings_service.ts +++ b/src/core/server/ui_settings/ui_settings_service.ts @@ -26,58 +26,16 @@ import { Logger } from '../logging'; import { SavedObjectsClientContract, SavedObjectAttribute } from '../saved_objects/types'; import { InternalHttpServiceSetup } from '../http'; import { UiSettingsConfigType } from './ui_settings_config'; -import { IUiSettingsClient, UiSettingsClient } from './ui_settings_client'; +import { UiSettingsClient } from './ui_settings_client'; +import { InternalUiSettingsServiceSetup, UiSettingsParams } from './types'; import { mapToObject } from '../../utils/'; +import { registerRoutes } from './routes'; + interface SetupDeps { http: InternalHttpServiceSetup; } -/** - * UI element type to represent the settings. - * @public - * */ -export type UiSettingsType = 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string'; - -/** - * UiSettings parameters defined by the plugins. - * @public - * */ -export interface UiSettingsParams { - /** title in the UI */ - name: string; - /** default value to fall back to if a user doesn't provide any */ - value: SavedObjectAttribute; - /** description provided to a user in UI */ - description: string; - /** used to group the configured setting in the UI */ - category: string[]; - /** a range of valid values */ - options?: string[]; - /** text labels for 'select' type UI element */ - optionLabels?: Record; - /** a flag indicating whether new value applying requires page reloading */ - requiresPageReload?: boolean; - /** a flag indicating that value cannot be changed */ - readonly?: boolean; - /** defines a type of UI element {@link UiSettingsType} */ - type?: UiSettingsType; -} - -/** @internal */ -export interface InternalUiSettingsServiceSetup { - /** - * Sets the parameters with default values for the uiSettings. - * @param values - */ - setDefaults(values: Record): void; - /** - * Creates uiSettings client with provided *scoped* saved objects client {@link IUiSettingsClient} - * @param values - */ - asScopedToClient(savedObjectsClient: SavedObjectsClientContract): IUiSettingsClient; -} - /** @internal */ export class UiSettingsService implements CoreService { private readonly log: Logger; @@ -90,12 +48,13 @@ export class UiSettingsService implements CoreService { + registerRoutes(deps.http.createRouter('')); this.log.debug('Setting up ui settings service'); const overrides = await this.getOverrides(deps); const { version, buildNum } = this.coreContext.env.packageInfo; return { - setDefaults: this.setDefaults.bind(this), + register: this.register.bind(this), asScopedToClient: (savedObjectsClient: SavedObjectsClientContract) => { return new UiSettingsClient({ type: 'config', @@ -114,10 +73,10 @@ export class UiSettingsService implements CoreService = {}) { - Object.entries(values).forEach(([key, value]) => { + private register(settings: Record = {}) { + Object.entries(settings).forEach(([key, value]) => { if (this.uiSettingsDefaults.has(key)) { - throw new Error(`uiSettings defaults for key [${key}] has been already set`); + throw new Error(`uiSettings for the key [${key}] has been already registered`); } this.uiSettingsDefaults.set(key, value); }); diff --git a/src/legacy/server/http/integration_tests/default_route_provider.test.ts b/src/legacy/server/http/integration_tests/default_route_provider.test.ts index d74be851a353..4898cb2b6785 100644 --- a/src/legacy/server/http/integration_tests/default_route_provider.test.ts +++ b/src/legacy/server/http/integration_tests/default_route_provider.test.ts @@ -44,7 +44,7 @@ describe('default route provider', () => { } throw Error(`unsupported ui setting: ${key}`); }, - getDefaults: () => { + getRegistered: () => { return { defaultRoute: { value: '/app/kibana', diff --git a/src/legacy/server/http/setup_default_route_provider.ts b/src/legacy/server/http/setup_default_route_provider.ts index f627cf30a3cf..0e7bcf1f56f6 100644 --- a/src/legacy/server/http/setup_default_route_provider.ts +++ b/src/legacy/server/http/setup_default_route_provider.ts @@ -40,7 +40,7 @@ export function setupDefaultRouteProvider(server: Legacy.Server) { `Ignoring configured default route of '${defaultRoute}', as it is malformed.` ); - const fallbackRoute = uiSettings.getDefaults().defaultRoute.value; + const fallbackRoute = uiSettings.getRegistered().defaultRoute.value; const qualifiedFallbackRoute = `${request.getBasePath()}${fallbackRoute}`; return qualifiedFallbackRoute; diff --git a/src/legacy/ui/__tests__/ui_exports_replace_injected_vars.js b/src/legacy/ui/__tests__/ui_exports_replace_injected_vars.js index 20ff1d5bcfe0..a58ec992495f 100644 --- a/src/legacy/ui/__tests__/ui_exports_replace_injected_vars.js +++ b/src/legacy/ui/__tests__/ui_exports_replace_injected_vars.js @@ -69,7 +69,7 @@ describe('UiExports', function () { sandbox .stub(getUiSettingsServiceForRequestNS, 'getUiSettingsServiceForRequest') .returns({ - getDefaults: noop, + getRegistered: noop, getUserProvided: noop }); }); diff --git a/src/legacy/ui/field_formats/mixin/field_formats_mixin.ts b/src/legacy/ui/field_formats/mixin/field_formats_mixin.ts index 27b3967d6f46..66466b96abe8 100644 --- a/src/legacy/ui/field_formats/mixin/field_formats_mixin.ts +++ b/src/legacy/ui/field_formats/mixin/field_formats_mixin.ts @@ -28,9 +28,9 @@ export function fieldFormatsMixin(kbnServer: any, server: Legacy.Server) { // for use outside of the request context, for special cases server.decorate('server', 'fieldFormatServiceFactory', async function(uiSettings) { const uiConfigs = await uiSettings.getAll(); - const uiSettingDefaults = uiSettings.getDefaults(); - Object.keys(uiSettingDefaults).forEach(key => { - if (has(uiConfigs, key) && uiSettingDefaults[key].type === 'json') { + const registeredUiSettings = uiSettings.getRegistered(); + Object.keys(registeredUiSettings).forEach(key => { + if (has(uiConfigs, key) && registeredUiSettings[key].type === 'json') { uiConfigs[key] = JSON.parse(uiConfigs[key]); } }); diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 883358e0d3c9..0d05ea259d1a 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -185,7 +185,7 @@ export function uiRenderMixin(kbnServer, server, config) { async function getUiSettings({ request, includeUserProvidedConfig }) { const uiSettings = request.getUiSettingsService(); return props({ - defaults: uiSettings.getDefaults(), + defaults: uiSettings.getRegistered(), user: includeUserProvidedConfig && uiSettings.getUserProvided() }); } diff --git a/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts b/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts index 3da7f83be489..dd3f12903abc 100644 --- a/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts +++ b/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts @@ -73,7 +73,7 @@ describe('uiSettingsMixin()', () => { newPlatform: { __internals: { uiSettings: { - setDefaults: sinon.stub(), + register: sinon.stub(), }, }, }, @@ -93,9 +93,9 @@ describe('uiSettingsMixin()', () => { it('passes uiSettingsDefaults to the new platform', () => { const { kbnServer } = setup(); - sinon.assert.calledOnce(kbnServer.newPlatform.__internals.uiSettings.setDefaults); + sinon.assert.calledOnce(kbnServer.newPlatform.__internals.uiSettings.register); sinon.assert.calledWithExactly( - kbnServer.newPlatform.__internals.uiSettings.setDefaults, + kbnServer.newPlatform.__internals.uiSettings.register, uiSettingDefaults ); }); diff --git a/src/legacy/ui/ui_settings/routes/set.ts b/src/legacy/ui/ui_settings/routes/set.ts deleted file mode 100644 index 1f1ab17a0daf..000000000000 --- a/src/legacy/ui/ui_settings/routes/set.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { Legacy } from 'kibana'; -import Joi from 'joi'; - -async function handleRequest(request: Legacy.Request) { - const { key } = request.params; - const { value } = request.payload as any; - const uiSettings = request.getUiSettingsService(); - - await uiSettings.set(key, value); - - return { - settings: await uiSettings.getUserProvided(), - }; -} - -export const setRoute = { - path: '/api/kibana/settings/{key}', - method: 'POST', - config: { - validate: { - params: Joi.object() - .keys({ - key: Joi.string().required(), - }) - .default(), - - payload: Joi.object() - .keys({ - value: Joi.any().required(), - }) - .required(), - }, - handler(request: Legacy.Request) { - return handleRequest(request); - }, - }, -}; diff --git a/src/legacy/ui/ui_settings/ui_settings_mixin.js b/src/legacy/ui/ui_settings/ui_settings_mixin.js index 8c7ef25c6f8d..64251d290776 100644 --- a/src/legacy/ui/ui_settings/ui_settings_mixin.js +++ b/src/legacy/ui/ui_settings/ui_settings_mixin.js @@ -19,12 +19,6 @@ import { uiSettingsServiceFactory } from './ui_settings_service_factory'; import { getUiSettingsServiceForRequest } from './ui_settings_service_for_request'; -import { - deleteRoute, - getRoute, - setManyRoute, - setRoute, -} from './routes'; export function uiSettingsMixin(kbnServer, server) { const { uiSettingDefaults = {} } = kbnServer.uiExports; @@ -43,7 +37,7 @@ export function uiSettingsMixin(kbnServer, server) { return acc; }, {}); - kbnServer.newPlatform.__internals.uiSettings.setDefaults(mergedUiSettingDefaults); + kbnServer.newPlatform.__internals.uiSettings.register(mergedUiSettingDefaults); server.decorate('server', 'uiSettingsServiceFactory', (options = {}) => { return uiSettingsServiceFactory(server, options); @@ -58,9 +52,4 @@ export function uiSettingsMixin(kbnServer, server) { server.uiSettings has been removed, see https://github.com/elastic/kibana/pull/12243. `); }); - - server.route(deleteRoute); - server.route(getRoute); - server.route(setManyRoute); - server.route(setRoute); } diff --git a/src/test_utils/kbn_server.ts b/src/test_utils/kbn_server.ts index e7e5e0776765..380d21b66ec5 100644 --- a/src/test_utils/kbn_server.ts +++ b/src/test_utils/kbn_server.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import { Client } from 'elasticsearch'; import { ToolingLog } from '@kbn/dev-utils'; import { createLegacyEsTestCluster, @@ -36,6 +36,7 @@ import { CliArgs, Env } from '../core/server/config'; import { LegacyObjectToConfigAdapter } from '../core/server/legacy'; import { Root } from '../core/server/root'; import KbnServer from '../legacy/server/kbn_server'; +import { CallCluster } from '../legacy/core_plugins/elasticsearch'; type HttpMethod = 'delete' | 'get' | 'head' | 'post' | 'put'; @@ -144,6 +145,35 @@ export const request: Record< put: (root, path) => getSupertest(root, 'put', path), }; +export interface TestElasticsearchServer { + getStartTimeout: () => number; + start: (esArgs: string[], esEnvVars: Record) => Promise; + stop: () => Promise; + cleanup: () => Promise; + getClient: () => Client; + getCallCluster: () => CallCluster; + getUrl: () => string; +} + +export interface TestElasticsearchUtils { + stop: () => Promise; + es: TestElasticsearchServer; + hosts: string[]; + username: string; + password: string; +} + +export interface TestKibanaUtils { + root: Root; + kbnServer: KbnServer; + stop: () => Promise; +} + +export interface TestUtils { + startES: () => Promise; + startKibana: () => Promise; +} + /** * Creates an instance of the Root, including all of the core "legacy" plugins, * with default configuration tailored for unit tests, and starts es. @@ -158,7 +188,7 @@ export function createTestServers({ settings = {}, }: { adjustTimeout: (timeout: number) => void; - settings: { + settings?: { es?: { license: 'oss' | 'basic' | 'gold' | 'trial'; [key: string]: any; @@ -179,7 +209,7 @@ export function createTestServers({ */ users?: Array<{ username: string; password: string; roles: string[] }>; }; -}) { +}): TestUtils { if (!adjustTimeout) { throw new Error('adjustTimeout is required in order to avoid flaky tests'); } diff --git a/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts b/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts index 67c9120b1f2d..771e50b22f66 100644 --- a/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts +++ b/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts @@ -29,7 +29,7 @@ declare module 'kibana/server' { export class CorePluginBPlugin implements Plugin { public setup(core: CoreSetup, deps: {}) { const router = core.http.createRouter(); - router.get({ path: '/core_plugin_b/', validate: false }, async (context, req, res) => { + router.get({ path: '/core_plugin_b', validate: false }, async (context, req, res) => { if (!context.pluginA) return res.internalError({ body: 'pluginA is disabled' }); const response = await context.pluginA.ping(); return res.ok({ body: `Pong via plugin A: ${response}` }); diff --git a/test/plugin_functional/plugins/ui_settings_plugin/kibana.json b/test/plugin_functional/plugins/ui_settings_plugin/kibana.json new file mode 100644 index 000000000000..05d2dca0af93 --- /dev/null +++ b/test/plugin_functional/plugins/ui_settings_plugin/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "ui_settings_plugin", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["ui_settings_plugin"], + "server": true, + "ui": true +} diff --git a/test/plugin_functional/plugins/ui_settings_plugin/package.json b/test/plugin_functional/plugins/ui_settings_plugin/package.json new file mode 100644 index 000000000000..6a0d5999412f --- /dev/null +++ b/test/plugin_functional/plugins/ui_settings_plugin/package.json @@ -0,0 +1,17 @@ +{ + "name": "ui_settings_plugin", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/ui_settings_plugin", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.5.3" + } +} diff --git a/src/legacy/ui/ui_settings/routes/index.ts b/test/plugin_functional/plugins/ui_settings_plugin/public/index.ts similarity index 84% rename from src/legacy/ui/ui_settings/routes/index.ts rename to test/plugin_functional/plugins/ui_settings_plugin/public/index.ts index f3c9d4f0d8d1..3c5997132d46 100644 --- a/src/legacy/ui/ui_settings/routes/index.ts +++ b/test/plugin_functional/plugins/ui_settings_plugin/public/index.ts @@ -16,8 +16,6 @@ * specific language governing permissions and limitations * under the License. */ +import { UiSettingsPlugin } from './plugin'; -export { deleteRoute } from './delete'; -export { getRoute } from './get'; -export { setManyRoute } from './set_many'; -export { setRoute } from './set'; +export const plugin = () => new UiSettingsPlugin(); diff --git a/src/legacy/ui/ui_settings/routes/delete.ts b/test/plugin_functional/plugins/ui_settings_plugin/public/plugin.tsx similarity index 63% rename from src/legacy/ui/ui_settings/routes/delete.ts rename to test/plugin_functional/plugins/ui_settings_plugin/public/plugin.tsx index 7825204e6b99..883d203b4c37 100644 --- a/src/legacy/ui/ui_settings/routes/delete.ts +++ b/test/plugin_functional/plugins/ui_settings_plugin/public/plugin.tsx @@ -16,22 +16,22 @@ * specific language governing permissions and limitations * under the License. */ -import { Legacy } from 'kibana'; -async function handleRequest(request: Legacy.Request) { - const { key } = request.params; - const uiSettings = request.getUiSettingsService(); +import { CoreSetup, Plugin } from 'kibana/public'; - await uiSettings.remove(key); - return { - settings: await uiSettings.getUserProvided(), - }; +declare global { + interface Window { + uiSettingsPlugin?: Record; + uiSettingsPluginValue?: string; + } } -export const deleteRoute = { - path: '/api/kibana/settings/{key}', - method: 'DELETE', - handler: async (request: Legacy.Request) => { - return await handleRequest(request); - }, -}; +export class UiSettingsPlugin implements Plugin { + public setup(core: CoreSetup) { + window.uiSettingsPlugin = core.uiSettings.getAll().ui_settings_plugin; + window.uiSettingsPluginValue = core.uiSettings.get('ui_settings_plugin'); + } + + public start() {} + public stop() {} +} diff --git a/test/plugin_functional/services/index.js b/test/plugin_functional/plugins/ui_settings_plugin/server/index.ts similarity index 86% rename from test/plugin_functional/services/index.js rename to test/plugin_functional/plugins/ui_settings_plugin/server/index.ts index bf02587772f4..7715cef31d72 100644 --- a/test/plugin_functional/services/index.js +++ b/test/plugin_functional/plugins/ui_settings_plugin/server/index.ts @@ -17,8 +17,6 @@ * under the License. */ -import { KibanaSupertestProvider } from './supertest'; +import { UiSettingsPlugin } from './plugin'; -export const services = { - supertest: KibanaSupertestProvider, -}; +export const plugin = () => new UiSettingsPlugin(); diff --git a/src/legacy/ui/ui_settings/routes/set_many.ts b/test/plugin_functional/plugins/ui_settings_plugin/server/plugin.ts similarity index 53% rename from src/legacy/ui/ui_settings/routes/set_many.ts rename to test/plugin_functional/plugins/ui_settings_plugin/server/plugin.ts index 18b1046417fe..c32e8a75d95d 100644 --- a/src/legacy/ui/ui_settings/routes/set_many.ts +++ b/test/plugin_functional/plugins/ui_settings_plugin/server/plugin.ts @@ -16,35 +16,29 @@ * specific language governing permissions and limitations * under the License. */ -import { Legacy } from 'kibana'; -import Joi from 'joi'; -async function handleRequest(request: Legacy.Request) { - const { changes } = request.payload as any; - const uiSettings = request.getUiSettingsService(); +import { Plugin, CoreSetup } from 'kibana/server'; - await uiSettings.setMany(changes); +export class UiSettingsPlugin implements Plugin { + public setup(core: CoreSetup) { + core.uiSettings.register({ + ui_settings_plugin: { + name: 'from_ui_settings_plugin', + description: 'just for testing', + value: '2', + category: ['any'], + }, + }); - return { - settings: await uiSettings.getUserProvided(), - }; + const router = core.http.createRouter(); + router.get({ path: '/api/ui-settings-plugin', validate: false }, async (context, req, res) => { + const uiSettingsValue = await context.core.uiSettings.client.get( + 'ui_settings_plugin' + ); + return res.ok({ body: { uiSettingsValue } }); + }); + } + + public start() {} + public stop() {} } - -export const setManyRoute = { - path: '/api/kibana/settings', - method: 'POST', - config: { - validate: { - payload: Joi.object() - .keys({ - changes: Joi.object() - .unknown(true) - .required(), - }) - .required(), - }, - handler(request: Legacy.Request) { - return handleRequest(request); - }, - }, -}; diff --git a/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json b/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json new file mode 100644 index 000000000000..1ba21f11b7de --- /dev/null +++ b/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../../../typings/**/*", + ], + "exclude": [] +} diff --git a/src/legacy/ui/ui_settings/routes/get.ts b/test/plugin_functional/services/index.ts similarity index 68% rename from src/legacy/ui/ui_settings/routes/get.ts rename to test/plugin_functional/services/index.ts index 3e165a12522b..dd2b25e14fe1 100644 --- a/src/legacy/ui/ui_settings/routes/get.ts +++ b/test/plugin_functional/services/index.ts @@ -16,19 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import { Legacy } from 'kibana'; +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { FtrProviderContext } from 'test/functional/ftr_provider_context'; -async function handleRequest(request: Legacy.Request) { - const uiSettings = request.getUiSettingsService(); - return { - settings: await uiSettings.getUserProvided(), - }; -} +import { KibanaSupertestProvider } from './supertest'; -export const getRoute = { - path: '/api/kibana/settings', - method: 'GET', - handler(request: Legacy.Request) { - return handleRequest(request); - }, +export const services = { + supertest: KibanaSupertestProvider, }; + +export type PluginFunctionalProviderContext = FtrProviderContext & + GenericFtrProviderContext; diff --git a/test/plugin_functional/services/supertest.js b/test/plugin_functional/services/supertest.ts similarity index 87% rename from test/plugin_functional/services/supertest.js rename to test/plugin_functional/services/supertest.ts index 390f89acaa77..6b7dc26248c0 100644 --- a/test/plugin_functional/services/supertest.js +++ b/test/plugin_functional/services/supertest.ts @@ -16,13 +16,12 @@ * specific language governing permissions and limitations * under the License. */ - - import { format as formatUrl } from 'url'; +import { FtrProviderContext } from 'test/functional/ftr_provider_context'; import supertestAsPromised from 'supertest-as-promised'; -export function KibanaSupertestProvider({ getService }) { +export function KibanaSupertestProvider({ getService }: FtrProviderContext) { const config = getService('config'); const kibanaServerUrl = formatUrl(config.get('servers.kibana')); return supertestAsPromised(kibanaServerUrl); diff --git a/test/plugin_functional/test_suites/core_plugins/applications.js b/test/plugin_functional/test_suites/core_plugins/applications.ts similarity index 86% rename from test/plugin_functional/test_suites/core_plugins/applications.js rename to test/plugin_functional/test_suites/core_plugins/applications.ts index 4c4c198d1af9..eec2ec019a51 100644 --- a/test/plugin_functional/test_suites/core_plugins/applications.js +++ b/test/plugin_functional/test_suites/core_plugins/applications.ts @@ -18,8 +18,10 @@ */ import url from 'url'; import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; -export default function ({ getService, getPageObjects }) { +// eslint-disable-next-line import/no-default-export +export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { const PageObjects = getPageObjects(['common']); const browser = getService('browser'); @@ -29,16 +31,16 @@ export default function ({ getService, getPageObjects }) { const loadingScreenNotShown = async () => expect(await testSubjects.exists('kbnLoadingMessage')).to.be(false); - const loadingScreenShown = () => - testSubjects.existOrFail('kbnLoadingMessage'); + const loadingScreenShown = () => testSubjects.existOrFail('kbnLoadingMessage'); - const getKibanaUrl = (pathname, search) => url.format({ - protocol: 'http:', - hostname: process.env.TEST_KIBANA_HOST || 'localhost', - port: process.env.TEST_KIBANA_PORT || '5620', - pathname, - search, - }); + const getKibanaUrl = (pathname?: string, search?: string) => + url.format({ + protocol: 'http:', + hostname: process.env.TEST_KIBANA_HOST || 'localhost', + port: process.env.TEST_KIBANA_PORT || '5620', + pathname, + search, + }); describe('ui applications', function describeIndexTests() { before(async () => { diff --git a/test/plugin_functional/test_suites/core_plugins/index.js b/test/plugin_functional/test_suites/core_plugins/index.ts similarity index 81% rename from test/plugin_functional/test_suites/core_plugins/index.js rename to test/plugin_functional/test_suites/core_plugins/index.ts index 376c6f1ebadb..bf33f37694c3 100644 --- a/test/plugin_functional/test_suites/core_plugins/index.js +++ b/test/plugin_functional/test_suites/core_plugins/index.ts @@ -16,13 +16,16 @@ * specific language governing permissions and limitations * under the License. */ +import { PluginFunctionalProviderContext } from '../../services'; -export default function ({ loadTestFile }) { +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: PluginFunctionalProviderContext) { describe('core plugins', () => { loadTestFile(require.resolve('./applications')); loadTestFile(require.resolve('./legacy_plugins')); loadTestFile(require.resolve('./server_plugins')); loadTestFile(require.resolve('./ui_plugins')); + loadTestFile(require.resolve('./ui_settings')); loadTestFile(require.resolve('./top_nav')); }); } diff --git a/test/plugin_functional/test_suites/core_plugins/legacy_plugins.js b/test/plugin_functional/test_suites/core_plugins/legacy_plugins.ts similarity index 84% rename from test/plugin_functional/test_suites/core_plugins/legacy_plugins.js rename to test/plugin_functional/test_suites/core_plugins/legacy_plugins.ts index c6edf803c993..d0c01f1e9caf 100644 --- a/test/plugin_functional/test_suites/core_plugins/legacy_plugins.js +++ b/test/plugin_functional/test_suites/core_plugins/legacy_plugins.ts @@ -18,13 +18,15 @@ */ import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; -export default function ({ getService, getPageObjects }) { +// eslint-disable-next-line import/no-default-export +export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { const PageObjects = getPageObjects(['common']); const testSubjects = getService('testSubjects'); const supertest = getService('supertest'); - describe('legacy plugins', function describeIndexTests() { + describe('legacy plugins', () => { describe('http', () => { it('has access to New Platform HTTP service', async () => { await supertest @@ -41,7 +43,7 @@ export default function ({ getService, getPageObjects }) { }); }); - describe('application service compatibility layer', function describeIndexTests() { + describe('application service compatibility layer', () => { it('can render legacy apps', async () => { await PageObjects.common.navigateToApp('core_plugin_legacy'); expect(await testSubjects.exists('coreLegacyCompatH1')).to.be(true); diff --git a/test/plugin_functional/test_suites/core_plugins/server_plugins.js b/test/plugin_functional/test_suites/core_plugins/server_plugins.ts similarity index 67% rename from test/plugin_functional/test_suites/core_plugins/server_plugins.js rename to test/plugin_functional/test_suites/core_plugins/server_plugins.ts index a17b19468d6a..3881af564299 100644 --- a/test/plugin_functional/test_suites/core_plugins/server_plugins.js +++ b/test/plugin_functional/test_suites/core_plugins/server_plugins.ts @@ -16,20 +16,18 @@ * specific language governing permissions and limitations * under the License. */ +import { PluginFunctionalProviderContext } from '../../services'; -import expect from '@kbn/expect'; - -export default function ({ getService, getPageObjects }) { - const PageObjects = getPageObjects(['common']); - const browser = getService('browser'); +// eslint-disable-next-line import/no-default-export +export default function({ getService }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); describe('server plugins', function describeIndexTests() { it('extend request handler context', async () => { - const url = `${PageObjects.common.getHostPort()}/core_plugin_b/`; - await browser.get(url); - - const pageSource = await browser.execute('return window.document.body.textContent;'); - expect(pageSource).to.equal('Pong via plugin A: true'); + await supertest + .get('/core_plugin_b') + .expect(200) + .expect('Pong via plugin A: true'); }); }); } diff --git a/test/plugin_functional/test_suites/core_plugins/ui_plugins.js b/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts similarity index 84% rename from test/plugin_functional/test_suites/core_plugins/ui_plugins.js rename to test/plugin_functional/test_suites/core_plugins/ui_plugins.ts index 15a4dcabddbd..a971921ad3ed 100644 --- a/test/plugin_functional/test_suites/core_plugins/ui_plugins.js +++ b/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts @@ -18,12 +18,14 @@ */ import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; -export default function ({ getService, getPageObjects }) { +// eslint-disable-next-line import/no-default-export +export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { const PageObjects = getPageObjects(['common']); const browser = getService('browser'); - describe('ui plugins', function () { + describe('ui plugins', function() { describe('loading', function describeIndexTests() { before(async () => { await PageObjects.common.navigateToApp('settings'); @@ -40,7 +42,9 @@ export default function ({ getService, getPageObjects }) { }); it('should attach string to window.corePluginB', async () => { - const hasAccessToInjectedMetadata = await browser.execute('return window.hasAccessToInjectedMetadata'); + const hasAccessToInjectedMetadata = await browser.execute( + 'return window.hasAccessToInjectedMetadata' + ); expect(hasAccessToInjectedMetadata).to.equal(true); }); }); @@ -50,7 +54,7 @@ export default function ({ getService, getPageObjects }) { }); it('should attach pluginContext to window.corePluginB', async () => { - const envData = await browser.execute('return window.env'); + const envData: any = await browser.execute('return window.env'); expect(envData.mode.dev).to.be(true); expect(envData.packageInfo.version).to.be.a('string'); }); diff --git a/test/plugin_functional/test_suites/core_plugins/ui_settings.ts b/test/plugin_functional/test_suites/core_plugins/ui_settings.ts new file mode 100644 index 000000000000..2b4227ee798e --- /dev/null +++ b/test/plugin_functional/test_suites/core_plugins/ui_settings.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const PageObjects = getPageObjects(['common']); + const browser = getService('browser'); + const supertest = getService('supertest'); + + describe('ui settings', function() { + before(async () => { + await PageObjects.common.navigateToApp('settings'); + }); + + it('client plugins have access to registered settings', async () => { + const settings = await browser.execute('return window.uiSettingsPlugin'); + expect(settings).to.eql({ + category: ['any'], + description: 'just for testing', + name: 'from_ui_settings_plugin', + value: '2', + }); + const settingsValue = await browser.execute('return window.uiSettingsPluginValue'); + expect(settingsValue).to.be('2'); + }); + + it('server plugins have access to registered settings', async () => { + await supertest + .get('/api/ui-settings-plugin') + .expect(200) + .expect({ uiSettingsValue: 2 }); + }); + }); +} diff --git a/yarn.lock b/yarn.lock index facbec2d7340..f012a9188f99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3045,6 +3045,11 @@ resolved "https://registry.yarnpkg.com/@types/base64-js/-/base64-js-1.2.5.tgz#582b2476169a6cba460a214d476c744441d873d5" integrity sha1-WCskdhaabLpGCiFNR2x0REHYc9U= +"@types/bluebird@*": + version "3.5.28" + resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.28.tgz#04c1a520ff076649236bc8ca21198542ce2bdb09" + integrity sha512-0Vk/kqkukxPKSzP9c8WJgisgGDx5oZDbsLLWIP5t70yThO/YleE+GEm2S1GlRALTaack3O7U5OS5qEm7q2kciA== + "@types/bluebird@^3.1.1": version "3.5.20" resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.20.tgz#f6363172add6f4eabb8cada53ca9af2781e8d6a1" @@ -3986,6 +3991,22 @@ "@types/cookiejar" "*" "@types/node" "*" +"@types/supertest-as-promised@^2.0.38": + version "2.0.38" + resolved "https://registry.yarnpkg.com/@types/supertest-as-promised/-/supertest-as-promised-2.0.38.tgz#5077adf2a31429e06ba8de6799ebdb796ad50fc7" + integrity sha512-2vdlnsZBIgaX0DFNOACK4xFDqvoA1sAR78QD3LiDWGmzSfrRCNt1WFyUYe2Vf0QS03tZf6XC8bNlLaLYXhZbGA== + dependencies: + "@types/bluebird" "*" + "@types/superagent" "*" + "@types/supertest" "*" + +"@types/supertest@*": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.8.tgz#23801236e2b85204ed771a8e7c40febba7da2bda" + integrity sha512-wcax7/ip4XSSJRLbNzEIUVy2xjcBIZZAuSd2vtltQfRK7kxhx5WMHbLHkYdxN3wuQCrwpYrg86/9byDjPXoGMA== + dependencies: + "@types/superagent" "*" + "@types/supertest@^2.0.5": version "2.0.5" resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.5.tgz#18d082a667eaed22759be98f4923e0061ae70c62"