Add SavedObject management section registration in core (#59291)

* add management section to SavedObjectsType

* adapt import/export routes to get types accessor

* add documentation

* update generated doc

* update migration guide

* use request context to access exportable types

* update generated doc

* adapt SavedObjectsManagement to use the registry

* stop magical tricks about the config type, register it as any other so type.

* fix FTR assertions

* fix so_mixin tests

* register the `config` type from the uiSettings service

* nits and comments

* update generated doc

* remove true from dynamic property definition, use force-cast back for config type

* remove obsolete test comment
This commit is contained in:
Pierre Gayvallet 2020-03-10 11:13:45 +01:00 committed by GitHub
parent 28d0cf4485
commit f1272b5ffe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 966 additions and 374 deletions

View file

@ -18,5 +18,5 @@ export interface AuthToolkit
| --- | --- | --- |
| [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | <code>(data?: AuthResultParams) =&gt; AuthResult</code> | Authentication is successful with given credentials, allow request to pass through |
| [notHandled](./kibana-plugin-server.authtoolkit.nothandled.md) | <code>() =&gt; AuthResult</code> | User has no credentials. Allows user to access a resource when authRequired: 'optional' Rejects a request when authRequired: true |
| [redirected](./kibana-plugin-server.authtoolkit.redirected.md) | <code>(headers: {</code><br/><code> location: string;</code><br/><code> } &amp; ResponseHeaders) =&gt; AuthResult</code> | Redirect user to IdP when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional' |
| [redirected](./kibana-plugin-server.authtoolkit.redirected.md) | <code>(headers: {</code><br/><code> location: string;</code><br/><code> } &amp; ResponseHeaders) =&gt; AuthResult</code> | Redirects user to another location to complete authentication when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional' |

View file

@ -4,7 +4,7 @@
## AuthToolkit.redirected property
Redirect user to IdP when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional'
Redirects user to another location to complete authentication when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional'
<b>Signature:</b>

View file

@ -9,5 +9,5 @@ See [SavedObjectTypeRegistry](./kibana-plugin-server.savedobjecttyperegistry.md)
<b>Signature:</b>
```typescript
export declare type ISavedObjectTypeRegistry = Pick<SavedObjectTypeRegistry, 'getType' | 'getAllTypes' | 'getIndex' | 'isNamespaceAgnostic' | 'isHidden'>;
export declare type ISavedObjectTypeRegistry = Pick<SavedObjectTypeRegistry, 'getType' | 'getAllTypes' | 'getIndex' | 'isNamespaceAgnostic' | 'isHidden' | 'getImportableAndExportableTypes' | 'isImportableAndExportable'>;
```

View file

@ -116,7 +116,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [PluginManifest](./kibana-plugin-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. |
| [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | |
| [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | |
| [RequestHandlerContext](./kibana-plugin-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.<!-- -->Provides the following clients: - [rendering](./kibana-plugin-server.iscopedrenderingclient.md) - Rendering client which uses the data of the incoming request - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request |
| [RequestHandlerContext](./kibana-plugin-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.<!-- -->Provides the following clients and services: - [rendering](./kibana-plugin-server.iscopedrenderingclient.md) - Rendering client which uses the data of the incoming request - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request |
| [RouteConfig](./kibana-plugin-server.routeconfig.md) | Route specific configuration. |
| [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Additional route options. |
| [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md) | Additional body options for a route |
@ -164,6 +164,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) | Saved Objects is Kibana's data persistence mechanism allowing plugins to use Elasticsearch for storing and querying state. The SavedObjectsServiceSetup API exposes methods for registering Saved Object types, creating and registering Saved Object client wrappers and factories. |
| [SavedObjectsServiceStart](./kibana-plugin-server.savedobjectsservicestart.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing and querying state. The SavedObjectsServiceStart API provides a scoped Saved Objects client for interacting with Saved Objects. |
| [SavedObjectsType](./kibana-plugin-server.savedobjectstype.md) | |
| [SavedObjectsTypeManagementDefinition](./kibana-plugin-server.savedobjectstypemanagementdefinition.md) | Configuration options for the [type](./kibana-plugin-server.savedobjectstype.md)<!-- -->'s management section. |
| [SavedObjectsTypeMappingDefinition](./kibana-plugin-server.savedobjectstypemappingdefinition.md) | Describe a saved object type mapping. |
| [SavedObjectsUpdateOptions](./kibana-plugin-server.savedobjectsupdateoptions.md) | |
| [SavedObjectsUpdateResponse](./kibana-plugin-server.savedobjectsupdateresponse.md) | |

View file

@ -11,6 +11,7 @@ core: {
rendering: IScopedRenderingClient;
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
dataClient: IScopedClusterClient;

View file

@ -6,7 +6,7 @@
Plugin specific context passed to a route handler.
Provides the following clients: - [rendering](./kibana-plugin-server.iscopedrenderingclient.md) - Rendering client which uses the data of the incoming request - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request
Provides the following clients and services: - [rendering](./kibana-plugin-server.iscopedrenderingclient.md) - Rendering client which uses the data of the incoming request - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request
<b>Signature:</b>
@ -18,5 +18,5 @@ export interface RequestHandlerContext
| Property | Type | Description |
| --- | --- | --- |
| [core](./kibana-plugin-server.requesthandlercontext.core.md) | <code>{</code><br/><code> rendering: IScopedRenderingClient;</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> | |
| [core](./kibana-plugin-server.requesthandlercontext.core.md) | <code>{</code><br/><code> rendering: IScopedRenderingClient;</code><br/><code> savedObjects: {</code><br/><code> client: SavedObjectsClientContract;</code><br/><code> typeRegistry: ISavedObjectTypeRegistry;</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

@ -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; [SavedObjectsType](./kibana-plugin-server.savedobjectstype.md) &gt; [management](./kibana-plugin-server.savedobjectstype.management.md)
## SavedObjectsType.management property
An optional [saved objects management section](./kibana-plugin-server.savedobjectstypemanagementdefinition.md) definition for the type.
<b>Signature:</b>
```typescript
management?: SavedObjectsTypeManagementDefinition;
```

View file

@ -21,6 +21,7 @@ This is only internal for now, and will only be public when we expose the regist
| [convertToAliasScript](./kibana-plugin-server.savedobjectstype.converttoaliasscript.md) | <code>string</code> | If defined, will be used to convert the type to an alias. |
| [hidden](./kibana-plugin-server.savedobjectstype.hidden.md) | <code>boolean</code> | Is the type hidden by default. If true, repositories will not have access to this type unless explicitly declared as an <code>extraType</code> when creating the repository.<!-- -->See [createInternalRepository](./kibana-plugin-server.savedobjectsservicestart.createinternalrepository.md)<!-- -->. |
| [indexPattern](./kibana-plugin-server.savedobjectstype.indexpattern.md) | <code>string</code> | If defined, the type instances will be stored in the given index instead of the default one. |
| [management](./kibana-plugin-server.savedobjectstype.management.md) | <code>SavedObjectsTypeManagementDefinition</code> | An optional [saved objects management section](./kibana-plugin-server.savedobjectstypemanagementdefinition.md) definition for the type. |
| [mappings](./kibana-plugin-server.savedobjectstype.mappings.md) | <code>SavedObjectsTypeMappingDefinition</code> | The [mapping definition](./kibana-plugin-server.savedobjectstypemappingdefinition.md) for the type. |
| [migrations](./kibana-plugin-server.savedobjectstype.migrations.md) | <code>SavedObjectMigrationMap</code> | An optional map of [migrations](./kibana-plugin-server.savedobjectmigrationfn.md) to be used to migrate the type. |
| [name](./kibana-plugin-server.savedobjectstype.name.md) | <code>string</code> | The name of the type, which is also used as the internal id. |

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; [SavedObjectsTypeManagementDefinition](./kibana-plugin-server.savedobjectstypemanagementdefinition.md) &gt; [defaultSearchField](./kibana-plugin-server.savedobjectstypemanagementdefinition.defaultsearchfield.md)
## SavedObjectsTypeManagementDefinition.defaultSearchField property
The default search field to use for this type. Defaults to `id`<!-- -->.
<b>Signature:</b>
```typescript
defaultSearchField?: string;
```

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; [SavedObjectsTypeManagementDefinition](./kibana-plugin-server.savedobjectstypemanagementdefinition.md) &gt; [getEditUrl](./kibana-plugin-server.savedobjectstypemanagementdefinition.getediturl.md)
## SavedObjectsTypeManagementDefinition.getEditUrl property
Function returning the url to use to redirect to the editing page of this object. If not defined, editing will not be allowed.
<b>Signature:</b>
```typescript
getEditUrl?: (savedObject: SavedObject<any>) => string;
```

View file

@ -0,0 +1,16 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [SavedObjectsTypeManagementDefinition](./kibana-plugin-server.savedobjectstypemanagementdefinition.md) &gt; [getInAppUrl](./kibana-plugin-server.savedobjectstypemanagementdefinition.getinappurl.md)
## SavedObjectsTypeManagementDefinition.getInAppUrl property
Function returning the url to use to redirect to this object from the management section. If not defined, redirecting to the object will not be allowed.
<b>Signature:</b>
```typescript
getInAppUrl?: (savedObject: SavedObject<any>) => {
path: string;
uiCapabilitiesPath: string;
};
```

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; [SavedObjectsTypeManagementDefinition](./kibana-plugin-server.savedobjectstypemanagementdefinition.md) &gt; [getTitle](./kibana-plugin-server.savedobjectstypemanagementdefinition.gettitle.md)
## SavedObjectsTypeManagementDefinition.getTitle property
Function returning the title to display in the management table. If not defined, will use the object's type and id to generate a label.
<b>Signature:</b>
```typescript
getTitle?: (savedObject: SavedObject<any>) => string;
```

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; [SavedObjectsTypeManagementDefinition](./kibana-plugin-server.savedobjectstypemanagementdefinition.md) &gt; [icon](./kibana-plugin-server.savedobjectstypemanagementdefinition.icon.md)
## SavedObjectsTypeManagementDefinition.icon property
The eui icon name to display in the management table. If not defined, the default icon will be used.
<b>Signature:</b>
```typescript
icon?: string;
```

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; [SavedObjectsTypeManagementDefinition](./kibana-plugin-server.savedobjectstypemanagementdefinition.md) &gt; [importableAndExportable](./kibana-plugin-server.savedobjectstypemanagementdefinition.importableandexportable.md)
## SavedObjectsTypeManagementDefinition.importableAndExportable property
Is the type importable or exportable. Defaults to `false`<!-- -->.
<b>Signature:</b>
```typescript
importableAndExportable?: boolean;
```

View file

@ -0,0 +1,25 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [SavedObjectsTypeManagementDefinition](./kibana-plugin-server.savedobjectstypemanagementdefinition.md)
## SavedObjectsTypeManagementDefinition interface
Configuration options for the [type](./kibana-plugin-server.savedobjectstype.md)<!-- -->'s management section.
<b>Signature:</b>
```typescript
export interface SavedObjectsTypeManagementDefinition
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [defaultSearchField](./kibana-plugin-server.savedobjectstypemanagementdefinition.defaultsearchfield.md) | <code>string</code> | The default search field to use for this type. Defaults to <code>id</code>. |
| [getEditUrl](./kibana-plugin-server.savedobjectstypemanagementdefinition.getediturl.md) | <code>(savedObject: SavedObject&lt;any&gt;) =&gt; string</code> | Function returning the url to use to redirect to the editing page of this object. If not defined, editing will not be allowed. |
| [getInAppUrl](./kibana-plugin-server.savedobjectstypemanagementdefinition.getinappurl.md) | <code>(savedObject: SavedObject&lt;any&gt;) =&gt; {</code><br/><code> path: string;</code><br/><code> uiCapabilitiesPath: string;</code><br/><code> }</code> | Function returning the url to use to redirect to this object from the management section. If not defined, redirecting to the object will not be allowed. |
| [getTitle](./kibana-plugin-server.savedobjectstypemanagementdefinition.gettitle.md) | <code>(savedObject: SavedObject&lt;any&gt;) =&gt; string</code> | Function returning the title to display in the management table. If not defined, will use the object's type and id to generate a label. |
| [icon](./kibana-plugin-server.savedobjectstypemanagementdefinition.icon.md) | <code>string</code> | The eui icon name to display in the management table. If not defined, the default icon will be used. |
| [importableAndExportable](./kibana-plugin-server.savedobjectstypemanagementdefinition.importableandexportable.md) | <code>boolean</code> | Is the type importable or exportable. Defaults to <code>false</code>. |

View file

@ -4,7 +4,7 @@
## SavedObjectsTypeMappingDefinition.dynamic property
The dynamic property of the mapping. either `false` or 'strict'. Defaults to strict
The dynamic property of the mapping. either `false` or 'strict'. Defaults to `false`
<b>Signature:</b>

View file

@ -41,6 +41,6 @@ const typeDefinition: SavedObjectsTypeMappingDefinition = {
| Property | Type | Description |
| --- | --- | --- |
| [dynamic](./kibana-plugin-server.savedobjectstypemappingdefinition.dynamic.md) | <code>false &#124; 'strict'</code> | The dynamic property of the mapping. either <code>false</code> or 'strict'. Defaults to strict |
| [dynamic](./kibana-plugin-server.savedobjectstypemappingdefinition.dynamic.md) | <code>false &#124; 'strict'</code> | The dynamic property of the mapping. either <code>false</code> or 'strict'. Defaults to <code>false</code> |
| [properties](./kibana-plugin-server.savedobjectstypemappingdefinition.properties.md) | <code>SavedObjectsMappingProperties</code> | The underlying properties of the type mapping |

View file

@ -0,0 +1,17 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [SavedObjectTypeRegistry](./kibana-plugin-server.savedobjecttyperegistry.md) &gt; [getImportableAndExportableTypes](./kibana-plugin-server.savedobjecttyperegistry.getimportableandexportabletypes.md)
## SavedObjectTypeRegistry.getImportableAndExportableTypes() method
Return all [types](./kibana-plugin-server.savedobjectstype.md) currently registered that are importable/exportable.
<b>Signature:</b>
```typescript
getImportableAndExportableTypes(): SavedObjectsType[];
```
<b>Returns:</b>
`SavedObjectsType[]`

View file

@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [SavedObjectTypeRegistry](./kibana-plugin-server.savedobjecttyperegistry.md) &gt; [isImportableAndExportable](./kibana-plugin-server.savedobjecttyperegistry.isimportableandexportable.md)
## SavedObjectTypeRegistry.isImportableAndExportable() method
Returns the `management.importableAndExportable` property for given type, or `false` if the type is not registered or does not define a management section.
<b>Signature:</b>
```typescript
isImportableAndExportable(type: string): boolean;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| type | <code>string</code> | |
<b>Returns:</b>
`boolean`

View file

@ -17,9 +17,11 @@ export declare class SavedObjectTypeRegistry
| Method | Modifiers | Description |
| --- | --- | --- |
| [getAllTypes()](./kibana-plugin-server.savedobjecttyperegistry.getalltypes.md) | | Return all [types](./kibana-plugin-server.savedobjectstype.md) currently registered. |
| [getImportableAndExportableTypes()](./kibana-plugin-server.savedobjecttyperegistry.getimportableandexportabletypes.md) | | Return all [types](./kibana-plugin-server.savedobjectstype.md) currently registered that are importable/exportable. |
| [getIndex(type)](./kibana-plugin-server.savedobjecttyperegistry.getindex.md) | | Returns the <code>indexPattern</code> property for given type, or <code>undefined</code> if the type is not registered. |
| [getType(type)](./kibana-plugin-server.savedobjecttyperegistry.gettype.md) | | Return the [type](./kibana-plugin-server.savedobjectstype.md) definition for given type name. |
| [isHidden(type)](./kibana-plugin-server.savedobjecttyperegistry.ishidden.md) | | Returns the <code>hidden</code> property for given type, or <code>false</code> if the type is not registered. |
| [isImportableAndExportable(type)](./kibana-plugin-server.savedobjecttyperegistry.isimportableandexportable.md) | | Returns the <code>management.importableAndExportable</code> property for given type, or <code>false</code> if the type is not registered or does not define a management section. |
| [isNamespaceAgnostic(type)](./kibana-plugin-server.savedobjecttyperegistry.isnamespaceagnostic.md) | | Returns the <code>namespaceAgnostic</code> property for given type, or <code>false</code> if the type is not registered. |
| [registerType(type)](./kibana-plugin-server.savedobjecttyperegistry.registertype.md) | | Register a [type](./kibana-plugin-server.savedobjectstype.md) inside the registry. A type can only be registered once. subsequent calls with the same type name will throw an error. |

View file

@ -1210,6 +1210,7 @@ In server code, `core` can be accessed from either `server.newPlatform` or `kbnS
| `kibana.Plugin.savedObjectSchemas` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) |
| `kibana.Plugin.mappings` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) |
| `kibana.Plugin.migrations` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) |
| `kibana.Plugin.savedObjectsManagement` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) |
_See also: [Server's CoreSetup API Docs](/docs/development/core/server/kibana-plugin-server.coresetup.md)_

View file

@ -749,7 +749,7 @@ using the core `savedObjects`'s `registerType` setup API.
The most notable difference is that in the new platform, the type registration is performed in a single call to
`registerType`, passing a new `SavedObjectsType` structure that is a superset of the legacy `schema`, `migrations`
and `mappings`.
`mappings` and `savedObjectsManagement`.
### Concrete example
@ -775,6 +775,32 @@ new kibana.Plugin({
isHidden: true,
},
},
savedObjectsManagement: {
'first-type': {
isImportableAndExportable: true,
icon: 'myFirstIcon',
defaultSearchField: 'title',
getTitle(obj) {
return obj.attributes.title;
},
getEditUrl(obj) {
return `/some-url/${encodeURIComponent(obj.id)}`;
},
},
'second-type': {
isImportableAndExportable: false,
icon: 'mySecondIcon',
getTitle(obj) {
return obj.attributes.myTitleField;
},
getInAppUrl(obj) {
return {
path: `/some-url/${encodeURIComponent(obj.id)}`,
uiCapabilitiesPath: 'myPlugin.myType.show',
};
},
},
},
},
})
```
@ -844,6 +870,17 @@ export const firstType: SavedObjectsType = {
'1.0.0': migrateFirstTypeToV1,
'2.0.0': migrateFirstTypeToV2,
},
management: {
importableAndExportable: true,
icon: 'myFirstIcon',
defaultSearchField: 'title',
getTitle(obj) {
return obj.attributes.title;
},
getEditUrl(obj) {
return `/some-url/${encodeURIComponent(obj.id)}`;
},
},
};
```
@ -870,6 +907,19 @@ export const secondType: SavedObjectsType = {
migrations: {
'1.5.0': migrateSecondTypeToV15,
},
management: {
importableAndExportable: false,
icon: 'mySecondIcon',
getTitle(obj) {
return obj.attributes.myTitleField;
},
getInAppUrl(obj) {
return {
path: `/some-url/${encodeURIComponent(obj.id)}`,
uiCapabilitiesPath: 'myPlugin.myType.show',
};
},
},
};
```
@ -895,6 +945,8 @@ The NP `registerType` expected input is very close to the legacy format. However
- The `schema.indexPattern` was accepting either a `string` or a `(config: LegacyConfig) => string`. `SavedObjectsType.indexPattern` only accepts a string, as you can access the configuration during your plugin's setup phase.
- The `savedObjectsManagement.isImportableAndExportable` property has been renamed: `SavedObjectsType.management.importableAndExportable`
- The migration function signature has changed:
In legacy, it was `(doc: SavedObjectUnsanitizedDoc, log: SavedObjectsMigrationLogger) => SavedObjectUnsanitizedDoc;`
In new platform, it is now `(doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc;`

View file

@ -51,7 +51,11 @@ import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plug
import { ContextSetup } from './context';
import { IUiSettingsClient, UiSettingsServiceSetup, UiSettingsServiceStart } from './ui_settings';
import { SavedObjectsClientContract } from './saved_objects/types';
import { SavedObjectsServiceSetup, SavedObjectsServiceStart } from './saved_objects';
import {
ISavedObjectTypeRegistry,
SavedObjectsServiceSetup,
SavedObjectsServiceStart,
} from './saved_objects';
import { CapabilitiesSetup, CapabilitiesStart } from './capabilities';
import { UuidServiceSetup } from './uuid';
import { MetricsServiceSetup } from './metrics';
@ -233,6 +237,7 @@ export {
SavedObjectTypeRegistry,
ISavedObjectTypeRegistry,
SavedObjectsType,
SavedObjectsTypeManagementDefinition,
SavedObjectMigrationMap,
SavedObjectMigrationFn,
exportSavedObjectsToStream,
@ -289,11 +294,13 @@ export {
/**
* Plugin specific context passed to a route handler.
*
* Provides the following clients:
* Provides the following clients and services:
* - {@link IScopedRenderingClient | rendering} - Rendering client
* which uses the data of the incoming request
* - {@link SavedObjectsClient | savedObjects.client} - Saved Objects client
* which uses the credentials of the incoming request
* - {@link ISavedObjectTypeRegistry | savedObjects.typeRegistry} - Type registry containing
* all the registered types.
* - {@link ScopedClusterClient | elasticsearch.dataClient} - Elasticsearch
* data client which uses the credentials of the incoming request
* - {@link ScopedClusterClient | elasticsearch.adminClient} - Elasticsearch
@ -308,6 +315,7 @@ export interface RequestHandlerContext {
rendering: IScopedRenderingClient;
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
dataClient: IScopedClusterClient;

View file

@ -26,6 +26,7 @@ import { httpServiceMock } from './http/http_service.mock';
import { contextServiceMock } from './context/context_service.mock';
import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock';
import { savedObjectsClientMock } from './saved_objects/service/saved_objects_client.mock';
import { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock';
import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
import { SharedGlobalConfig } from './plugins';
import { InternalCoreSetup, InternalCoreStart } from './internal_types';
@ -177,6 +178,7 @@ function createCoreRequestHandlerContextMock() {
},
savedObjects: {
client: savedObjectsClientMock.create(),
typeRegistry: savedObjectsTypeRegistryMock.create(),
},
elasticsearch: {
adminClient: elasticsearchServiceMock.createScopedClusterClient(),

View file

@ -6,6 +6,7 @@ Array [
"convertToAliasScript": undefined,
"hidden": false,
"indexPattern": undefined,
"management": undefined,
"mappings": Object {
"properties": Object {
"fieldA": Object {
@ -21,6 +22,7 @@ Array [
"convertToAliasScript": undefined,
"hidden": false,
"indexPattern": undefined,
"management": undefined,
"mappings": Object {
"properties": Object {
"fieldB": Object {
@ -36,6 +38,7 @@ Array [
"convertToAliasScript": undefined,
"hidden": false,
"indexPattern": undefined,
"management": undefined,
"mappings": Object {
"properties": Object {
"fieldC": Object {
@ -56,6 +59,7 @@ Array [
"convertToAliasScript": undefined,
"hidden": true,
"indexPattern": "myIndex",
"management": undefined,
"mappings": Object {
"properties": Object {
"fieldA": Object {
@ -74,6 +78,7 @@ Array [
"convertToAliasScript": "some alias script",
"hidden": false,
"indexPattern": undefined,
"management": undefined,
"mappings": Object {
"properties": Object {
"anotherFieldB": Object {
@ -92,6 +97,7 @@ Array [
"convertToAliasScript": undefined,
"hidden": false,
"indexPattern": undefined,
"management": undefined,
"mappings": Object {
"properties": Object {
"fieldC": Object {
@ -114,6 +120,7 @@ Array [
"convertToAliasScript": undefined,
"hidden": true,
"indexPattern": "fooBar",
"management": undefined,
"mappings": Object {
"properties": Object {
"fieldA": Object {
@ -129,6 +136,7 @@ Array [
"convertToAliasScript": undefined,
"hidden": false,
"indexPattern": undefined,
"management": undefined,
"mappings": Object {
"properties": Object {
"fieldC": Object {

View file

@ -70,7 +70,7 @@ export {
SavedObjectMigrationContext,
} from './migrations';
export { SavedObjectsType } from './types';
export { SavedObjectsType, SavedObjectsTypeManagementDefinition } from './types';
export { savedObjectsConfig, savedObjectsMigrationConfig } from './saved_objects_config';
export { SavedObjectTypeRegistry, ISavedObjectTypeRegistry } from './saved_objects_type_registry';

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { SavedObjectsManagement, SavedObjectsManagementDefinition } from './management';
export { SavedObjectsManagement } from './management';

View file

@ -24,6 +24,7 @@ const createManagementMock = () => {
const mocked: jest.Mocked<Management> = {
isImportAndExportable: jest.fn().mockReturnValue(true),
getDefaultSearchField: jest.fn(),
getImportableAndExportableTypes: jest.fn(),
getIcon: jest.fn(),
getTitle: jest.fn(),
getEditUrl: jest.fn(),

View file

@ -18,157 +18,185 @@
*/
import { SavedObjectsManagement } from './management';
import { SavedObjectsType } from '../types';
import { SavedObjectTypeRegistry } from '../saved_objects_type_registry';
describe('isImportAndExportable()', () => {
it('returns false for unknown types', () => {
const management = new SavedObjectsManagement();
const result = management.isImportAndExportable('bar');
expect(result).toBe(false);
});
describe('SavedObjectsManagement', () => {
let registry: SavedObjectTypeRegistry;
let management: SavedObjectsManagement;
it('returns true for explicitly importable and exportable type', () => {
const management = new SavedObjectsManagement({
foo: {
isImportableAndExportable: true,
},
const registerType = (type: Partial<SavedObjectsType>) =>
registry.registerType({
name: 'unknown',
hidden: false,
namespaceAgnostic: false,
mappings: { properties: {} },
migrations: {},
...type,
});
const result = management.isImportAndExportable('foo');
expect(result).toBe(true);
beforeEach(() => {
registry = new SavedObjectTypeRegistry();
management = new SavedObjectsManagement(registry);
});
it('returns false for explicitly importable and exportable type', () => {
const management = new SavedObjectsManagement({
foo: {
isImportableAndExportable: false,
},
describe('isImportAndExportable()', () => {
it('returns false for unknown types', () => {
const result = management.isImportAndExportable('bar');
expect(result).toBe(false);
});
const result = management.isImportAndExportable('foo');
expect(result).toBe(false);
});
});
describe('getDefaultSearchField()', () => {
it('returns empty for unknown types', () => {
const management = new SavedObjectsManagement();
const result = management.getDefaultSearchField('bar');
expect(result).toEqual(undefined);
});
it('returns explicit value', () => {
const management = new SavedObjectsManagement({
foo: {
defaultSearchField: 'value',
},
});
const result = management.getDefaultSearchField('foo');
expect(result).toEqual('value');
});
});
describe('getIcon', () => {
it('returns empty for unknown types', () => {
const management = new SavedObjectsManagement();
const result = management.getIcon('bar');
expect(result).toEqual(undefined);
});
it('returns explicit value', () => {
const management = new SavedObjectsManagement({
foo: {
icon: 'value',
},
});
const result = management.getIcon('foo');
expect(result).toEqual('value');
});
});
describe('getTitle', () => {
it('returns empty for unknown type', () => {
const management = new SavedObjectsManagement();
const result = management.getTitle({
id: '1',
type: 'foo',
attributes: {},
references: [],
});
expect(result).toEqual(undefined);
});
it('returns explicit value', () => {
const management = new SavedObjectsManagement({
foo: {
getTitle() {
return 'called';
it('returns true for explicitly importable and exportable type', () => {
registerType({
name: 'foo',
management: {
importableAndExportable: true,
},
},
});
const result = management.getTitle({
id: '1',
type: 'foo',
attributes: {},
references: [],
});
expect(result).toEqual('called');
});
});
});
describe('getEditUrl()', () => {
it('returns empty for unknown type', () => {
const management = new SavedObjectsManagement();
const result = management.getEditUrl({
id: '1',
type: 'foo',
attributes: {},
references: [],
const result = management.isImportAndExportable('foo');
expect(result).toBe(true);
});
expect(result).toEqual(undefined);
});
it('returns explicit value', () => {
const management = new SavedObjectsManagement({
foo: {
getEditUrl() {
return 'called';
it('returns false for explicitly importable and exportable type', () => {
registerType({
name: 'foo',
management: {
importableAndExportable: false,
},
},
});
const result = management.getEditUrl({
id: '1',
type: 'foo',
attributes: {},
references: [],
});
expect(result).toEqual('called');
});
});
});
describe('getInAppUrl()', () => {
it('returns empty array for unknown type', () => {
const management = new SavedObjectsManagement();
const result = management.getInAppUrl({
id: '1',
type: 'foo',
attributes: {},
references: [],
const result = management.isImportAndExportable('foo');
expect(result).toBe(false);
});
expect(result).toEqual(undefined);
});
it('returns explicit value', () => {
const management = new SavedObjectsManagement({
foo: {
getInAppUrl() {
return { path: 'called', uiCapabilitiesPath: 'my.path' };
describe('getDefaultSearchField()', () => {
it('returns empty for unknown types', () => {
const result = management.getDefaultSearchField('bar');
expect(result).toEqual(undefined);
});
it('returns explicit value', () => {
registerType({
name: 'foo',
management: {
defaultSearchField: 'value',
},
},
});
const result = management.getDefaultSearchField('foo');
expect(result).toEqual('value');
});
const result = management.getInAppUrl({
id: '1',
type: 'foo',
attributes: {},
references: [],
});
describe('getIcon()', () => {
it('returns empty for unknown types', () => {
const result = management.getIcon('bar');
expect(result).toEqual(undefined);
});
it('returns explicit value', () => {
registerType({
name: 'foo',
management: {
icon: 'value',
},
});
const result = management.getIcon('foo');
expect(result).toEqual('value');
});
});
describe('getTitle()', () => {
it('returns empty for unknown type', () => {
const result = management.getTitle({
id: '1',
type: 'foo',
attributes: {},
references: [],
});
expect(result).toEqual(undefined);
});
it('returns explicit value', () => {
registerType({
name: 'foo',
management: {
getTitle() {
return 'called';
},
},
});
const result = management.getTitle({
id: '1',
type: 'foo',
attributes: {},
references: [],
});
expect(result).toEqual('called');
});
});
describe('getEditUrl()', () => {
it('returns empty for unknown type', () => {
const result = management.getEditUrl({
id: '1',
type: 'foo',
attributes: {},
references: [],
});
expect(result).toEqual(undefined);
});
it('returns explicit value', () => {
registerType({
name: 'foo',
management: {
getEditUrl() {
return 'called';
},
},
});
const result = management.getEditUrl({
id: '1',
type: 'foo',
attributes: {},
references: [],
});
expect(result).toEqual('called');
});
});
describe('getInAppUrl()', () => {
it('returns empty array for unknown type', () => {
const result = management.getInAppUrl({
id: '1',
type: 'foo',
attributes: {},
references: [],
});
expect(result).toEqual(undefined);
});
it('returns explicit value', () => {
registerType({
name: 'foo',
management: {
getInAppUrl() {
return { path: 'called', uiCapabilitiesPath: 'my.path' };
},
},
});
const result = management.getInAppUrl({
id: '1',
type: 'foo',
attributes: {},
references: [],
});
expect(result).toEqual({ path: 'called', uiCapabilitiesPath: 'my.path' });
});
expect(result).toEqual({ path: 'called', uiCapabilitiesPath: 'my.path' });
});
});

View file

@ -18,74 +18,42 @@
*/
import { SavedObject } from '../types';
interface SavedObjectsManagementTypeDefinition {
isImportableAndExportable?: boolean;
defaultSearchField?: string;
icon?: string;
getTitle?: (savedObject: SavedObject<any>) => string;
getEditUrl?: (savedObject: SavedObject<any>) => string;
getInAppUrl?: (savedObject: SavedObject<any>) => { path: string; uiCapabilitiesPath: string };
}
export interface SavedObjectsManagementDefinition {
[key: string]: SavedObjectsManagementTypeDefinition;
}
import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry';
export class SavedObjectsManagement {
private readonly definition?: SavedObjectsManagementDefinition;
constructor(private readonly registry: ISavedObjectTypeRegistry) {}
constructor(managementDefinition?: SavedObjectsManagementDefinition) {
this.definition = managementDefinition;
public getImportableAndExportableTypes() {
return this.registry
.getAllTypes()
.map(type => type.name)
.filter(type => this.isImportAndExportable(type));
}
public isImportAndExportable(type: string) {
if (this.definition && this.definition.hasOwnProperty(type)) {
return this.definition[type].isImportableAndExportable === true;
}
return false;
return this.registry.isImportableAndExportable(type);
}
public getDefaultSearchField(type: string) {
if (this.definition && this.definition.hasOwnProperty(type)) {
return this.definition[type].defaultSearchField;
}
return this.registry.getType(type)?.management?.defaultSearchField;
}
public getIcon(type: string) {
if (this.definition && this.definition.hasOwnProperty(type)) {
return this.definition[type].icon;
}
return this.registry.getType(type)?.management?.icon;
}
public getTitle(savedObject: SavedObject) {
const { type } = savedObject;
if (this.definition && this.definition.hasOwnProperty(type) && this.definition[type].getTitle) {
const { getTitle } = this.definition[type];
if (getTitle) {
return getTitle(savedObject);
}
}
const getTitle = this.registry.getType(savedObject.type)?.management?.getTitle;
return getTitle ? getTitle(savedObject) : undefined;
}
public getEditUrl(savedObject: SavedObject) {
const { type } = savedObject;
if (this.definition && this.definition.hasOwnProperty(type)) {
const { getEditUrl } = this.definition[type];
if (getEditUrl) {
return getEditUrl(savedObject);
}
}
const getEditUrl = this.registry.getType(savedObject.type)?.management?.getEditUrl;
return getEditUrl ? getEditUrl(savedObject) : undefined;
}
public getInAppUrl(savedObject: SavedObject) {
const { type } = savedObject;
if (this.definition && this.definition.hasOwnProperty(type)) {
const { getInAppUrl } = this.definition[type];
if (getInAppUrl) {
return getInAppUrl(savedObject);
}
}
const getInAppUrl = this.registry.getType(savedObject.type)?.management?.getInAppUrl;
return getInAppUrl ? getInAppUrl(savedObject) : undefined;
}
}

View file

@ -45,7 +45,7 @@
* @public
*/
export interface SavedObjectsTypeMappingDefinition {
/** The dynamic property of the mapping. either `false` or 'strict'. Defaults to strict */
/** The dynamic property of the mapping. either `false` or 'strict'. Defaults to `false` */
dynamic?: false | 'strict';
/** The underlying properties of the type mapping */
properties: SavedObjectsMappingProperties;

View file

@ -6,7 +6,6 @@ Object {
"migrationMappingPropertyHashes": Object {
"aaa": "625b32086eb1d1203564cf85062dd22e",
"bbb": "18c78c995965207ed3f6e7fc5c6e55fe",
"config": "87aca8fdb053154f11383fce3dbf3edf",
"migrationVersion": "4a1746014a75ade3a714e1db5763276f",
"namespace": "2f4316de49999235636386fe51dc06c1",
"references": "7997cf5a56cc02bdc9c93361bde732b0",
@ -22,14 +21,6 @@ Object {
"bbb": Object {
"type": "long",
},
"config": Object {
"dynamic": "true",
"properties": Object {
"buildNum": Object {
"type": "keyword",
},
},
},
"migrationVersion": Object {
"dynamic": "true",
"type": "object",
@ -65,7 +56,6 @@ exports[`buildActiveMappings handles the \`dynamic\` property of types 1`] = `
Object {
"_meta": Object {
"migrationMappingPropertyHashes": Object {
"config": "87aca8fdb053154f11383fce3dbf3edf",
"firstType": "635418ab953d81d93f1190b70a8d3f57",
"migrationVersion": "4a1746014a75ade3a714e1db5763276f",
"namespace": "2f4316de49999235636386fe51dc06c1",
@ -78,14 +68,6 @@ Object {
},
"dynamic": "strict",
"properties": Object {
"config": Object {
"dynamic": "true",
"properties": Object {
"buildNum": Object {
"type": "keyword",
},
},
},
"firstType": Object {
"dynamic": "strict",
"properties": Object {

View file

@ -132,14 +132,6 @@ function defaultMapping(): IndexMapping {
return {
dynamic: 'strict',
properties: {
config: {
dynamic: 'true',
properties: {
buildNum: {
type: 'keyword',
},
},
},
migrationVersion: {
dynamic: 'true',
type: 'object',

View file

@ -58,7 +58,6 @@ describe('IndexMigrator', () => {
dynamic: 'strict',
_meta: {
migrationMappingPropertyHashes: {
config: '87aca8fdb053154f11383fce3dbf3edf',
foo: '18c78c995965207ed3f6e7fc5c6e55fe',
migrationVersion: '4a1746014a75ade3a714e1db5763276f',
namespace: '2f4316de49999235636386fe51dc06c1',
@ -68,10 +67,6 @@ describe('IndexMigrator', () => {
},
},
properties: {
config: {
dynamic: 'true',
properties: { buildNum: { type: 'keyword' } },
},
foo: { type: 'long' },
migrationVersion: { dynamic: 'true', type: 'object' },
namespace: { type: 'keyword' },
@ -180,7 +175,6 @@ describe('IndexMigrator', () => {
dynamic: 'strict',
_meta: {
migrationMappingPropertyHashes: {
config: '87aca8fdb053154f11383fce3dbf3edf',
foo: '625b32086eb1d1203564cf85062dd22e',
migrationVersion: '4a1746014a75ade3a714e1db5763276f',
namespace: '2f4316de49999235636386fe51dc06c1',
@ -191,10 +185,6 @@ describe('IndexMigrator', () => {
},
properties: {
author: { type: 'text' },
config: {
dynamic: 'true',
properties: { buildNum: { type: 'keyword' } },
},
foo: { type: 'text' },
migrationVersion: { dynamic: 'true', type: 'object' },
namespace: { type: 'keyword' },

View file

@ -6,7 +6,6 @@ Object {
"migrationMappingPropertyHashes": Object {
"amap": "510f1f0adb69830cf8a1c5ce2923ed82",
"bmap": "510f1f0adb69830cf8a1c5ce2923ed82",
"config": "87aca8fdb053154f11383fce3dbf3edf",
"migrationVersion": "4a1746014a75ade3a714e1db5763276f",
"namespace": "2f4316de49999235636386fe51dc06c1",
"references": "7997cf5a56cc02bdc9c93361bde732b0",
@ -30,14 +29,6 @@ Object {
},
},
},
"config": Object {
"dynamic": "true",
"properties": Object {
"buildNum": Object {
"type": "keyword",
},
},
},
"migrationVersion": Object {
"dynamic": "true",
"type": "object",

View file

@ -27,32 +27,21 @@ import {
import { IRouter } from '../../http';
import { SavedObjectConfig } from '../saved_objects_config';
import { exportSavedObjectsToStream } from '../export';
import { validateTypes, validateObjects } from './utils';
export const registerExportRoute = (
router: IRouter,
config: SavedObjectConfig,
supportedTypes: string[]
) => {
export const registerExportRoute = (router: IRouter, config: SavedObjectConfig) => {
const { maxImportExportSize } = config;
const typeSchema = schema.string({
validate: (type: string) => {
if (!supportedTypes.includes(type)) {
return `${type} is not exportable`;
}
},
});
router.post(
{
path: '/_export',
validate: {
body: schema.object({
type: schema.maybe(schema.oneOf([typeSchema, schema.arrayOf(typeSchema)])),
type: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])),
objects: schema.maybe(
schema.arrayOf(
schema.object({
type: typeSchema,
type: schema.string(),
id: schema.string(),
}),
{ maxSize: maxImportExportSize }
@ -67,9 +56,36 @@ export const registerExportRoute = (
router.handleLegacyErrors(async (context, req, res) => {
const savedObjectsClient = context.core.savedObjects.client;
const { type, objects, search, excludeExportDetails, includeReferencesDeep } = req.body;
const types = typeof type === 'string' ? [type] : type;
// need to access the registry for type validation, can't use the schema for this
const supportedTypes = context.core.savedObjects.typeRegistry
.getImportableAndExportableTypes()
.map(t => t.name);
if (types) {
const validationError = validateTypes(types, supportedTypes);
if (validationError) {
return res.badRequest({
body: {
message: validationError,
},
});
}
}
if (objects) {
const validationError = validateObjects(objects, supportedTypes);
if (validationError) {
return res.badRequest({
body: {
message: validationError,
},
});
}
}
const exportStream = await exportSavedObjectsToStream({
savedObjectsClient,
types: typeof type === 'string' ? [type] : type,
types,
search,
objects,
exportSizeLimit: maxImportExportSize,

View file

@ -31,11 +31,7 @@ interface FileStream extends Readable {
};
}
export const registerImportRoute = (
router: IRouter,
config: SavedObjectConfig,
supportedTypes: string[]
) => {
export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) => {
const { maxImportExportSize, maxImportPayloadBytes } = config;
router.post(
@ -65,6 +61,10 @@ export const registerImportRoute = (
return res.badRequest({ body: `Invalid file extension ${fileExtension}` });
}
const supportedTypes = context.core.savedObjects.typeRegistry
.getImportableAndExportableTypes()
.map(type => type.name);
const result = await importSavedObjectsFromStream({
supportedTypes,
savedObjectsClient: context.core.savedObjects.client,

View file

@ -39,13 +39,11 @@ export function registerRoutes({
http,
logger,
config,
importableExportableTypes,
migratorPromise,
}: {
http: InternalHttpServiceSetup;
logger: Logger;
config: SavedObjectConfig;
importableExportableTypes: string[];
migratorPromise: Promise<IKibanaMigrator>;
}) {
const router = http.createRouter('/api/saved_objects/');
@ -59,9 +57,9 @@ export function registerRoutes({
registerBulkCreateRoute(router);
registerBulkUpdateRoute(router);
registerLogLegacyImportRoute(router, logger);
registerExportRoute(router, config, importableExportableTypes);
registerImportRoute(router, config, importableExportableTypes);
registerResolveImportErrorsRoute(router, config, importableExportableTypes);
registerExportRoute(router, config);
registerImportRoute(router, config);
registerResolveImportErrorsRoute(router, config);
const internalRouter = http.createRouter('/internal/saved_objects/');

View file

@ -27,7 +27,7 @@ import supertest from 'supertest';
import { UnwrapPromise } from '@kbn/utility-types';
import { SavedObjectConfig } from '../../saved_objects_config';
import { registerExportRoute } from '../export';
import { setupServer } from './test_utils';
import { setupServer, createExportableType } from './test_utils';
type setupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
const exportSavedObjectsToStream = exportMock.exportSavedObjectsToStream as jest.Mock;
@ -40,12 +40,16 @@ const config = {
describe('POST /api/saved_objects/_export', () => {
let server: setupServerReturn['server'];
let httpSetup: setupServerReturn['httpSetup'];
let handlerContext: setupServerReturn['handlerContext'];
beforeEach(async () => {
({ server, httpSetup } = await setupServer());
({ server, httpSetup, handlerContext } = await setupServer());
handlerContext.savedObjects.typeRegistry.getImportableAndExportableTypes.mockReturnValue(
allowedTypes.map(createExportableType)
);
const router = httpSetup.createRouter('/api/saved_objects/');
registerExportRoute(router, config, allowedTypes);
registerExportRoute(router, config);
await server.start();
});

View file

@ -22,7 +22,7 @@ import { UnwrapPromise } from '@kbn/utility-types';
import { registerImportRoute } from '../import';
import { savedObjectsClientMock } from '../../../../../core/server/mocks';
import { SavedObjectConfig } from '../../saved_objects_config';
import { setupServer } from './test_utils';
import { setupServer, createExportableType } from './test_utils';
type setupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
@ -47,12 +47,15 @@ describe('POST /internal/saved_objects/_import', () => {
beforeEach(async () => {
({ server, httpSetup, handlerContext } = await setupServer());
savedObjectsClient = handlerContext.savedObjects.client;
handlerContext.savedObjects.typeRegistry.getImportableAndExportableTypes.mockReturnValue(
allowedTypes.map(createExportableType)
);
savedObjectsClient = handlerContext.savedObjects.client;
savedObjectsClient.find.mockResolvedValue(emptyResponse);
const router = httpSetup.createRouter('/internal/saved_objects/');
registerImportRoute(router, config, allowedTypes);
registerImportRoute(router, config);
await server.start();
});

View file

@ -21,7 +21,7 @@ import supertest from 'supertest';
import { UnwrapPromise } from '@kbn/utility-types';
import { registerResolveImportErrorsRoute } from '../resolve_import_errors';
import { savedObjectsClientMock } from '../../../../../core/server/mocks';
import { setupServer } from './test_utils';
import { setupServer, createExportableType } from './test_utils';
import { SavedObjectConfig } from '../../saved_objects_config';
type setupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
@ -40,10 +40,14 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
beforeEach(async () => {
({ server, httpSetup, handlerContext } = await setupServer());
handlerContext.savedObjects.typeRegistry.getImportableAndExportableTypes.mockReturnValue(
allowedTypes.map(createExportableType)
);
savedObjectsClient = handlerContext.savedObjects.client;
const router = httpSetup.createRouter('/api/saved_objects/');
registerResolveImportErrorsRoute(router, config, allowedTypes);
registerResolveImportErrorsRoute(router, config);
await server.start();
});

View file

@ -20,6 +20,7 @@
import { ContextService } from '../../../context';
import { createHttpServer, createCoreContext } from '../../../http/test_utils';
import { coreMock } from '../../../mocks';
import { SavedObjectsType } from '../../types';
const coreId = Symbol('core');
@ -43,3 +44,17 @@ export const setupServer = async () => {
handlerContext,
};
};
export const createExportableType = (name: string): SavedObjectsType => {
return {
name,
hidden: false,
namespaceAgnostic: false,
mappings: {
properties: {},
},
management: {
importableAndExportable: true,
},
};
};

View file

@ -31,11 +31,7 @@ interface FileStream extends Readable {
};
}
export const registerResolveImportErrorsRoute = (
router: IRouter,
config: SavedObjectConfig,
supportedTypes: string[]
) => {
export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedObjectConfig) => {
const { maxImportExportSize, maxImportPayloadBytes } = config;
router.post(
@ -75,6 +71,11 @@ export const registerResolveImportErrorsRoute = (
if (fileExtension !== '.ndjson') {
return res.badRequest({ body: `Invalid file extension ${fileExtension}` });
}
const supportedTypes = context.core.savedObjects.typeRegistry
.getImportableAndExportableTypes()
.map(type => type.name);
const result = await resolveSavedObjectsImportErrors({
supportedTypes,
savedObjectsClient: context.core.savedObjects.client,

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { createSavedObjectsStreamFromNdJson } from './utils';
import { createSavedObjectsStreamFromNdJson, validateTypes, validateObjects } from './utils';
import { Readable } from 'stream';
import { createPromiseFromStreams, createConcatStream } from '../../../../legacy/utils/streams';
@ -104,3 +104,53 @@ describe('createSavedObjectsStreamFromNdJson', () => {
]);
});
});
describe('validateTypes', () => {
const allowedTypes = ['config', 'index-pattern', 'dashboard'];
it('returns an error message if some types are not allowed', () => {
expect(validateTypes(['config', 'not-allowed-type'], allowedTypes)).toMatchInlineSnapshot(
`"Trying to export non-exportable type(s): not-allowed-type"`
);
expect(
validateTypes(['index-pattern', 'not-allowed-type', 'not-allowed-type-2'], allowedTypes)
).toMatchInlineSnapshot(
`"Trying to export non-exportable type(s): not-allowed-type, not-allowed-type-2"`
);
});
it('returns undefined if all types are allowed', () => {
expect(validateTypes(allowedTypes, allowedTypes)).toBeUndefined();
expect(validateTypes(['config'], allowedTypes)).toBeUndefined();
});
});
describe('validateObjects', () => {
const allowedTypes = ['config', 'index-pattern', 'dashboard'];
it('returns an error message if some objects have types that are not allowed', () => {
expect(
validateObjects(
[
{ id: '1', type: 'config' },
{ id: '1', type: 'not-allowed' },
{ id: '42', type: 'not-allowed-either' },
],
allowedTypes
)
).toMatchInlineSnapshot(
`"Trying to export object(s) with non-exportable types: not-allowed:1, not-allowed-either:42"`
);
});
it('returns undefined if all objects have allowed types', () => {
expect(
validateObjects(
[
{ id: '1', type: 'config' },
{ id: '2', type: 'config' },
{ id: '1', type: 'index-pattern' },
],
allowedTypes
)
).toBeUndefined();
});
});

View file

@ -41,3 +41,22 @@ export function createSavedObjectsStreamFromNdJson(ndJsonStream: Readable) {
)
);
}
export function validateTypes(types: string[], supportedTypes: string[]): string | undefined {
const invalidTypes = types.filter(t => !supportedTypes.includes(t));
if (invalidTypes.length) {
return `Trying to export non-exportable type(s): ${invalidTypes.join(', ')}`;
}
}
export function validateObjects(
objects: Array<{ id: string; type: string }>,
supportedTypes: string[]
): string | undefined {
const invalidObjects = objects.filter(obj => !supportedTypes.includes(obj.type));
if (invalidObjects.length) {
return `Trying to export object(s) with non-exportable types: ${invalidObjects
.map(obj => `${obj.type}:${obj.id}`)
.join(', ')}`;
}
}

View file

@ -23,7 +23,7 @@ import {
clientProviderInstanceMock,
typeRegistryInstanceMock,
} from './saved_objects_service.test.mocks';
import { BehaviorSubject } from 'rxjs';
import { ByteSizeValue } from '@kbn/config-schema';
import { SavedObjectsService } from './saved_objects_service';
import { mockCoreContext } from '../core_context.mock';
@ -34,7 +34,6 @@ import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service
import { legacyServiceMock } from '../legacy/legacy_service.mock';
import { httpServiceMock } from '../http/http_service.mock';
import { SavedObjectsClientFactoryProvider } from './service/lib';
import { BehaviorSubject } from 'rxjs';
import { NodesVersionCompatibility } from '../elasticsearch/version_check/ensure_es_version';
describe('SavedObjectsService', () => {

View file

@ -38,7 +38,7 @@ import {
SavedObjectConfig,
} from './saved_objects_config';
import { KibanaRequest, InternalHttpServiceSetup } from '../http';
import { SavedObjectsClientContract, SavedObjectsType, SavedObjectsLegacyUiExports } from './types';
import { SavedObjectsClientContract, SavedObjectsType } from './types';
import { ISavedObjectsRepository, SavedObjectsRepository } from './service/lib/repository';
import {
SavedObjectsClientFactoryProvider,
@ -301,10 +301,6 @@ export class SavedObjectsService
legacyTypes.forEach(type => this.typeRegistry.registerType(type));
this.validations = setupDeps.legacyPlugins.uiExports.savedObjectValidations || {};
const importableExportableTypes = getImportableAndExportableTypes(
setupDeps.legacyPlugins.uiExports
);
const savedObjectsConfig = await this.coreContext.configService
.atPath<SavedObjectsConfigType>('savedObjects')
.pipe(first())
@ -320,7 +316,6 @@ export class SavedObjectsService
logger: this.logger,
config: this.config,
migratorPromise: this.migrator$.pipe(first()).toPromise(),
importableExportableTypes,
});
return {
@ -479,16 +474,3 @@ export class SavedObjectsService
});
}
}
function getImportableAndExportableTypes({
savedObjectMappings = [],
savedObjectsManagement = {},
}: SavedObjectsLegacyUiExports) {
const visibleTypes = savedObjectMappings.reduce(
(types, mapping) => [...types, ...Object.keys(mapping.properties)],
[] as string[]
);
return visibleTypes.filter(
type => savedObjectsManagement[type]?.isImportableAndExportable === true ?? false
);
}

View file

@ -25,14 +25,20 @@ const createRegistryMock = (): jest.Mocked<ISavedObjectTypeRegistry &
registerType: jest.fn(),
getType: jest.fn(),
getAllTypes: jest.fn(),
getImportableAndExportableTypes: jest.fn(),
isNamespaceAgnostic: jest.fn(),
isHidden: jest.fn(),
getIndex: jest.fn(),
isImportableAndExportable: jest.fn(),
};
mock.getAllTypes.mockReturnValue([]);
mock.getImportableAndExportableTypes.mockReturnValue([]);
mock.getIndex.mockReturnValue('.kibana-test');
mock.getIndex.mockReturnValue('.kibana-test');
mock.isHidden.mockReturnValue(false);
mock.isNamespaceAgnostic.mockImplementation((type: string) => type === 'global');
mock.isImportableAndExportable.mockReturnValue(true);
return mock;
};

View file

@ -212,4 +212,45 @@ describe('SavedObjectTypeRegistry', () => {
expect(registry.getIndex('unknownType')).toBeUndefined();
});
});
describe('#isImportableAndExportable', () => {
it('returns correct value for the type', () => {
registry.registerType(
createType({ name: 'typeA', management: { importableAndExportable: true } })
);
registry.registerType(
createType({ name: 'typeB', management: { importableAndExportable: false } })
);
expect(registry.isImportableAndExportable('typeA')).toBe(true);
expect(registry.isImportableAndExportable('typeB')).toBe(false);
});
it('returns false when the type is not registered', () => {
registry.registerType(createType({ name: 'typeA', management: {} }));
registry.registerType(createType({ name: 'typeB', management: {} }));
expect(registry.isImportableAndExportable('typeA')).toBe(false);
});
it('returns false when management is not defined for the type', () => {
registry.registerType(createType({ name: 'typeA' }));
expect(registry.isImportableAndExportable('unknownType')).toBe(false);
});
});
describe('#getImportableAndExportableTypes', () => {
it('returns all registered types that are importable/exportable', () => {
const typeA = createType({ name: 'typeA', management: { importableAndExportable: true } });
const typeB = createType({ name: 'typeB' });
const typeC = createType({ name: 'typeC', management: { importableAndExportable: false } });
const typeD = createType({ name: 'typeD', management: { importableAndExportable: true } });
registry.registerType(typeA);
registry.registerType(typeB);
registry.registerType(typeC);
registry.registerType(typeD);
const types = registry.getImportableAndExportableTypes();
expect(types.length).toEqual(2);
expect(types.map(t => t.name)).toEqual(['typeA', 'typeD']);
});
});
});

View file

@ -27,7 +27,13 @@ import { SavedObjectsType } from './types';
*/
export type ISavedObjectTypeRegistry = Pick<
SavedObjectTypeRegistry,
'getType' | 'getAllTypes' | 'getIndex' | 'isNamespaceAgnostic' | 'isHidden'
| 'getType'
| 'getAllTypes'
| 'getIndex'
| 'isNamespaceAgnostic'
| 'isHidden'
| 'getImportableAndExportableTypes'
| 'isImportableAndExportable'
>;
/**
@ -63,6 +69,13 @@ export class SavedObjectTypeRegistry {
return [...this.types.values()];
}
/**
* Return all {@link SavedObjectsType | types} currently registered that are importable/exportable.
*/
public getImportableAndExportableTypes() {
return this.getAllTypes().filter(type => this.isImportableAndExportable(type.name));
}
/**
* Returns the `namespaceAgnostic` property for given type, or `false` if
* the type is not registered.
@ -86,4 +99,12 @@ export class SavedObjectTypeRegistry {
public getIndex(type: string) {
return this.types.get(type)?.indexPattern;
}
/**
* Returns the `management.importableAndExportable` property for given type, or
* `false` if the type is not registered or does not define a management section.
*/
public isImportableAndExportable(type: string) {
return this.types.get(type)?.management?.importableAndExportable ?? false;
}
}

View file

@ -102,7 +102,6 @@ describe('SavedObjectsRepository#createRepository', () => {
expect(repository).toBeDefined();
expect(RepositoryConstructor.mock.calls[0][0].allowedTypes).toMatchInlineSnapshot(`
Array [
"config",
"nsAgnosticType",
"nsType",
]
@ -121,7 +120,6 @@ describe('SavedObjectsRepository#createRepository', () => {
expect(repository).toBeDefined();
expect(RepositoryConstructor.mock.calls[0][0].allowedTypes).toMatchInlineSnapshot(`
Array [
"config",
"nsAgnosticType",
"nsType",
"hiddenType",

View file

@ -21,7 +21,6 @@ import { SavedObjectsClient } from './service/saved_objects_client';
import { SavedObjectsTypeMappingDefinition, SavedObjectsTypeMappingDefinitions } from './mappings';
import { SavedObjectMigrationMap } from './migrations';
import { PropertyValidators } from './validation';
import { SavedObjectsManagementDefinition } from './management';
export {
SavedObjectsImportResponse,
@ -246,6 +245,50 @@ export interface SavedObjectsType {
* An optional map of {@link SavedObjectMigrationFn | migrations} to be used to migrate the type.
*/
migrations?: SavedObjectMigrationMap;
/**
* An optional {@link SavedObjectsTypeManagementDefinition | saved objects management section} definition for the type.
*/
management?: SavedObjectsTypeManagementDefinition;
}
/**
* Configuration options for the {@link SavedObjectsType | type}'s management section.
*
* @public
*/
export interface SavedObjectsTypeManagementDefinition {
/**
* Is the type importable or exportable. Defaults to `false`.
*/
importableAndExportable?: boolean;
/**
* The default search field to use for this type. Defaults to `id`.
*/
defaultSearchField?: string;
/**
* The eui icon name to display in the management table.
* If not defined, the default icon will be used.
*/
icon?: string;
/**
* Function returning the title to display in the management table.
* If not defined, will use the object's type and id to generate a label.
*/
getTitle?: (savedObject: SavedObject<any>) => string;
/**
* Function returning the url to use to redirect to the editing page of this object.
* If not defined, editing will not be allowed.
*/
getEditUrl?: (savedObject: SavedObject<any>) => string;
/**
* Function returning the url to use to redirect to this object from the management section.
* If not defined, redirecting to the object will not be allowed.
*
* @returns an object containing a `path` and `uiCapabilitiesPath` properties. the `path` is the path to
* the object page, relative to the base path. `uiCapabilitiesPath` is the path to check in the
* {@link Capabilities | uiCapabilities} to check if the user has permission to access the object.
*/
getInAppUrl?: (savedObject: SavedObject<any>) => { path: string; uiCapabilitiesPath: string };
}
/**
@ -257,7 +300,7 @@ export interface SavedObjectsLegacyUiExports {
savedObjectMigrations: SavedObjectsLegacyMigrationDefinitions;
savedObjectSchemas: SavedObjectsLegacySchemaDefinitions;
savedObjectValidations: PropertyValidators;
savedObjectsManagement: SavedObjectsManagementDefinition;
savedObjectsManagement: SavedObjectsLegacyManagementDefinition;
}
/**
@ -269,6 +312,28 @@ export interface SavedObjectsLegacyMapping {
properties: SavedObjectsTypeMappingDefinitions;
}
/**
* @internal
* @deprecated Use {@link SavedObjectsTypeManagementDefinition | management definition} when registering
* from new platform plugins
*/
export interface SavedObjectsLegacyManagementDefinition {
[key: string]: SavedObjectsLegacyManagementTypeDefinition;
}
/**
* @internal
* @deprecated
*/
export interface SavedObjectsLegacyManagementTypeDefinition {
isImportableAndExportable?: boolean;
defaultSearchField?: string;
icon?: string;
getTitle?: (savedObject: SavedObject<any>) => string;
getEditUrl?: (savedObject: SavedObject<any>) => string;
getInAppUrl?: (savedObject: SavedObject<any>) => { path: string; uiCapabilitiesPath: string };
}
/**
* @internal
* @deprecated

View file

@ -235,6 +235,75 @@ describe('convertLegacyTypes', () => {
expect(legacyMigration).toHaveBeenCalledWith(doc, context.log);
});
it('imports type management information', () => {
const uiExports: SavedObjectsLegacyUiExports = {
savedObjectMappings: [
{
pluginId: 'pluginA',
properties: {
typeA: {
properties: {
fieldA: { type: 'text' },
},
},
},
},
{
pluginId: 'pluginB',
properties: {
typeB: {
properties: {
fieldB: { type: 'text' },
},
},
typeC: {
properties: {
fieldC: { type: 'text' },
},
},
},
},
],
savedObjectsManagement: {
typeA: {
isImportableAndExportable: true,
icon: 'iconA',
defaultSearchField: 'searchFieldA',
getTitle: savedObject => savedObject.id,
},
typeB: {
isImportableAndExportable: false,
icon: 'iconB',
getEditUrl: savedObject => `/some-url/${savedObject.id}`,
getInAppUrl: savedObject => ({ path: 'path', uiCapabilitiesPath: 'ui-path' }),
},
},
savedObjectMigrations: {},
savedObjectSchemas: {},
savedObjectValidations: {},
};
const converted = convertLegacyTypes(uiExports, legacyConfig);
expect(converted.length).toEqual(3);
const [typeA, typeB, typeC] = converted;
expect(typeA.management).toEqual({
importableAndExportable: true,
icon: 'iconA',
defaultSearchField: 'searchFieldA',
getTitle: uiExports.savedObjectsManagement.typeA.getTitle,
});
expect(typeB.management).toEqual({
importableAndExportable: false,
icon: 'iconB',
getEditUrl: uiExports.savedObjectsManagement.typeB.getEditUrl,
getInAppUrl: uiExports.savedObjectsManagement.typeB.getInAppUrl,
});
expect(typeC.management).toBeUndefined();
});
it('merges everything when all are present', () => {
const uiExports: SavedObjectsLegacyUiExports = {
savedObjectMappings: [

View file

@ -23,6 +23,8 @@ import {
SavedObjectsType,
SavedObjectsLegacyUiExports,
SavedObjectLegacyMigrationMap,
SavedObjectsLegacyManagementTypeDefinition,
SavedObjectsTypeManagementDefinition,
} from './types';
import { SavedObjectsSchemaDefinition } from './schema';
@ -35,15 +37,17 @@ export const convertLegacyTypes = (
savedObjectMappings = [],
savedObjectMigrations = {},
savedObjectSchemas = {},
savedObjectsManagement = {},
}: SavedObjectsLegacyUiExports,
legacyConfig: LegacyConfig
): SavedObjectsType[] => {
return savedObjectMappings.reduce((types, { pluginId, properties }) => {
return savedObjectMappings.reduce((types, { properties }) => {
return [
...types,
...Object.entries(properties).map(([type, mappings]) => {
const schema = savedObjectSchemas[type];
const migrations = savedObjectMigrations[type];
const management = savedObjectsManagement[type];
return {
name: type,
hidden: schema?.hidden ?? false,
@ -55,6 +59,7 @@ export const convertLegacyTypes = (
: schema?.indexPattern,
convertToAliasScript: schema?.convertToAliasScript,
migrations: convertLegacyMigrations(migrations ?? {}),
management: management ? convertLegacyTypeManagement(management) : undefined,
};
}),
];
@ -90,3 +95,16 @@ const convertLegacyMigrations = (
};
}, {} as SavedObjectMigrationMap);
};
const convertLegacyTypeManagement = (
legacyTypeManagement: SavedObjectsLegacyManagementTypeDefinition
): SavedObjectsTypeManagementDefinition => {
return {
importableAndExportable: legacyTypeManagement.isImportableAndExportable,
defaultSearchField: legacyTypeManagement.defaultSearchField,
icon: legacyTypeManagement.icon,
getTitle: legacyTypeManagement.getTitle,
getEditUrl: legacyTypeManagement.getEditUrl,
getInAppUrl: legacyTypeManagement.getInAppUrl,
};
};

View file

@ -968,7 +968,7 @@ export type IsAuthenticated = (request: KibanaRequest | LegacyRequest) => boolea
export type ISavedObjectsRepository = Pick<SavedObjectsRepository, keyof SavedObjectsRepository>;
// @public
export type ISavedObjectTypeRegistry = Pick<SavedObjectTypeRegistry, 'getType' | 'getAllTypes' | 'getIndex' | 'isNamespaceAgnostic' | 'isHidden'>;
export type ISavedObjectTypeRegistry = Pick<SavedObjectTypeRegistry, 'getType' | 'getAllTypes' | 'getIndex' | 'isNamespaceAgnostic' | 'isHidden' | 'getImportableAndExportableTypes' | 'isImportableAndExportable'>;
// @public
export type IScopedClusterClient = Pick<ScopedClusterClient, 'callAsCurrentUser' | 'callAsInternalUser'>;
@ -1456,6 +1456,7 @@ export interface RequestHandlerContext {
rendering: IScopedRenderingClient;
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
dataClient: IScopedClusterClient;
@ -2150,12 +2151,26 @@ export interface SavedObjectsType {
convertToAliasScript?: string;
hidden: boolean;
indexPattern?: string;
management?: SavedObjectsTypeManagementDefinition;
mappings: SavedObjectsTypeMappingDefinition;
migrations?: SavedObjectMigrationMap;
name: string;
namespaceAgnostic: boolean;
}
// @public
export interface SavedObjectsTypeManagementDefinition {
defaultSearchField?: string;
getEditUrl?: (savedObject: SavedObject<any>) => string;
getInAppUrl?: (savedObject: SavedObject<any>) => {
path: string;
uiCapabilitiesPath: string;
};
getTitle?: (savedObject: SavedObject<any>) => string;
icon?: string;
importableAndExportable?: boolean;
}
// @public
export interface SavedObjectsTypeMappingDefinition {
dynamic?: false | 'strict';
@ -2180,9 +2195,11 @@ export interface SavedObjectsUpdateResponse<T = unknown> extends Omit<SavedObjec
// @public
export class SavedObjectTypeRegistry {
getAllTypes(): SavedObjectsType[];
getImportableAndExportableTypes(): SavedObjectsType[];
getIndex(type: string): string | undefined;
getType(type: string): SavedObjectsType | undefined;
isHidden(type: string): boolean;
isImportableAndExportable(type: string): boolean;
isNamespaceAgnostic(type: string): boolean;
registerType(type: SavedObjectsType): void;
}

View file

@ -130,16 +130,17 @@ export class Server {
http: httpSetup,
});
const uiSettingsSetup = await this.uiSettings.setup({
http: httpSetup,
});
const savedObjectsSetup = await this.savedObjects.setup({
http: httpSetup,
elasticsearch: elasticsearchServiceSetup,
legacyPlugins,
});
const uiSettingsSetup = await this.uiSettings.setup({
http: httpSetup,
savedObjects: savedObjectsSetup,
});
const metricsSetup = await this.metrics.setup({ http: httpSetup });
const coreSetup: InternalCoreSetup = {
@ -242,6 +243,7 @@ export class Server {
},
savedObjects: {
client: savedObjectsClient,
typeRegistry: this.coreStart!.savedObjects.getTypeRegistry(),
},
elasticsearch: {
adminClient: coreSetup.elasticsearch.adminClient.asScoped(req),

View file

@ -0,0 +1,20 @@
/*
* 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 { uiSettingsType } from './ui_settings';

View file

@ -0,0 +1,49 @@
/*
* 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 { SavedObjectsType } from '../../saved_objects';
export const uiSettingsType: SavedObjectsType = {
name: 'config',
hidden: false,
namespaceAgnostic: false,
mappings: {
// we don't want to allow `true` in the public `SavedObjectsTypeMappingDefinition` type, however
// this is needed for the config that is kinda a special type. To avoid adding additional internal types
// just for this, we hardcast to any here.
dynamic: true as any,
properties: {
buildNum: {
type: 'keyword',
},
},
},
management: {
importableAndExportable: true,
getInAppUrl() {
return {
path: `/app/kibana#/management/kibana/settings`,
uiCapabilitiesPath: 'advancedSettings.show',
};
},
getTitle(obj) {
return `Advanced Settings [${obj.id}]`;
},
},
};

View file

@ -17,13 +17,13 @@
* under the License.
*/
import { BehaviorSubject } from 'rxjs';
import { MockUiSettingsClientConstructor } from './ui_settings_service.test.mock';
import { UiSettingsService } from './ui_settings_service';
import { UiSettingsService, SetupDeps } from './ui_settings_service';
import { httpServiceMock } from '../http/http_service.mock';
import { savedObjectsClientMock } from '../mocks';
import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock';
import { mockCoreContext } from '../core_context.mock';
import { uiSettingsType } from './saved_objects';
const overrides = {
overrideBaz: 'baz',
@ -38,21 +38,34 @@ const defaults = {
},
};
const coreContext = mockCoreContext.create();
coreContext.configService.atPath.mockReturnValue(new BehaviorSubject({ overrides }));
const httpSetup = httpServiceMock.createSetupContract();
const setupDeps = { http: httpSetup };
const savedObjectsClient = savedObjectsClientMock.create();
afterEach(() => {
MockUiSettingsClientConstructor.mockClear();
});
describe('uiSettings', () => {
let service: UiSettingsService;
let setupDeps: SetupDeps;
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
beforeEach(() => {
const coreContext = mockCoreContext.create();
coreContext.configService.atPath.mockReturnValue(new BehaviorSubject({ overrides }));
const httpSetup = httpServiceMock.createSetupContract();
const savedObjectsSetup = savedObjectsServiceMock.createInternalSetupContract();
setupDeps = { http: httpSetup, savedObjects: savedObjectsSetup };
savedObjectsClient = savedObjectsClientMock.create();
service = new UiSettingsService(coreContext);
});
afterEach(() => {
MockUiSettingsClientConstructor.mockClear();
});
describe('#setup', () => {
it('registers the uiSettings type to the savedObjects registry', async () => {
await service.setup(setupDeps);
expect(setupDeps.savedObjects.registerType).toHaveBeenCalledTimes(1);
expect(setupDeps.savedObjects.registerType).toHaveBeenCalledWith(uiSettingsType);
});
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);
@ -60,7 +73,6 @@ describe('uiSettings', () => {
});
it('passes overrides to UiSettingsClient', async () => {
const service = new UiSettingsService(coreContext);
const setup = await service.setup(setupDeps);
setup.asScopedToClient(savedObjectsClient);
expect(MockUiSettingsClientConstructor).toBeCalledTimes(1);
@ -69,7 +81,6 @@ describe('uiSettings', () => {
});
it('passes a copy of set defaults to UiSettingsClient', async () => {
const service = new UiSettingsService(coreContext);
const setup = await service.setup(setupDeps);
setup.register(defaults);
@ -83,7 +94,6 @@ describe('uiSettings', () => {
describe('#register', () => {
it('throws if registers the same key twice', async () => {
const service = new UiSettingsService(coreContext);
const setup = await service.setup(setupDeps);
setup.register(defaults);
expect(() => setup.register(defaults)).toThrowErrorMatchingInlineSnapshot(
@ -96,7 +106,6 @@ describe('uiSettings', () => {
describe('#start', () => {
describe('#asScopedToClient', () => {
it('passes saved object type "config" to UiSettingsClient', async () => {
const service = new UiSettingsService(coreContext);
await service.setup(setupDeps);
const start = await service.start();
start.asScopedToClient(savedObjectsClient);
@ -106,7 +115,6 @@ describe('uiSettings', () => {
});
it('passes overrides to UiSettingsClient', async () => {
const service = new UiSettingsService(coreContext);
await service.setup(setupDeps);
const start = await service.start();
start.asScopedToClient(savedObjectsClient);
@ -116,7 +124,6 @@ describe('uiSettings', () => {
});
it('passes a copy of set defaults to UiSettingsClient', async () => {
const service = new UiSettingsService(coreContext);
const setup = await service.setup(setupDeps);
setup.register(defaults);
const start = await service.start();

View file

@ -24,6 +24,7 @@ import { CoreContext } from '../core_context';
import { Logger } from '../logging';
import { SavedObjectsClientContract } from '../saved_objects/types';
import { InternalSavedObjectsServiceSetup } from '../saved_objects';
import { InternalHttpServiceSetup } from '../http';
import { UiSettingsConfigType, config as uiConfigDefinition } from './ui_settings_config';
import { UiSettingsClient } from './ui_settings_client';
@ -33,11 +34,12 @@ import {
UiSettingsParams,
} from './types';
import { mapToObject } from '../../utils/';
import { uiSettingsType } from './saved_objects';
import { registerRoutes } from './routes';
interface SetupDeps {
export interface SetupDeps {
http: InternalHttpServiceSetup;
savedObjects: InternalSavedObjectsServiceSetup;
}
/** @internal */
@ -53,9 +55,11 @@ export class UiSettingsService
this.config$ = coreContext.configService.atPath<UiSettingsConfigType>(uiConfigDefinition.path);
}
public async setup(deps: SetupDeps): Promise<InternalUiSettingsServiceSetup> {
registerRoutes(deps.http.createRouter(''));
public async setup({ http, savedObjects }: SetupDeps): Promise<InternalUiSettingsServiceSetup> {
this.log.debug('Setting up ui settings service');
savedObjects.registerType(uiSettingsType);
registerRoutes(http.createRouter(''));
const config = await this.config$.pipe(first()).toPromise();
this.overrides = config.overrides;

View file

@ -201,18 +201,6 @@ export default function(kibana) {
return `/goto/${encodeURIComponent(obj.id)}`;
},
},
config: {
isImportableAndExportable: true,
getInAppUrl() {
return {
path: `/app/kibana#/management/kibana/settings`,
uiCapabilitiesPath: 'advancedSettings.show',
};
},
getTitle(obj) {
return `Advanced Settings [${obj.id}]`;
},
},
},
savedObjectSchemas: {

View file

@ -19,13 +19,13 @@
import { Server } from '../../server/kbn_server';
import { Capabilities } from '../../../core/server';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { SavedObjectsManagementDefinition } from '../../../core/server/saved_objects/management';
import { SavedObjectsLegacyManagementDefinition } from '../../../core/server/saved_objects/types';
export type InitPluginFunction = (server: Server) => void;
export interface UiExports {
injectDefaultVars?: (server: Server) => { [key: string]: any };
styleSheetPaths?: string;
savedObjectsManagement?: SavedObjectsManagementDefinition;
savedObjectsManagement?: SavedObjectsLegacyManagementDefinition;
mappings?: unknown;
visTypes?: string[];
interpreter?: string[];

View file

@ -23,7 +23,7 @@ import { Capabilities } from '../../core/server';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { SavedObjectsSchemaDefinition } from '../../core/server/saved_objects/schema';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { SavedObjectsManagementDefinition } from '../../core/server/saved_objects/management';
import { SavedObjectsLegacyManagementDefinition } from '../../core/server/saved_objects/types';
import { AppCategory } from '../../core/types';
/**
@ -73,7 +73,7 @@ export interface LegacyPluginOptions {
mappings: any;
migrations: any;
savedObjectSchemas: SavedObjectsSchemaDefinition;
savedObjectsManagement: SavedObjectsManagementDefinition;
savedObjectsManagement: SavedObjectsLegacyManagementDefinition;
visTypes: string[];
embeddableActions?: string[];
embeddableFactories?: string[];

View file

@ -77,7 +77,7 @@ declare module 'hapi' {
addScopedTutorialContextFactory: (
scopedTutorialContextFactory: (...args: any[]) => any
) => void;
savedObjectsManagement(): SavedObjectsManagement;
getSavedObjectsManagement(): SavedObjectsManagement;
getInjectedUiAppVars: (pluginName: string) => { [key: string]: any };
getUiNavLinks(): Array<{ _id: string }>;
addMemoizedFactoryToRequest: (

View file

@ -43,7 +43,7 @@ export function savedObjectsMixin(kbnServer, server) {
server.decorate(
'server',
'getSavedObjectsManagement',
() => new SavedObjectsManagement(kbnServer.uiExports.savedObjectsManagement)
() => new SavedObjectsManagement(typeRegistry)
);
const warn = message => server.log(['warning', 'saved-objects'], message);

View file

@ -201,7 +201,7 @@ describe('Saved Objects Mixin', () => {
it('should return all but hidden types', async () => {
expect(service).toBeDefined();
expect(service.types).toEqual(['config', 'testtype', 'doc1', 'doc2']);
expect(service.types).toEqual(['testtype', 'doc1', 'doc2']);
});
const mockCallEs = jest.fn();
@ -215,16 +215,12 @@ describe('Saved Objects Mixin', () => {
it('should create a repository without hidden types', () => {
const repository = service.getSavedObjectsRepository(mockCallEs);
expect(repository).toBeDefined();
expect(repository._allowedTypes).toEqual(['config', 'testtype', 'doc1', 'doc2']);
expect(repository._allowedTypes).toEqual(['testtype', 'doc1', 'doc2']);
});
it('should create a repository with a unique list of allowed types', () => {
const repository = service.getSavedObjectsRepository(mockCallEs, [
'config',
'config',
'config',
]);
expect(repository._allowedTypes).toEqual(['config', 'testtype', 'doc1', 'doc2']);
const repository = service.getSavedObjectsRepository(mockCallEs, ['doc1', 'doc1', 'doc1']);
expect(repository._allowedTypes).toEqual(['testtype', 'doc1', 'doc2']);
});
it('should create a repository with extraTypes minus duplicate', () => {
@ -232,13 +228,7 @@ describe('Saved Objects Mixin', () => {
'hiddentype',
'hiddentype',
]);
expect(repository._allowedTypes).toEqual([
'config',
'testtype',
'doc1',
'doc2',
'hiddentype',
]);
expect(repository._allowedTypes).toEqual(['testtype', 'doc1', 'doc2', 'hiddentype']);
});
it('should not allow a repository without a callCluster function', () => {

View file

@ -191,10 +191,28 @@ export default function({ getService }) {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message:
'[request body.type]: types that failed validation:\n' +
'- [request body.type.0]: expected value of type [string] but got [Array]\n' +
'- [request body.type.1.0]: wigwags is not exportable',
message: 'Trying to export non-exportable type(s): wigwags',
});
});
});
it(`should return 400 when exporting objects with unsupported type`, async () => {
await supertest
.post('/api/saved_objects/_export')
.send({
objects: [
{
type: 'wigwags',
id: '1',
},
],
})
.expect(400)
.then(resp => {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message: 'Trying to export object(s) with non-exportable types: wigwags:1',
});
});
});

View file

@ -59,7 +59,7 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message: `[request body.objects.0.type]: hiddentype is not exportable`,
message: `Trying to export object(s) with non-exportable types: hiddentype:hiddentype_1`,
});
};