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 <me@joshdover.com>
This commit is contained in:
Mikhail Shustov 2019-10-28 20:32:51 -04:00 committed by GitHub
parent 07caa65860
commit c3f6af3ae3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
96 changed files with 1236 additions and 638 deletions

View file

@ -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.
<b>Signature:</b>

View file

@ -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 <code>path</code> as not requiring authentication |
| [isAnonymous(path)](./kibana-plugin-public.ianonymouspaths.isanonymous.md) | Determines whether the provided path doesn't require authentication. <code>path</code> should include the current basePath. |
| [register(path)](./kibana-plugin-public.ianonymouspaths.register.md) | Register <code>path</code> as not requiring authentication. <code>path</code> should not include the current basePath. |

View file

@ -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.
<b>Signature:</b>

View file

@ -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) |

View file

@ -9,9 +9,9 @@ Gets the metadata about all uiSettings, including the type, default value, and u
<b>Signature:</b>
```typescript
getAll(): UiSettingsState;
getAll(): Record<string, UiSettingsParams & UserProvidedValues<any>>;
```
<b>Returns:</b>
`UiSettingsState`
`Record<string, UiSettingsParams & UserProvidedValues<any>>`

View file

@ -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)
<b>Signature:</b>

View file

@ -19,4 +19,5 @@ export interface CoreSetup
| [context](./kibana-plugin-server.coresetup.context.md) | <code>ContextSetup</code> | [ContextSetup](./kibana-plugin-server.contextsetup.md) |
| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | <code>ElasticsearchServiceSetup</code> | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) |
| [http](./kibana-plugin-server.coresetup.http.md) | <code>HttpServiceSetup</code> | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) |
| [uiSettings](./kibana-plugin-server.coresetup.uisettings.md) | <code>UiSettingsServiceSetup</code> | [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [CoreSetup](./kibana-plugin-server.coresetup.md) &gt; [uiSettings](./kibana-plugin-server.coresetup.uisettings.md)
## CoreSetup.uiSettings property
[UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md)
<b>Signature:</b>
```typescript
uiSettings: UiSettingsServiceSetup;
```

View file

@ -1,13 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) &gt; [getDefaults](./kibana-plugin-server.iuisettingsclient.getdefaults.md)
## IUiSettingsClient.getDefaults property
Returns uiSettings default values [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md)
<b>Signature:</b>
```typescript
getDefaults: () => Record<string, UiSettingsParams>;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) &gt; [getRegistered](./kibana-plugin-server.iuisettingsclient.getregistered.md)
## IUiSettingsClient.getRegistered property
Returns registered uiSettings values [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md)
<b>Signature:</b>
```typescript
getRegistered: () => Readonly<Record<string, UiSettingsParams>>;
```

View file

@ -9,8 +9,5 @@ Retrieves a set of all uiSettings values set by the user.
<b>Signature:</b>
```typescript
getUserProvided: <T extends SavedObjectAttribute = any>() => Promise<Record<string, {
userValue?: T;
isOverridden?: boolean;
}>>;
getUserProvided: <T extends SavedObjectAttribute = any>() => Promise<Record<string, UserProvidedValues<T>>>;
```

View file

@ -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.
<b>Signature:</b>
@ -18,8 +18,8 @@ export interface IUiSettingsClient
| --- | --- | --- |
| [get](./kibana-plugin-server.iuisettingsclient.get.md) | <code>&lt;T extends SavedObjectAttribute = any&gt;(key: string) =&gt; Promise&lt;T&gt;</code> | Retrieves uiSettings values set by the user with fallbacks to default values if not specified. |
| [getAll](./kibana-plugin-server.iuisettingsclient.getall.md) | <code>&lt;T extends SavedObjectAttribute = any&gt;() =&gt; Promise&lt;Record&lt;string, T&gt;&gt;</code> | 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) | <code>() =&gt; Record&lt;string, UiSettingsParams&gt;</code> | Returns uiSettings default values [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) |
| [getUserProvided](./kibana-plugin-server.iuisettingsclient.getuserprovided.md) | <code>&lt;T extends SavedObjectAttribute = any&gt;() =&gt; Promise&lt;Record&lt;string, {</code><br/><code> userValue?: T;</code><br/><code> isOverridden?: boolean;</code><br/><code> }&gt;&gt;</code> | Retrieves a set of all uiSettings values set by the user. |
| [getRegistered](./kibana-plugin-server.iuisettingsclient.getregistered.md) | <code>() =&gt; Readonly&lt;Record&lt;string, UiSettingsParams&gt;&gt;</code> | Returns registered uiSettings values [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) |
| [getUserProvided](./kibana-plugin-server.iuisettingsclient.getuserprovided.md) | <code>&lt;T extends SavedObjectAttribute = any&gt;() =&gt; Promise&lt;Record&lt;string, UserProvidedValues&lt;T&gt;&gt;&gt;</code> | Retrieves a set of all uiSettings values set by the user. |
| [isOverridden](./kibana-plugin-server.iuisettingsclient.isoverridden.md) | <code>(key: string) =&gt; boolean</code> | Shows whether the uiSettings value set by the user. |
| [remove](./kibana-plugin-server.iuisettingsclient.remove.md) | <code>(key: string) =&gt; Promise&lt;void&gt;</code> | Removes uiSettings value by key. |
| [removeMany](./kibana-plugin-server.iuisettingsclient.removemany.md) | <code>(keys: string[]) =&gt; Promise&lt;void&gt;</code> | Removes multiple uiSettings values by keys. |

View file

@ -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

View file

@ -15,5 +15,8 @@ core: {
dataClient: IScopedClusterClient;
adminClient: IScopedClusterClient;
};
uiSettings: {
client: IUiSettingsClient;
};
};
```

View file

@ -18,5 +18,5 @@ export interface RequestHandlerContext
| Property | Type | Description |
| --- | --- | --- |
| [core](./kibana-plugin-server.requesthandlercontext.core.md) | <code>{</code><br/><code> savedObjects: {</code><br/><code> client: SavedObjectsClientContract;</code><br/><code> };</code><br/><code> elasticsearch: {</code><br/><code> dataClient: IScopedClusterClient;</code><br/><code> adminClient: IScopedClusterClient;</code><br/><code> };</code><br/><code> }</code> | |
| [core](./kibana-plugin-server.requesthandlercontext.core.md) | <code>{</code><br/><code> savedObjects: {</code><br/><code> client: SavedObjectsClientContract;</code><br/><code> };</code><br/><code> elasticsearch: {</code><br/><code> dataClient: IScopedClusterClient;</code><br/><code> adminClient: IScopedClusterClient;</code><br/><code> };</code><br/><code> uiSettings: {</code><br/><code> client: IUiSettingsClient;</code><br/><code> };</code><br/><code> }</code> | |

View file

@ -9,5 +9,5 @@ used to group the configured setting in the UI
<b>Signature:</b>
```typescript
category: string[];
category?: string[];
```

View file

@ -9,5 +9,5 @@ description provided to a user in UI
<b>Signature:</b>
```typescript
description: string;
description?: string;
```

View file

@ -20,7 +20,7 @@ export interface UiSettingsParams
| [description](./kibana-plugin-server.uisettingsparams.description.md) | <code>string</code> | description provided to a user in UI |
| [name](./kibana-plugin-server.uisettingsparams.name.md) | <code>string</code> | title in the UI |
| [optionLabels](./kibana-plugin-server.uisettingsparams.optionlabels.md) | <code>Record&lt;string, string&gt;</code> | text labels for 'select' type UI element |
| [options](./kibana-plugin-server.uisettingsparams.options.md) | <code>string[]</code> | a range of valid values |
| [options](./kibana-plugin-server.uisettingsparams.options.md) | <code>string[]</code> | array of permitted values for this setting |
| [readonly](./kibana-plugin-server.uisettingsparams.readonly.md) | <code>boolean</code> | a flag indicating that value cannot be changed |
| [requiresPageReload](./kibana-plugin-server.uisettingsparams.requirespagereload.md) | <code>boolean</code> | a flag indicating whether new value applying requires page reloading |
| [type](./kibana-plugin-server.uisettingsparams.type.md) | <code>UiSettingsType</code> | defines a type of UI element [UiSettingsType](./kibana-plugin-server.uisettingstype.md) |

View file

@ -9,5 +9,5 @@ title in the UI
<b>Signature:</b>
```typescript
name: string;
name?: string;
```

View file

@ -4,7 +4,7 @@
## UiSettingsParams.options property
a range of valid values
array of permitted values for this setting
<b>Signature:</b>

View file

@ -9,5 +9,5 @@ default value to fall back to if a user doesn't provide any
<b>Signature:</b>
```typescript
value: SavedObjectAttribute;
value?: SavedObjectAttribute;
```

View file

@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md)
## UiSettingsServiceSetup interface
<b>Signature:</b>
```typescript
export interface UiSettingsServiceSetup
```
## Methods
| Method | Description |
| --- | --- |
| [register(settings)](./kibana-plugin-server.uisettingsservicesetup.register.md) | Sets settings with default values for the uiSettings. |

View file

@ -0,0 +1,28 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) &gt; [register](./kibana-plugin-server.uisettingsservicesetup.register.md)
## UiSettingsServiceSetup.register() method
Sets settings with default values for the uiSettings.
<b>Signature:</b>
```typescript
register(settings: Record<string, UiSettingsParams>): void;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| settings | <code>Record&lt;string, UiSettingsParams&gt;</code> | |
<b>Returns:</b>
`void`
## Example
setup(core: CoreSetup)<!-- -->{ core.uiSettings.register(\[{ foo: { name: i18n.translate('my foo settings'), value: true, description: 'add some awesomeness', }<!-- -->, }<!-- -->\]); }

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [UserProvidedValues](./kibana-plugin-server.userprovidedvalues.md) &gt; [isOverridden](./kibana-plugin-server.userprovidedvalues.isoverridden.md)
## UserProvidedValues.isOverridden property
<b>Signature:</b>
```typescript
isOverridden?: boolean;
```

View file

@ -0,0 +1,21 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [UserProvidedValues](./kibana-plugin-server.userprovidedvalues.md)
## UserProvidedValues interface
Describes the values explicitly set by user.
<b>Signature:</b>
```typescript
export interface UserProvidedValues<T extends SavedObjectAttribute = any>
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [isOverridden](./kibana-plugin-server.userprovidedvalues.isoverridden.md) | <code>boolean</code> | |
| [userValue](./kibana-plugin-server.userprovidedvalues.uservalue.md) | <code>T</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [UserProvidedValues](./kibana-plugin-server.userprovidedvalues.md) &gt; [userValue](./kibana-plugin-server.userprovidedvalues.uservalue.md)
## UserProvidedValues.userValue property
<b>Signature:</b>
```typescript
userValue?: T;
```

View file

@ -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",

View file

@ -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',

View file

@ -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<string, UiSettingsParams>;
user?: Record<string, UserProvidedValues>;
};
};
};
@ -179,8 +183,8 @@ export interface InjectedMetadataSetup {
serverName: string;
devMode: boolean;
uiSettings: {
defaults: UiSettingsState;
user?: UiSettingsState | undefined;
defaults: Record<string, UiSettingsParams>;
user?: Record<string, UserProvidedValues> | undefined;
};
};
getInjectedVar: (name: string, defaultValue?: any) => unknown;

View file

@ -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<any>;
get(key: string, defaultOverride?: any): any;
getAll(): UiSettingsState;
getAll(): Record<string, UiSettingsParams_2 & UserProvidedValues_2<any>>;
getSaved$(): Rx.Observable<{
key: string;
newValue: any;
@ -979,16 +981,13 @@ export class UiSettingsClient {
stop(): void;
}
// @public (undocumented)
// @public
export type UiSettingsClientContract = PublicMethodsOf<UiSettingsClient>;
// @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;
}

View file

@ -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;
}

View file

@ -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<string, UiSettingsParams>;
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<UiSettingsClient>;
@ -44,8 +51,8 @@ export class UiSettingsClient {
private readonly updateErrors$ = new Rx.Subject<Error>();
private readonly api: UiSettingsApi;
private readonly defaults: UiSettingsState;
private cache: UiSettingsState;
private readonly defaults: Record<string, UiSettingsParams>;
private cache: Record<string, UiSettingsParams & UserProvidedValues>;
constructor(params: UiSettingsClientParams) {
this.api = params.api;

View file

@ -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) {

View file

@ -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;
}
/**

View file

@ -72,7 +72,7 @@ beforeEach(() => {
core: {
context: contextServiceMock.createSetupContract(),
elasticsearch: { legacy: {} } as any,
uiSettings: uiSettingsServiceMock.createSetup(),
uiSettings: uiSettingsServiceMock.createSetupContract(),
http: {
...httpServiceMock.createSetupContract(),
auth: {

View file

@ -248,6 +248,9 @@ export class LegacyService implements CoreService<LegacyServiceSetup> {
basePath: setupDeps.core.http.basePath,
isTlsEnabled: setupDeps.core.http.isTlsEnabled,
},
uiSettings: {
register: setupDeps.core.uiSettings.register,
},
};
const coreStart: CoreStart = {};

View file

@ -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<CoreSetup> = {
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,
};

View file

@ -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());

View file

@ -123,6 +123,9 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
basePath: deps.http.basePath,
isTlsEnabled: deps.http.isTlsEnabled,
},
uiSettings: {
register: deps.uiSettings.register,
},
};
}

View file

@ -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> = PluginsSystem as any;
@ -42,11 +40,7 @@ let configService: ConfigService;
let coreId: symbol;
let env: Env;
let mockPluginSystem: jest.Mocked<PluginsSystem>;
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 => {

View file

@ -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

View file

@ -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());

View file

@ -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<string, UiSettingsParams>): void;
register(settings: Record<string, UiSettingsParams>): void;
}
// @public
@ -763,11 +765,8 @@ export type IScopedClusterClient = Pick<ScopedClusterClient, 'callAsCurrentUser'
export interface IUiSettingsClient {
get: <T extends SavedObjectAttribute = any>(key: string) => Promise<T>;
getAll: <T extends SavedObjectAttribute = any>() => Promise<Record<string, T>>;
getDefaults: () => Record<string, UiSettingsParams>;
getUserProvided: <T extends SavedObjectAttribute = any>() => Promise<Record<string, {
userValue?: T;
isOverridden?: boolean;
}>>;
getRegistered: () => Readonly<Record<string, UiSettingsParams>>;
getUserProvided: <T extends SavedObjectAttribute = any>() => Promise<Record<string, UserProvidedValues<T>>>;
isOverridden: (key: string) => boolean;
remove: (key: string) => Promise<void>;
removeMany: (keys: string[]) => Promise<void>;
@ -1074,6 +1073,9 @@ export interface RequestHandlerContext {
dataClient: IScopedClusterClient;
adminClient: IScopedClusterClient;
};
uiSettings: {
client: IUiSettingsClient;
};
};
}
@ -1610,24 +1612,37 @@ export interface SessionStorageFactory<T> {
// @public
export interface UiSettingsParams {
category: string[];
description: string;
name: string;
category?: string[];
description?: string;
name?: string;
optionLabels?: Record<string, string>;
options?: string[];
readonly?: boolean;
requiresPageReload?: boolean;
type?: UiSettingsType;
value: SavedObjectAttribute;
value?: SavedObjectAttribute;
}
// @public (undocumented)
export interface UiSettingsServiceSetup {
register(settings: Record<string, UiSettingsParams>): void;
}
// @public
export type UiSettingsType = 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string';
// @public
export interface UserProvidedValues<T extends SavedObjectAttribute = any> {
// (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
```

View file

@ -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<RequestHandlerContext['core']> => {
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),
},
};
}
);

View file

@ -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';

View file

@ -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);
});
});
});
});

View file

@ -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?: <T extends SavedObjectAttribute = any>(
error: Error,
attributes: Record<string, any>
) => Record<string, T> | undefined;
handleWriteErrors: boolean;
}
export async function createOrUpgradeSavedConfig<T extends SavedObjectAttribute = any>(
options: Options
): Promise<Record<string, T> | 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<T extends SavedObjectAttribute
// create the new SavedConfig
await savedObjectsClient.create('config', attributes, { id: version });
} catch (error) {
if (onWriteError) {
return onWriteError(error, attributes);
if (handleWriteErrors) {
if (SavedObjectsErrorHelpers.isConflictError(error)) {
return;
}
if (
SavedObjectsErrorHelpers.isNotAuthorizedError(error) ||
SavedObjectsErrorHelpers.isForbiddenError(error)
) {
return attributes;
}
}
throw error;

View file

@ -0,0 +1,86 @@
/*
* 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 { getUpgradeableConfig } from './get_upgradeable_config';
import { savedObjectsClientMock } from '../../saved_objects/service/saved_objects_client.mock';
describe('getUpgradeableConfig', () => {
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);
});
});

View file

@ -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<typeof createTestServers>;
let esServer: UnwrapPromise<ReturnType<typeof servers['startES']>>;
let kbn: UnwrapPromise<ReturnType<typeof servers['startKibana']>>;
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');

View file

@ -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';

View file

@ -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<typeof createTestServers>;
let esServer: UnwrapPromise<ReturnType<typeof servers['startES']>>;
let kbn: UnwrapPromise<ReturnType<typeof servers['startKibana']>>;
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;

View file

@ -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;
}
}
);
}

View file

@ -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;
}
}
);
}

View file

@ -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);
}

View file

@ -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;
}
}
);
}

View file

@ -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;
}
});
}

View file

@ -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<Record<string, UiSettingsParams>>;
/**
* Retrieves uiSettings values set by the user with fallbacks to default values if not specified.
*/
get: <T extends SavedObjectAttribute = any>(key: string) => Promise<T>;
/**
* Retrieves a set of all uiSettings values set by the user with fallbacks to default values if not specified.
*/
getAll: <T extends SavedObjectAttribute = any>() => Promise<Record<string, T>>;
/**
* Retrieves a set of all uiSettings values set by the user.
*/
getUserProvided: <T extends SavedObjectAttribute = any>() => Promise<
Record<string, UserProvidedValues<T>>
>;
/**
* Writes multiple uiSettings values and marks them as set by the user.
*/
setMany: <T extends SavedObjectAttribute = any>(changes: Record<string, T>) => Promise<void>;
/**
* Writes uiSettings value and marks it as set by the user.
*/
set: <T extends SavedObjectAttribute = any>(key: string, value: T) => Promise<void>;
/**
* Removes uiSettings value by key.
*/
remove: (key: string) => Promise<void>;
/**
* Removes multiple uiSettings values by keys.
*/
removeMany: (keys: string[]) => Promise<void>;
/**
* Shows whether the uiSettings value set by the user.
*/
isOverridden: (key: string) => boolean;
}
/**
* Describes the values explicitly set by user.
* @public
* */
export interface UserProvidedValues<T extends SavedObjectAttribute = any> {
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<string, string>;
/** 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<string, UiSettingsParams>): 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<string, UiSettingsParams>): void;
}

View file

@ -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);
});
});
});
});

View file

@ -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<T extends SavedObjectAttribute = any> = Record<string, UserProvidedValue<T>>;
type UiSettingsRaw = Record<string, UiSettingsRawValue>;
/**
* Service that provides access to the UiSettings stored in elasticsearch.
*
* @public
*/
export interface IUiSettingsClient {
/**
* Returns uiSettings default values {@link UiSettingsParams}
*/
getDefaults: () => Record<string, UiSettingsParams>;
/**
* Retrieves uiSettings values set by the user with fallbacks to default values if not specified.
*/
get: <T extends SavedObjectAttribute = any>(key: string) => Promise<T>;
/**
* Retrieves a set of all uiSettings values set by the user with fallbacks to default values if not specified.
*/
getAll: <T extends SavedObjectAttribute = any>() => Promise<Record<string, T>>;
/**
* Retrieves a set of all uiSettings values set by the user.
*/
getUserProvided: <T extends SavedObjectAttribute = any>() => Promise<
Record<string, { userValue?: T; isOverridden?: boolean }>
>;
/**
* Writes multiple uiSettings values and marks them as set by the user.
*/
setMany: <T extends SavedObjectAttribute = any>(changes: Record<string, T>) => Promise<void>;
/**
* Writes uiSettings value and marks it as set by the user.
*/
set: <T extends SavedObjectAttribute = any>(key: string, value: T) => Promise<void>;
/**
* Removes uiSettings value by key.
*/
remove: (key: string) => Promise<void>;
/**
* Removes multiple uiSettings values by keys.
*/
removeMany: (keys: string[]) => Promise<void>;
/**
* 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<UiSettingsRaw> {
const userProvided = await this.getUserProvided();
return defaultsDeep(userProvided, this.defaults);
}
async getUserProvided<T extends SavedObjectAttribute = any>(
options: ReadOptions = {}
): Promise<UserProvided<T>> {
async getUserProvided<T extends SavedObjectAttribute = any>(): Promise<UserProvided<T>> {
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<UiSettingsRaw> {
const userProvided = await this.getUserProvided();
return defaultsDeep(userProvided, this.defaults);
}
private async write<T extends SavedObjectAttribute = any>({
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<Record<string, T>> {
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<T>({
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) {

View file

@ -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);
}
}

View file

@ -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<IUiSettingsClient> = {
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<InternalUiSettingsServiceSetup> = {
setDefaults: jest.fn(),
register: jest.fn(),
asScopedToClient: jest.fn(),
};
@ -48,6 +47,6 @@ const createSetupMock = () => {
};
export const uiSettingsServiceMock = {
createSetup: createSetupMock,
createSetupContract: createSetupMock,
createClient: createClientMock,
};

View file

@ -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"`
);
});
});

View file

@ -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<string, string>;
/** 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<string, UiSettingsParams>): void;
/**
* Creates uiSettings client with provided *scoped* saved objects client {@link IUiSettingsClient}
* @param values
*/
asScopedToClient(savedObjectsClient: SavedObjectsClientContract): IUiSettingsClient;
}
/** @internal */
export class UiSettingsService implements CoreService<InternalUiSettingsServiceSetup> {
private readonly log: Logger;
@ -90,12 +48,13 @@ export class UiSettingsService implements CoreService<InternalUiSettingsServiceS
}
public async setup(deps: SetupDeps): Promise<InternalUiSettingsServiceSetup> {
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<InternalUiSettingsServiceS
public async stop() {}
private setDefaults(values: Record<string, UiSettingsParams> = {}) {
Object.entries(values).forEach(([key, value]) => {
private register(settings: Record<string, UiSettingsParams> = {}) {
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);
});

View file

@ -44,7 +44,7 @@ describe('default route provider', () => {
}
throw Error(`unsupported ui setting: ${key}`);
},
getDefaults: () => {
getRegistered: () => {
return {
defaultRoute: {
value: '/app/kibana',

View file

@ -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;

View file

@ -69,7 +69,7 @@ describe('UiExports', function () {
sandbox
.stub(getUiSettingsServiceForRequestNS, 'getUiSettingsServiceForRequest')
.returns({
getDefaults: noop,
getRegistered: noop,
getUserProvided: noop
});
});

View file

@ -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]);
}
});

View file

@ -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()
});
}

View file

@ -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
);
});

View file

@ -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);
},
},
};

View file

@ -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);
}

View file

@ -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<string, string>) => Promise<void>;
stop: () => Promise<void>;
cleanup: () => Promise<void>;
getClient: () => Client;
getCallCluster: () => CallCluster;
getUrl: () => string;
}
export interface TestElasticsearchUtils {
stop: () => Promise<void>;
es: TestElasticsearchServer;
hosts: string[];
username: string;
password: string;
}
export interface TestKibanaUtils {
root: Root;
kbnServer: KbnServer;
stop: () => Promise<void>;
}
export interface TestUtils {
startES: () => Promise<TestElasticsearchUtils>;
startKibana: () => Promise<TestKibanaUtils>;
}
/**
* 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');
}

View file

@ -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}` });

View file

@ -0,0 +1,8 @@
{
"id": "ui_settings_plugin",
"version": "0.0.1",
"kibanaVersion": "kibana",
"configPath": ["ui_settings_plugin"],
"server": true,
"ui": true
}

View file

@ -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"
}
}

View file

@ -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();

View file

@ -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<string, any>;
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() {}
}

View file

@ -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();

View file

@ -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<number>(
'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);
},
},
};

View file

@ -0,0 +1,15 @@
{
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"../../../../typings/**/*",
],
"exclude": []
}

View file

@ -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<typeof services, {}>;

View file

@ -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);

View file

@ -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 () => {

View file

@ -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'));
});
}

View file

@ -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);

View file

@ -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');
});
});
}

View file

@ -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');
});

View file

@ -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 });
});
});
}

View file

@ -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"