mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Add SO import hook / warnings API (#87996)
* initial commit * adapt client-side signatures * more type fixes * adapt api IT asserts * fix some unit tests * fix more test usages * fix integration tests * fix FT test assertions * fix FT test assertions * add FTR API integ test suite * create the plugin_api_integration test suite * adapt and fix flyout tests * update documentation * update generated doc * add unit tests for `executeImportHooks` * wire resolve_import_errors and add unit tests * move hooks registration to SO type API * update generated doc * design integration * update generated doc * Add FTR tests for import warnings * deletes plugins api integ tests * self review * move onImport to management definition * update license header * rename actionUrl to actionPath
This commit is contained in:
parent
466d83c6d1
commit
edb338a8ad
79 changed files with 1538 additions and 129 deletions
|
@ -109,12 +109,14 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) | |
|
||||
| [SavedObjectsFindOptionsReference](./kibana-plugin-core-public.savedobjectsfindoptionsreference.md) | |
|
||||
| [SavedObjectsFindResponsePublic](./kibana-plugin-core-public.savedobjectsfindresponsepublic.md) | Return type of the Saved Objects <code>find()</code> method.<!-- -->\*Note\*: this type is different between the Public and Server Saved Objects clients. |
|
||||
| [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md) | A warning meant to notify that a specific user action is required to finalize the import of some type of object. The <code>actionUrl</code> must be a path relative to the basePath, and not include it. |
|
||||
| [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md) | Represents a failure to import due to a conflict, which can be resolved in different ways with an overwrite. |
|
||||
| [SavedObjectsImportConflictError](./kibana-plugin-core-public.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. |
|
||||
| [SavedObjectsImportFailure](./kibana-plugin-core-public.savedobjectsimportfailure.md) | Represents a failure to import. |
|
||||
| [SavedObjectsImportMissingReferencesError](./kibana-plugin-core-public.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. |
|
||||
| [SavedObjectsImportResponse](./kibana-plugin-core-public.savedobjectsimportresponse.md) | The response describing the result of an import. |
|
||||
| [SavedObjectsImportRetry](./kibana-plugin-core-public.savedobjectsimportretry.md) | Describes a retry operation for importing a saved object. |
|
||||
| [SavedObjectsImportSimpleWarning](./kibana-plugin-core-public.savedobjectsimportsimplewarning.md) | A simple informative warning that will be displayed to the user. |
|
||||
| [SavedObjectsImportSuccess](./kibana-plugin-core-public.savedobjectsimportsuccess.md) | Represents a successful import. |
|
||||
| [SavedObjectsImportUnknownError](./kibana-plugin-core-public.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. |
|
||||
| [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-core-public.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. |
|
||||
|
@ -163,6 +165,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value |
|
||||
| [SavedObjectAttributeSingle](./kibana-plugin-core-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) |
|
||||
| [SavedObjectsClientContract](./kibana-plugin-core-public.savedobjectsclientcontract.md) | SavedObjectsClientContract as implemented by the [SavedObjectsClient](./kibana-plugin-core-public.savedobjectsclient.md) |
|
||||
| [SavedObjectsImportWarning](./kibana-plugin-core-public.savedobjectsimportwarning.md) | Composite type of all the possible types of import warnings.<!-- -->See [SavedObjectsImportSimpleWarning](./kibana-plugin-core-public.savedobjectsimportsimplewarning.md) and [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md) for more details. |
|
||||
| [SavedObjectsNamespaceType](./kibana-plugin-core-public.savedobjectsnamespacetype.md) | The namespace type dictates how a saved object can be interacted in relation to namespaces. Each type is mutually exclusive: \* single (default): this type of saved object is namespace-isolated, e.g., it exists in only one namespace. \* multiple: this type of saved object is shareable, e.g., it can exist in one or more namespaces. \* agnostic: this type of saved object is global. |
|
||||
| [StartServicesAccessor](./kibana-plugin-core-public.startservicesaccessor.md) | Allows plugins to get access to APIs available in start inside async handlers, such as [App.mount](./kibana-plugin-core-public.app.mount.md)<!-- -->. Promise will not resolve until Core and plugin dependencies have completed <code>start</code>. |
|
||||
| [StringValidation](./kibana-plugin-core-public.stringvalidation.md) | Allows regex objects or a regex string |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md) > [actionPath](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.actionpath.md)
|
||||
|
||||
## SavedObjectsImportActionRequiredWarning.actionPath property
|
||||
|
||||
The path (without the basePath) that the user should be redirect to to address this warning.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
actionPath: string;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md) > [buttonLabel](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.buttonlabel.md)
|
||||
|
||||
## SavedObjectsImportActionRequiredWarning.buttonLabel property
|
||||
|
||||
An optional label to use for the link button. If unspecified, a default label will be used.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
buttonLabel?: string;
|
||||
```
|
|
@ -0,0 +1,25 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md)
|
||||
|
||||
## SavedObjectsImportActionRequiredWarning interface
|
||||
|
||||
A warning meant to notify that a specific user action is required to finalize the import of some type of object.
|
||||
|
||||
The `actionUrl` must be a path relative to the basePath, and not include it.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SavedObjectsImportActionRequiredWarning
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [actionPath](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.actionpath.md) | <code>string</code> | The path (without the basePath) that the user should be redirect to to address this warning. |
|
||||
| [buttonLabel](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.buttonlabel.md) | <code>string</code> | An optional label to use for the link button. If unspecified, a default label will be used. |
|
||||
| [message](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.message.md) | <code>string</code> | The translated message to display to the user. |
|
||||
| [type](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.type.md) | <code>'action_required'</code> | |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md) > [message](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.message.md)
|
||||
|
||||
## SavedObjectsImportActionRequiredWarning.message property
|
||||
|
||||
The translated message to display to the user.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
message: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md) > [type](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.type.md)
|
||||
|
||||
## SavedObjectsImportActionRequiredWarning.type property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
type: 'action_required';
|
||||
```
|
|
@ -20,4 +20,5 @@ export interface SavedObjectsImportResponse
|
|||
| [success](./kibana-plugin-core-public.savedobjectsimportresponse.success.md) | <code>boolean</code> | |
|
||||
| [successCount](./kibana-plugin-core-public.savedobjectsimportresponse.successcount.md) | <code>number</code> | |
|
||||
| [successResults](./kibana-plugin-core-public.savedobjectsimportresponse.successresults.md) | <code>SavedObjectsImportSuccess[]</code> | |
|
||||
| [warnings](./kibana-plugin-core-public.savedobjectsimportresponse.warnings.md) | <code>SavedObjectsImportWarning[]</code> | |
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportResponse](./kibana-plugin-core-public.savedobjectsimportresponse.md) > [warnings](./kibana-plugin-core-public.savedobjectsimportresponse.warnings.md)
|
||||
|
||||
## SavedObjectsImportResponse.warnings property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
warnings: SavedObjectsImportWarning[];
|
||||
```
|
|
@ -0,0 +1,21 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportSimpleWarning](./kibana-plugin-core-public.savedobjectsimportsimplewarning.md)
|
||||
|
||||
## SavedObjectsImportSimpleWarning interface
|
||||
|
||||
A simple informative warning that will be displayed to the user.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SavedObjectsImportSimpleWarning
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [message](./kibana-plugin-core-public.savedobjectsimportsimplewarning.message.md) | <code>string</code> | The translated message to display to the user |
|
||||
| [type](./kibana-plugin-core-public.savedobjectsimportsimplewarning.type.md) | <code>'simple'</code> | |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportSimpleWarning](./kibana-plugin-core-public.savedobjectsimportsimplewarning.md) > [message](./kibana-plugin-core-public.savedobjectsimportsimplewarning.message.md)
|
||||
|
||||
## SavedObjectsImportSimpleWarning.message property
|
||||
|
||||
The translated message to display to the user
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
message: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportSimpleWarning](./kibana-plugin-core-public.savedobjectsimportsimplewarning.md) > [type](./kibana-plugin-core-public.savedobjectsimportsimplewarning.type.md)
|
||||
|
||||
## SavedObjectsImportSimpleWarning.type property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
type: 'simple';
|
||||
```
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportWarning](./kibana-plugin-core-public.savedobjectsimportwarning.md)
|
||||
|
||||
## SavedObjectsImportWarning type
|
||||
|
||||
Composite type of all the possible types of import warnings.
|
||||
|
||||
See [SavedObjectsImportSimpleWarning](./kibana-plugin-core-public.savedobjectsimportsimplewarning.md) and [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-public.savedobjectsimportactionrequiredwarning.md) for more details.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type SavedObjectsImportWarning = SavedObjectsImportSimpleWarning | SavedObjectsImportActionRequiredWarning;
|
||||
```
|
|
@ -168,13 +168,16 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [SavedObjectsFindOptionsReference](./kibana-plugin-core-server.savedobjectsfindoptionsreference.md) | |
|
||||
| [SavedObjectsFindResponse](./kibana-plugin-core-server.savedobjectsfindresponse.md) | Return type of the Saved Objects <code>find()</code> method.<!-- -->\*Note\*: this type is different between the Public and Server Saved Objects clients. |
|
||||
| [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md) | |
|
||||
| [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md) | A warning meant to notify that a specific user action is required to finalize the import of some type of object. The <code>actionUrl</code> must be a path relative to the basePath, and not include it. |
|
||||
| [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md) | Represents a failure to import due to a conflict, which can be resolved in different ways with an overwrite. |
|
||||
| [SavedObjectsImportConflictError](./kibana-plugin-core-server.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. |
|
||||
| [SavedObjectsImportFailure](./kibana-plugin-core-server.savedobjectsimportfailure.md) | Represents a failure to import. |
|
||||
| [SavedObjectsImportHookResult](./kibana-plugin-core-server.savedobjectsimporthookresult.md) | Result from a [import hook](./kibana-plugin-core-server.savedobjectsimporthook.md) |
|
||||
| [SavedObjectsImportMissingReferencesError](./kibana-plugin-core-server.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. |
|
||||
| [SavedObjectsImportOptions](./kibana-plugin-core-server.savedobjectsimportoptions.md) | Options to control the import operation. |
|
||||
| [SavedObjectsImportResponse](./kibana-plugin-core-server.savedobjectsimportresponse.md) | The response describing the result of an import. |
|
||||
| [SavedObjectsImportRetry](./kibana-plugin-core-server.savedobjectsimportretry.md) | Describes a retry operation for importing a saved object. |
|
||||
| [SavedObjectsImportSimpleWarning](./kibana-plugin-core-server.savedobjectsimportsimplewarning.md) | A simple informative warning that will be displayed to the user. |
|
||||
| [SavedObjectsImportSuccess](./kibana-plugin-core-server.savedobjectsimportsuccess.md) | Represents a successful import. |
|
||||
| [SavedObjectsImportUnknownError](./kibana-plugin-core-server.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. |
|
||||
| [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-core-server.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. |
|
||||
|
@ -295,6 +298,8 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [SavedObjectsClientFactoryProvider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) | Provider to invoke to retrieve a [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md)<!-- -->. |
|
||||
| [SavedObjectsClientWrapperFactory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. |
|
||||
| [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) | Describe a [saved object type mapping](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) field.<!-- -->Please refer to [elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html) For the mapping documentation |
|
||||
| [SavedObjectsImportHook](./kibana-plugin-core-server.savedobjectsimporthook.md) | A hook associated with a specific saved object type, that will be invoked during the import process. The hook will have access to the objects of the registered type.<!-- -->Currently, the only supported feature for import hooks is to return warnings to be displayed in the UI when the import succeeds. The only interactions the hook can have with the import process is via the hook's response. Mutating the objects inside the hook's code will have no effect. |
|
||||
| [SavedObjectsImportWarning](./kibana-plugin-core-server.savedobjectsimportwarning.md) | Composite type of all the possible types of import warnings.<!-- -->See [SavedObjectsImportSimpleWarning](./kibana-plugin-core-server.savedobjectsimportsimplewarning.md) and [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md) for more details. |
|
||||
| [SavedObjectsNamespaceType](./kibana-plugin-core-server.savedobjectsnamespacetype.md) | The namespace type dictates how a saved object can be interacted in relation to namespaces. Each type is mutually exclusive: \* single (default): this type of saved object is namespace-isolated, e.g., it exists in only one namespace. \* multiple: this type of saved object is shareable, e.g., it can exist in one or more namespaces. \* agnostic: this type of saved object is global. |
|
||||
| [SavedObjectUnsanitizedDoc](./kibana-plugin-core-server.savedobjectunsanitizeddoc.md) | Describes Saved Object documents from Kibana < 7.0.0 which don't have a <code>references</code> root property defined. This type should only be used in migrations. |
|
||||
| [ScopeableRequest](./kibana-plugin-core-server.scopeablerequest.md) | A user credentials container. It accommodates the necessary auth credentials to impersonate the current user.<!-- -->See [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md)<!-- -->. |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md) > [actionPath](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.actionpath.md)
|
||||
|
||||
## SavedObjectsImportActionRequiredWarning.actionPath property
|
||||
|
||||
The path (without the basePath) that the user should be redirect to to address this warning.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
actionPath: string;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md) > [buttonLabel](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.buttonlabel.md)
|
||||
|
||||
## SavedObjectsImportActionRequiredWarning.buttonLabel property
|
||||
|
||||
An optional label to use for the link button. If unspecified, a default label will be used.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
buttonLabel?: string;
|
||||
```
|
|
@ -0,0 +1,25 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md)
|
||||
|
||||
## SavedObjectsImportActionRequiredWarning interface
|
||||
|
||||
A warning meant to notify that a specific user action is required to finalize the import of some type of object.
|
||||
|
||||
The `actionUrl` must be a path relative to the basePath, and not include it.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SavedObjectsImportActionRequiredWarning
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [actionPath](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.actionpath.md) | <code>string</code> | The path (without the basePath) that the user should be redirect to to address this warning. |
|
||||
| [buttonLabel](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.buttonlabel.md) | <code>string</code> | An optional label to use for the link button. If unspecified, a default label will be used. |
|
||||
| [message](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.message.md) | <code>string</code> | The translated message to display to the user. |
|
||||
| [type](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.type.md) | <code>'action_required'</code> | |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md) > [message](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.message.md)
|
||||
|
||||
## SavedObjectsImportActionRequiredWarning.message property
|
||||
|
||||
The translated message to display to the user.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
message: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md) > [type](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.type.md)
|
||||
|
||||
## SavedObjectsImportActionRequiredWarning.type property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
type: 'action_required';
|
||||
```
|
|
@ -0,0 +1,17 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportHook](./kibana-plugin-core-server.savedobjectsimporthook.md)
|
||||
|
||||
## SavedObjectsImportHook type
|
||||
|
||||
A hook associated with a specific saved object type, that will be invoked during the import process. The hook will have access to the objects of the registered type.
|
||||
|
||||
Currently, the only supported feature for import hooks is to return warnings to be displayed in the UI when the import succeeds.
|
||||
|
||||
The only interactions the hook can have with the import process is via the hook's response. Mutating the objects inside the hook's code will have no effect.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type SavedObjectsImportHook<T = unknown> = (objects: Array<SavedObject<T>>) => SavedObjectsImportHookResult | Promise<SavedObjectsImportHookResult>;
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportHookResult](./kibana-plugin-core-server.savedobjectsimporthookresult.md)
|
||||
|
||||
## SavedObjectsImportHookResult interface
|
||||
|
||||
Result from a [import hook](./kibana-plugin-core-server.savedobjectsimporthook.md)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SavedObjectsImportHookResult
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [warnings](./kibana-plugin-core-server.savedobjectsimporthookresult.warnings.md) | <code>SavedObjectsImportWarning[]</code> | An optional list of warnings to display in the UI when the import succeeds. |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportHookResult](./kibana-plugin-core-server.savedobjectsimporthookresult.md) > [warnings](./kibana-plugin-core-server.savedobjectsimporthookresult.warnings.md)
|
||||
|
||||
## SavedObjectsImportHookResult.warnings property
|
||||
|
||||
An optional list of warnings to display in the UI when the import succeeds.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
warnings?: SavedObjectsImportWarning[];
|
||||
```
|
|
@ -20,4 +20,5 @@ export interface SavedObjectsImportResponse
|
|||
| [success](./kibana-plugin-core-server.savedobjectsimportresponse.success.md) | <code>boolean</code> | |
|
||||
| [successCount](./kibana-plugin-core-server.savedobjectsimportresponse.successcount.md) | <code>number</code> | |
|
||||
| [successResults](./kibana-plugin-core-server.savedobjectsimportresponse.successresults.md) | <code>SavedObjectsImportSuccess[]</code> | |
|
||||
| [warnings](./kibana-plugin-core-server.savedobjectsimportresponse.warnings.md) | <code>SavedObjectsImportWarning[]</code> | |
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportResponse](./kibana-plugin-core-server.savedobjectsimportresponse.md) > [warnings](./kibana-plugin-core-server.savedobjectsimportresponse.warnings.md)
|
||||
|
||||
## SavedObjectsImportResponse.warnings property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
warnings: SavedObjectsImportWarning[];
|
||||
```
|
|
@ -0,0 +1,21 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportSimpleWarning](./kibana-plugin-core-server.savedobjectsimportsimplewarning.md)
|
||||
|
||||
## SavedObjectsImportSimpleWarning interface
|
||||
|
||||
A simple informative warning that will be displayed to the user.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SavedObjectsImportSimpleWarning
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [message](./kibana-plugin-core-server.savedobjectsimportsimplewarning.message.md) | <code>string</code> | The translated message to display to the user |
|
||||
| [type](./kibana-plugin-core-server.savedobjectsimportsimplewarning.type.md) | <code>'simple'</code> | |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportSimpleWarning](./kibana-plugin-core-server.savedobjectsimportsimplewarning.md) > [message](./kibana-plugin-core-server.savedobjectsimportsimplewarning.message.md)
|
||||
|
||||
## SavedObjectsImportSimpleWarning.message property
|
||||
|
||||
The translated message to display to the user
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
message: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportSimpleWarning](./kibana-plugin-core-server.savedobjectsimportsimplewarning.md) > [type](./kibana-plugin-core-server.savedobjectsimportsimplewarning.type.md)
|
||||
|
||||
## SavedObjectsImportSimpleWarning.type property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
type: 'simple';
|
||||
```
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportWarning](./kibana-plugin-core-server.savedobjectsimportwarning.md)
|
||||
|
||||
## SavedObjectsImportWarning type
|
||||
|
||||
Composite type of all the possible types of import warnings.
|
||||
|
||||
See [SavedObjectsImportSimpleWarning](./kibana-plugin-core-server.savedobjectsimportsimplewarning.md) and [SavedObjectsImportActionRequiredWarning](./kibana-plugin-core-server.savedobjectsimportactionrequiredwarning.md) for more details.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type SavedObjectsImportWarning = SavedObjectsImportSimpleWarning | SavedObjectsImportActionRequiredWarning;
|
||||
```
|
|
@ -22,4 +22,5 @@ export interface SavedObjectsTypeManagementDefinition
|
|||
| [getTitle](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md) | <code>(savedObject: SavedObject<any>) => 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-core-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-core-server.savedobjectstypemanagementdefinition.importableandexportable.md) | <code>boolean</code> | Is the type importable or exportable. Defaults to <code>false</code>. |
|
||||
| [onImport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md) | <code>SavedObjectsImportHook</code> | An optional [import hook](./kibana-plugin-core-server.savedobjectsimporthook.md) to use when importing given type.<!-- -->Import hooks are executed during the savedObjects import process and allow to interact with the imported objects. See the [hook documentation](./kibana-plugin-core-server.savedobjectsimporthook.md) for more info. |
|
||||
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsTypeManagementDefinition](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) > [onImport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md)
|
||||
|
||||
## SavedObjectsTypeManagementDefinition.onImport property
|
||||
|
||||
An optional [import hook](./kibana-plugin-core-server.savedobjectsimporthook.md) to use when importing given type.
|
||||
|
||||
Import hooks are executed during the savedObjects import process and allow to interact with the imported objects. See the [hook documentation](./kibana-plugin-core-server.savedobjectsimporthook.md) for more info.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
onImport?: SavedObjectsImportHook;
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
Registering a hook displaying a warning about a specific type of object
|
||||
|
||||
```ts
|
||||
// src/plugins/my_plugin/server/plugin.ts
|
||||
import { myType } from './saved_objects';
|
||||
|
||||
export class Plugin() {
|
||||
setup: (core: CoreSetup) => {
|
||||
core.savedObjects.registerType({
|
||||
...myType,
|
||||
management: {
|
||||
...myType.management,
|
||||
onImport: (objects) => {
|
||||
if(someActionIsNeeded(objects)) {
|
||||
return {
|
||||
warnings: [
|
||||
{
|
||||
type: 'action_required',
|
||||
message: 'Objects need to be manually enabled after import',
|
||||
actionPath: '/app/my-app/require-activation',
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
messages returned in the warnings are user facing and must be translated.
|
||||
|
28
src/core/public/http/base_path.mock.ts
Normal file
28
src/core/public/http/base_path.mock.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { IBasePath } from './types';
|
||||
|
||||
const createBasePathMock = ({
|
||||
publicBaseUrl = '/',
|
||||
serverBasePath = '/',
|
||||
}: { publicBaseUrl?: string; serverBasePath?: string } = {}) => {
|
||||
const mock: jest.Mocked<IBasePath> = {
|
||||
prepend: jest.fn(),
|
||||
get: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
publicBaseUrl,
|
||||
serverBasePath,
|
||||
};
|
||||
|
||||
return mock;
|
||||
};
|
||||
|
||||
export const basePathMock = {
|
||||
create: createBasePathMock,
|
||||
};
|
|
@ -11,6 +11,7 @@ import { HttpService } from './http_service';
|
|||
import { HttpSetup } from './types';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BasePath } from './base_path';
|
||||
import { basePathMock } from './base_path.mock';
|
||||
|
||||
export type HttpSetupMock = jest.Mocked<HttpSetup> & {
|
||||
basePath: BasePath;
|
||||
|
@ -54,4 +55,5 @@ export const httpServiceMock = {
|
|||
create: createMock,
|
||||
createSetupContract: createServiceMock,
|
||||
createStartContract: createServiceMock,
|
||||
createBasePath: basePathMock.create,
|
||||
};
|
||||
|
|
|
@ -130,6 +130,9 @@ export {
|
|||
SavedObjectsImportFailure,
|
||||
SavedObjectsImportRetry,
|
||||
SavedObjectsNamespaceType,
|
||||
SavedObjectsImportSimpleWarning,
|
||||
SavedObjectsImportActionRequiredWarning,
|
||||
SavedObjectsImportWarning,
|
||||
} from './saved_objects';
|
||||
|
||||
export {
|
||||
|
|
|
@ -1198,6 +1198,15 @@ export interface SavedObjectsFindResponsePublic<T = unknown> extends SavedObject
|
|||
total: number;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface SavedObjectsImportActionRequiredWarning {
|
||||
actionPath: string;
|
||||
buttonLabel?: string;
|
||||
message: string;
|
||||
// (undocumented)
|
||||
type: 'action_required';
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface SavedObjectsImportAmbiguousConflictError {
|
||||
// (undocumented)
|
||||
|
@ -1257,6 +1266,8 @@ export interface SavedObjectsImportResponse {
|
|||
successCount: number;
|
||||
// (undocumented)
|
||||
successResults?: SavedObjectsImportSuccess[];
|
||||
// (undocumented)
|
||||
warnings: SavedObjectsImportWarning[];
|
||||
}
|
||||
|
||||
// @public
|
||||
|
@ -1278,6 +1289,13 @@ export interface SavedObjectsImportRetry {
|
|||
type: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface SavedObjectsImportSimpleWarning {
|
||||
message: string;
|
||||
// (undocumented)
|
||||
type: 'simple';
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface SavedObjectsImportSuccess {
|
||||
// @deprecated (undocumented)
|
||||
|
@ -1311,6 +1329,9 @@ export interface SavedObjectsImportUnsupportedTypeError {
|
|||
type: 'unsupported_type';
|
||||
}
|
||||
|
||||
// @public
|
||||
export type SavedObjectsImportWarning = SavedObjectsImportSimpleWarning | SavedObjectsImportActionRequiredWarning;
|
||||
|
||||
// @public
|
||||
export interface SavedObjectsMigrationVersion {
|
||||
// (undocumented)
|
||||
|
|
|
@ -35,6 +35,9 @@ export {
|
|||
SavedObjectsImportFailure,
|
||||
SavedObjectsImportRetry,
|
||||
SavedObjectsNamespaceType,
|
||||
SavedObjectsImportSimpleWarning,
|
||||
SavedObjectsImportActionRequiredWarning,
|
||||
SavedObjectsImportWarning,
|
||||
} from '../../server/types';
|
||||
|
||||
export {
|
||||
|
|
|
@ -321,6 +321,11 @@ export {
|
|||
SavedObjectsImporter,
|
||||
ISavedObjectsImporter,
|
||||
SavedObjectsImportError,
|
||||
SavedObjectsImportHook,
|
||||
SavedObjectsImportHookResult,
|
||||
SavedObjectsImportSimpleWarning,
|
||||
SavedObjectsImportActionRequiredWarning,
|
||||
SavedObjectsImportWarning,
|
||||
} from './saved_objects';
|
||||
|
||||
export {
|
||||
|
|
|
@ -18,6 +18,7 @@ import { savedObjectsClientMock } from '../../mocks';
|
|||
import { ISavedObjectTypeRegistry } from '..';
|
||||
import { typeRegistryMock } from '../saved_objects_type_registry.mock';
|
||||
import { importSavedObjectsFromStream, ImportSavedObjectsOptions } from './import_saved_objects';
|
||||
import { SavedObjectsImportHook, SavedObjectsImportWarning } from './types';
|
||||
|
||||
import {
|
||||
collectSavedObjects,
|
||||
|
@ -26,6 +27,7 @@ import {
|
|||
checkConflicts,
|
||||
checkOriginConflicts,
|
||||
createSavedObjects,
|
||||
executeImportHooks,
|
||||
} from './lib';
|
||||
|
||||
jest.mock('./lib/collect_saved_objects');
|
||||
|
@ -34,6 +36,7 @@ jest.mock('./lib/validate_references');
|
|||
jest.mock('./lib/check_conflicts');
|
||||
jest.mock('./lib/check_origin_conflicts');
|
||||
jest.mock('./lib/create_saved_objects');
|
||||
jest.mock('./lib/execute_import_hooks');
|
||||
|
||||
const getMockFn = <T extends (...args: any[]) => any, U>(fn: (...args: Parameters<T>) => U) =>
|
||||
fn as jest.MockedFunction<(...args: Parameters<T>) => U>;
|
||||
|
@ -61,6 +64,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
pendingOverwrites: new Set(),
|
||||
});
|
||||
getMockFn(createSavedObjects).mockResolvedValue({ errors: [], createdObjects: [] });
|
||||
getMockFn(executeImportHooks).mockResolvedValue([]);
|
||||
});
|
||||
|
||||
let readStream: Readable;
|
||||
|
@ -70,14 +74,19 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
let typeRegistry: jest.Mocked<ISavedObjectTypeRegistry>;
|
||||
const namespace = 'some-namespace';
|
||||
|
||||
const setupOptions = (
|
||||
createNewCopies: boolean = false,
|
||||
getTypeImpl: (name: string) => any = (type: string) =>
|
||||
const setupOptions = ({
|
||||
createNewCopies = false,
|
||||
getTypeImpl = (type: string) =>
|
||||
({
|
||||
// other attributes aren't needed for the purposes of injecting metadata
|
||||
management: { icon: `${type}-icon` },
|
||||
} as any)
|
||||
): ImportSavedObjectsOptions => {
|
||||
} as any),
|
||||
importHooks = {},
|
||||
}: {
|
||||
createNewCopies?: boolean;
|
||||
getTypeImpl?: (name: string) => any;
|
||||
importHooks?: Record<string, SavedObjectsImportHook[]>;
|
||||
} = {}): ImportSavedObjectsOptions => {
|
||||
readStream = new Readable();
|
||||
savedObjectsClient = savedObjectsClientMock.create();
|
||||
typeRegistry = typeRegistryMock.create();
|
||||
|
@ -90,6 +99,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
typeRegistry,
|
||||
namespace,
|
||||
createNewCopies,
|
||||
importHooks,
|
||||
};
|
||||
};
|
||||
const createObject = ({
|
||||
|
@ -153,6 +163,31 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('executes import hooks', async () => {
|
||||
const importHooks = {
|
||||
foo: [jest.fn()],
|
||||
};
|
||||
|
||||
const options = setupOptions({ importHooks });
|
||||
const collectedObjects = [createObject()];
|
||||
getMockFn(collectSavedObjects).mockResolvedValue({
|
||||
errors: [],
|
||||
collectedObjects,
|
||||
importIdMap: new Map(),
|
||||
});
|
||||
getMockFn(createSavedObjects).mockResolvedValue({
|
||||
errors: [],
|
||||
createdObjects: collectedObjects,
|
||||
});
|
||||
|
||||
await importSavedObjectsFromStream(options);
|
||||
|
||||
expect(executeImportHooks).toHaveBeenCalledWith({
|
||||
objects: collectedObjects,
|
||||
importHooks,
|
||||
});
|
||||
});
|
||||
|
||||
describe('with createNewCopies disabled', () => {
|
||||
test('does not regenerate object IDs', async () => {
|
||||
const options = setupOptions();
|
||||
|
@ -256,7 +291,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
|
||||
describe('with createNewCopies enabled', () => {
|
||||
test('regenerates object IDs', async () => {
|
||||
const options = setupOptions(true);
|
||||
const options = setupOptions({ createNewCopies: true });
|
||||
const collectedObjects = [createObject()];
|
||||
getMockFn(collectSavedObjects).mockResolvedValue({
|
||||
errors: [],
|
||||
|
@ -269,7 +304,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
});
|
||||
|
||||
test('does not check conflicts or check origin conflicts', async () => {
|
||||
const options = setupOptions(true);
|
||||
const options = setupOptions({ createNewCopies: true });
|
||||
getMockFn(validateReferences).mockResolvedValue([]);
|
||||
|
||||
await importSavedObjectsFromStream(options);
|
||||
|
@ -278,7 +313,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
});
|
||||
|
||||
test('creates saved objects', async () => {
|
||||
const options = setupOptions(true);
|
||||
const options = setupOptions({ createNewCopies: true });
|
||||
const collectedObjects = [createObject()];
|
||||
const errors = [createError(), createError()];
|
||||
getMockFn(collectSavedObjects).mockResolvedValue({
|
||||
|
@ -313,7 +348,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
const options = setupOptions();
|
||||
|
||||
const result = await importSavedObjectsFromStream(options);
|
||||
expect(result).toEqual({ success: true, successCount: 0 });
|
||||
expect(result).toEqual({ success: true, successCount: 0, warnings: [] });
|
||||
});
|
||||
|
||||
test('returns success=false if an error occurred', async () => {
|
||||
|
@ -325,7 +360,33 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
});
|
||||
|
||||
const result = await importSavedObjectsFromStream(options);
|
||||
expect(result).toEqual({ success: false, successCount: 0, errors: [expect.any(Object)] });
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [expect.any(Object)],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('returns warnings from the import hooks', async () => {
|
||||
const options = setupOptions();
|
||||
const collectedObjects = [createObject()];
|
||||
getMockFn(collectSavedObjects).mockResolvedValue({
|
||||
errors: [],
|
||||
collectedObjects,
|
||||
importIdMap: new Map(),
|
||||
});
|
||||
getMockFn(createSavedObjects).mockResolvedValue({
|
||||
errors: [],
|
||||
createdObjects: collectedObjects,
|
||||
});
|
||||
|
||||
const warnings: SavedObjectsImportWarning[] = [{ type: 'simple', message: 'foo' }];
|
||||
getMockFn(executeImportHooks).mockResolvedValue(warnings);
|
||||
|
||||
const result = await importSavedObjectsFromStream(options);
|
||||
|
||||
expect(result.warnings).toEqual(warnings);
|
||||
});
|
||||
|
||||
describe('handles a mix of successes and errors and injects metadata', () => {
|
||||
|
@ -389,12 +450,13 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
successCount: 3,
|
||||
successResults,
|
||||
errors: errorResults,
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('with createNewCopies enabled', async () => {
|
||||
// however, we include it here for posterity
|
||||
const options = setupOptions(true);
|
||||
const options = setupOptions({ createNewCopies: true });
|
||||
getMockFn(createSavedObjects).mockResolvedValue({ errors, createdObjects });
|
||||
|
||||
const result = await importSavedObjectsFromStream(options);
|
||||
|
@ -410,6 +472,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
successCount: 3,
|
||||
successResults,
|
||||
errors: errorResults,
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -418,15 +481,18 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
const obj1 = createObject({ type: 'foo' });
|
||||
const obj2 = createObject({ type: 'bar', title: 'bar-title' });
|
||||
|
||||
const options = setupOptions(false, (type) => {
|
||||
if (type === 'foo') {
|
||||
const options = setupOptions({
|
||||
createNewCopies: false,
|
||||
getTypeImpl: (type) => {
|
||||
if (type === 'foo') {
|
||||
return {
|
||||
management: { getTitle: () => 'getTitle-foo', icon: `${type}-icon` },
|
||||
};
|
||||
}
|
||||
return {
|
||||
management: { getTitle: () => 'getTitle-foo', icon: `${type}-icon` },
|
||||
management: { icon: `${type}-icon` },
|
||||
};
|
||||
}
|
||||
return {
|
||||
management: { icon: `${type}-icon` },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
getMockFn(checkConflicts).mockResolvedValue({
|
||||
|
@ -456,6 +522,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
success: true,
|
||||
successCount: 2,
|
||||
successResults,
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -483,7 +550,12 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
|
||||
const result = await importSavedObjectsFromStream(options);
|
||||
const expectedErrors = errors.map(({ type, id }) => expect.objectContaining({ type, id }));
|
||||
expect(result).toEqual({ success: false, successCount: 0, errors: expectedErrors });
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: expectedErrors,
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,7 +9,11 @@
|
|||
import { Readable } from 'stream';
|
||||
import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry';
|
||||
import { SavedObjectsClientContract } from '../types';
|
||||
import { SavedObjectsImportFailure, SavedObjectsImportResponse } from './types';
|
||||
import {
|
||||
SavedObjectsImportFailure,
|
||||
SavedObjectsImportResponse,
|
||||
SavedObjectsImportHook,
|
||||
} from './types';
|
||||
import {
|
||||
validateReferences,
|
||||
checkOriginConflicts,
|
||||
|
@ -17,6 +21,7 @@ import {
|
|||
checkConflicts,
|
||||
regenerateIds,
|
||||
collectSavedObjects,
|
||||
executeImportHooks,
|
||||
} from './lib';
|
||||
|
||||
/**
|
||||
|
@ -33,6 +38,8 @@ export interface ImportSavedObjectsOptions {
|
|||
savedObjectsClient: SavedObjectsClientContract;
|
||||
/** The registry of all known saved object types */
|
||||
typeRegistry: ISavedObjectTypeRegistry;
|
||||
/** List of registered import hooks */
|
||||
importHooks: Record<string, SavedObjectsImportHook[]>;
|
||||
/** if specified, will import in given namespace, else will import as global object */
|
||||
namespace?: string;
|
||||
/** If true, will create new copies of import objects, each with a random `id` and undefined `originId`. */
|
||||
|
@ -52,6 +59,7 @@ export async function importSavedObjectsFromStream({
|
|||
createNewCopies,
|
||||
savedObjectsClient,
|
||||
typeRegistry,
|
||||
importHooks,
|
||||
namespace,
|
||||
}: ImportSavedObjectsOptions): Promise<SavedObjectsImportResponse> {
|
||||
let errorAccumulator: SavedObjectsImportFailure[] = [];
|
||||
|
@ -147,10 +155,15 @@ export async function importSavedObjectsFromStream({
|
|||
...(attemptedOverwrite && { overwrite: true }),
|
||||
};
|
||||
});
|
||||
const warnings = await executeImportHooks({
|
||||
objects: createSavedObjectsResult.createdObjects,
|
||||
importHooks,
|
||||
});
|
||||
|
||||
return {
|
||||
successCount: createSavedObjectsResult.createdObjects.length,
|
||||
success: errorAccumulator.length === 0,
|
||||
warnings,
|
||||
...(successResults.length && { successResults }),
|
||||
...(errorResults.length && { errors: errorResults }),
|
||||
};
|
||||
|
|
|
@ -19,5 +19,10 @@ export {
|
|||
SavedObjectsImportUnsupportedTypeError,
|
||||
SavedObjectsResolveImportErrorsOptions,
|
||||
SavedObjectsImportRetry,
|
||||
SavedObjectsImportHook,
|
||||
SavedObjectsImportHookResult,
|
||||
SavedObjectsImportSimpleWarning,
|
||||
SavedObjectsImportActionRequiredWarning,
|
||||
SavedObjectsImportWarning,
|
||||
} from './types';
|
||||
export { SavedObjectsImportError } from './errors';
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { SavedObject } from '../../types';
|
||||
import { SavedObjectsImportHookResult, SavedObjectsImportWarning } from '../types';
|
||||
import { executeImportHooks } from './execute_import_hooks';
|
||||
|
||||
const createObject = (type: string, id: string): SavedObject => ({
|
||||
type,
|
||||
id,
|
||||
attributes: {},
|
||||
references: [],
|
||||
});
|
||||
|
||||
const createHook = (
|
||||
result: SavedObjectsImportHookResult | Promise<SavedObjectsImportHookResult> = {}
|
||||
) => jest.fn().mockReturnValue(result);
|
||||
|
||||
const createWarning = (message: string): SavedObjectsImportWarning => ({
|
||||
type: 'simple',
|
||||
message,
|
||||
});
|
||||
|
||||
describe('executeImportHooks', () => {
|
||||
it('invokes the hooks with the correct objects', async () => {
|
||||
const foo1 = createObject('foo', '1');
|
||||
const foo2 = createObject('foo', '2');
|
||||
const bar1 = createObject('bar', '1');
|
||||
const objects = [foo1, bar1, foo2];
|
||||
|
||||
const fooHook = createHook();
|
||||
const barHook = createHook();
|
||||
|
||||
await executeImportHooks({
|
||||
objects,
|
||||
importHooks: {
|
||||
foo: [fooHook],
|
||||
bar: [barHook],
|
||||
},
|
||||
});
|
||||
|
||||
expect(fooHook).toHaveBeenCalledTimes(1);
|
||||
expect(fooHook).toHaveBeenCalledWith([foo1, foo2]);
|
||||
|
||||
expect(barHook).toHaveBeenCalledTimes(1);
|
||||
expect(barHook).toHaveBeenCalledWith([bar1]);
|
||||
});
|
||||
|
||||
it('handles multiple hooks per type', async () => {
|
||||
const foo1 = createObject('foo', '1');
|
||||
const foo2 = createObject('foo', '2');
|
||||
const bar1 = createObject('bar', '1');
|
||||
const objects = [foo1, bar1, foo2];
|
||||
|
||||
const fooHook1 = createHook();
|
||||
const fooHook2 = createHook();
|
||||
|
||||
await executeImportHooks({
|
||||
objects,
|
||||
importHooks: {
|
||||
foo: [fooHook1, fooHook2],
|
||||
},
|
||||
});
|
||||
|
||||
expect(fooHook1).toHaveBeenCalledTimes(1);
|
||||
expect(fooHook1).toHaveBeenCalledWith([foo1, foo2]);
|
||||
|
||||
expect(fooHook2).toHaveBeenCalledTimes(1);
|
||||
expect(fooHook2).toHaveBeenCalledWith([foo1, foo2]);
|
||||
});
|
||||
|
||||
it('does not call a hook if no object of its type is present', async () => {
|
||||
const objects = [createObject('foo', '1'), createObject('foo', '2')];
|
||||
const hook = createHook();
|
||||
|
||||
await executeImportHooks({
|
||||
objects,
|
||||
importHooks: {
|
||||
bar: [hook],
|
||||
},
|
||||
});
|
||||
|
||||
expect(hook).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns the warnings returned by the hooks', async () => {
|
||||
const foo1 = createObject('foo', '1');
|
||||
const bar1 = createObject('bar', '1');
|
||||
const objects = [foo1, bar1];
|
||||
|
||||
const fooWarning1 = createWarning('foo warning 1');
|
||||
const fooWarning2 = createWarning('foo warning 2');
|
||||
const barWarning = createWarning('bar warning');
|
||||
|
||||
const fooHook = createHook({ warnings: [fooWarning1, fooWarning2] });
|
||||
const barHook = createHook({ warnings: [barWarning] });
|
||||
|
||||
const warnings = await executeImportHooks({
|
||||
objects,
|
||||
importHooks: {
|
||||
foo: [fooHook],
|
||||
bar: [barHook],
|
||||
},
|
||||
});
|
||||
|
||||
expect(warnings).toEqual([fooWarning1, fooWarning2, barWarning]);
|
||||
});
|
||||
|
||||
it('handles asynchronous hooks', async () => {
|
||||
const foo1 = createObject('foo', '1');
|
||||
const bar1 = createObject('bar', '1');
|
||||
const objects = [foo1, bar1];
|
||||
|
||||
const fooWarning = createWarning('foo warning 1');
|
||||
const barWarning = createWarning('bar warning');
|
||||
|
||||
const fooHook = createHook(Promise.resolve({ warnings: [fooWarning] }));
|
||||
const barHook = createHook(Promise.resolve({ warnings: [barWarning] }));
|
||||
|
||||
const warnings = await executeImportHooks({
|
||||
objects,
|
||||
importHooks: {
|
||||
foo: [fooHook],
|
||||
bar: [barHook],
|
||||
},
|
||||
});
|
||||
|
||||
expect(warnings).toEqual([fooWarning, barWarning]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { SavedObject } from '../../types';
|
||||
import { SavedObjectsImportHook, SavedObjectsImportWarning } from '../types';
|
||||
|
||||
interface ExecuteImportHooksOptions {
|
||||
objects: SavedObject[];
|
||||
importHooks: Record<string, SavedObjectsImportHook[]>;
|
||||
}
|
||||
|
||||
export const executeImportHooks = async ({
|
||||
objects,
|
||||
importHooks,
|
||||
}: ExecuteImportHooksOptions): Promise<SavedObjectsImportWarning[]> => {
|
||||
const objsByType = splitByType(objects);
|
||||
let warnings: SavedObjectsImportWarning[] = [];
|
||||
|
||||
for (const [type, typeObjs] of Object.entries(objsByType)) {
|
||||
const hooks = importHooks[type] ?? [];
|
||||
for (const hook of hooks) {
|
||||
const hookResult = await hook(typeObjs);
|
||||
if (hookResult.warnings) {
|
||||
warnings = [...warnings, ...hookResult.warnings];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return warnings;
|
||||
};
|
||||
|
||||
const splitByType = (objects: SavedObject[]): Record<string, SavedObject[]> => {
|
||||
return objects.reduce((memo, obj) => {
|
||||
memo[obj.type] = [...(memo[obj.type] ?? []), obj];
|
||||
return memo;
|
||||
}, {} as Record<string, SavedObject[]>);
|
||||
};
|
|
@ -18,3 +18,4 @@ export { regenerateIds } from './regenerate_ids';
|
|||
export { splitOverwrites } from './split_overwrites';
|
||||
export { getNonExistingReferenceAsKeys, validateReferences } from './validate_references';
|
||||
export { validateRetries } from './validate_retries';
|
||||
export { executeImportHooks } from './execute_import_hooks';
|
||||
|
|
|
@ -15,9 +15,10 @@ import {
|
|||
SavedObjectsImportFailure,
|
||||
SavedObjectsImportRetry,
|
||||
SavedObjectReference,
|
||||
SavedObjectsImportWarning,
|
||||
} from '../types';
|
||||
import { savedObjectsClientMock } from '../../mocks';
|
||||
import { ISavedObjectTypeRegistry } from '..';
|
||||
import { ISavedObjectTypeRegistry, SavedObjectsImportHook } from '..';
|
||||
import { typeRegistryMock } from '../saved_objects_type_registry.mock';
|
||||
import {
|
||||
resolveSavedObjectsImportErrors,
|
||||
|
@ -34,6 +35,7 @@ import {
|
|||
splitOverwrites,
|
||||
createSavedObjects,
|
||||
createObjectsFilter,
|
||||
executeImportHooks,
|
||||
} from './lib';
|
||||
|
||||
jest.mock('./lib/validate_retries');
|
||||
|
@ -45,6 +47,7 @@ jest.mock('./lib/check_conflicts');
|
|||
jest.mock('./lib/check_origin_conflicts');
|
||||
jest.mock('./lib/split_overwrites');
|
||||
jest.mock('./lib/create_saved_objects');
|
||||
jest.mock('./lib/execute_import_hooks');
|
||||
|
||||
const getMockFn = <T extends (...args: any[]) => any, U>(fn: (...args: Parameters<T>) => U) =>
|
||||
fn as jest.MockedFunction<(...args: Parameters<T>) => U>;
|
||||
|
@ -73,6 +76,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
objectsToNotOverwrite: [],
|
||||
});
|
||||
getMockFn(createSavedObjects).mockResolvedValue({ errors: [], createdObjects: [] });
|
||||
getMockFn(executeImportHooks).mockResolvedValue([]);
|
||||
});
|
||||
|
||||
let readStream: Readable;
|
||||
|
@ -81,15 +85,21 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
let typeRegistry: jest.Mocked<ISavedObjectTypeRegistry>;
|
||||
const namespace = 'some-namespace';
|
||||
|
||||
const setupOptions = (
|
||||
retries: SavedObjectsImportRetry[] = [],
|
||||
createNewCopies: boolean = false,
|
||||
getTypeImpl: (name: string) => any = (type: string) =>
|
||||
const setupOptions = ({
|
||||
retries = [],
|
||||
createNewCopies = false,
|
||||
getTypeImpl = (type: string) =>
|
||||
({
|
||||
// other attributes aren't needed for the purposes of injecting metadata
|
||||
management: { icon: `${type}-icon` },
|
||||
} as any)
|
||||
): ResolveSavedObjectsImportErrorsOptions => {
|
||||
} as any),
|
||||
importHooks = {},
|
||||
}: {
|
||||
retries?: SavedObjectsImportRetry[];
|
||||
createNewCopies?: boolean;
|
||||
getTypeImpl?: (name: string) => any;
|
||||
importHooks?: Record<string, SavedObjectsImportHook[]>;
|
||||
} = {}): ResolveSavedObjectsImportErrorsOptions => {
|
||||
readStream = new Readable();
|
||||
savedObjectsClient = savedObjectsClientMock.create();
|
||||
typeRegistry = typeRegistryMock.create();
|
||||
|
@ -101,6 +111,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
retries,
|
||||
savedObjectsClient,
|
||||
typeRegistry,
|
||||
importHooks,
|
||||
// namespace and createNewCopies don't matter, as they don't change the logic in this module, they just get passed to sub-module methods
|
||||
namespace,
|
||||
createNewCopies,
|
||||
|
@ -148,7 +159,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
describe('module calls', () => {
|
||||
test('validates retries', async () => {
|
||||
const retry = createRetry();
|
||||
const options = setupOptions([retry]);
|
||||
const options = setupOptions({ retries: [retry] });
|
||||
|
||||
await resolveSavedObjectsImportErrors(options);
|
||||
expect(validateRetries).toHaveBeenCalledWith([retry]);
|
||||
|
@ -156,7 +167,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
|
||||
test('creates objects filter', async () => {
|
||||
const retry = createRetry();
|
||||
const options = setupOptions([retry]);
|
||||
const options = setupOptions({ retries: [retry] });
|
||||
|
||||
await resolveSavedObjectsImportErrors(options);
|
||||
expect(createObjectsFilter).toHaveBeenCalledWith([retry]);
|
||||
|
@ -178,7 +189,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
|
||||
test('validates references', async () => {
|
||||
const retries = [createRetry()];
|
||||
const options = setupOptions(retries);
|
||||
const options = setupOptions({ retries });
|
||||
const collectedObjects = [createObject()];
|
||||
getMockFn(collectSavedObjects).mockResolvedValue({
|
||||
errors: [],
|
||||
|
@ -195,6 +206,30 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('execute import hooks', async () => {
|
||||
const importHooks = {
|
||||
foo: [jest.fn()],
|
||||
};
|
||||
const options = setupOptions({ importHooks });
|
||||
const collectedObjects = [createObject()];
|
||||
getMockFn(collectSavedObjects).mockResolvedValue({
|
||||
errors: [],
|
||||
collectedObjects,
|
||||
importIdMap: new Map(),
|
||||
});
|
||||
getMockFn(createSavedObjects).mockResolvedValueOnce({
|
||||
errors: [],
|
||||
createdObjects: collectedObjects,
|
||||
});
|
||||
|
||||
await resolveSavedObjectsImportErrors(options);
|
||||
|
||||
expect(executeImportHooks).toHaveBeenCalledWith({
|
||||
objects: collectedObjects,
|
||||
importHooks,
|
||||
});
|
||||
});
|
||||
|
||||
test('uses `retries` to replace references of collected objects before validating', async () => {
|
||||
const object = createObject([{ type: 'bar-type', id: 'abc', name: 'some name' }]);
|
||||
const retries = [
|
||||
|
@ -203,7 +238,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
replaceReferences: [{ type: 'bar-type', from: 'abc', to: 'def' }],
|
||||
}),
|
||||
];
|
||||
const options = setupOptions(retries);
|
||||
const options = setupOptions({ retries });
|
||||
getMockFn(collectSavedObjects).mockResolvedValue({
|
||||
errors: [],
|
||||
collectedObjects: [object],
|
||||
|
@ -226,7 +261,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
test('checks conflicts', async () => {
|
||||
const createNewCopies = (Symbol() as unknown) as boolean;
|
||||
const retries = [createRetry()];
|
||||
const options = setupOptions(retries, createNewCopies);
|
||||
const options = setupOptions({ retries, createNewCopies });
|
||||
const collectedObjects = [createObject()];
|
||||
getMockFn(collectSavedObjects).mockResolvedValue({
|
||||
errors: [],
|
||||
|
@ -248,7 +283,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
test('gets import ID map for retries', async () => {
|
||||
const retries = [createRetry()];
|
||||
const createNewCopies = (Symbol() as unknown) as boolean;
|
||||
const options = setupOptions(retries, createNewCopies);
|
||||
const options = setupOptions({ retries, createNewCopies });
|
||||
const filteredObjects = [createObject()];
|
||||
getMockFn(checkConflicts).mockResolvedValue({
|
||||
errors: [],
|
||||
|
@ -264,7 +299,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
|
||||
test('splits objects to overwrite from those not to overwrite', async () => {
|
||||
const retries = [createRetry()];
|
||||
const options = setupOptions(retries);
|
||||
const options = setupOptions({ retries });
|
||||
const collectedObjects = [createObject()];
|
||||
getMockFn(collectSavedObjects).mockResolvedValue({
|
||||
errors: [],
|
||||
|
@ -344,7 +379,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
|
||||
describe('with createNewCopies enabled', () => {
|
||||
test('regenerates object IDs', async () => {
|
||||
const options = setupOptions([], true);
|
||||
const options = setupOptions({ createNewCopies: true });
|
||||
const collectedObjects = [createObject()];
|
||||
getMockFn(collectSavedObjects).mockResolvedValue({
|
||||
errors: [],
|
||||
|
@ -357,7 +392,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
});
|
||||
|
||||
test('creates saved objects', async () => {
|
||||
const options = setupOptions([], true);
|
||||
const options = setupOptions({ createNewCopies: true });
|
||||
const errors = [createError(), createError(), createError()];
|
||||
getMockFn(collectSavedObjects).mockResolvedValue({
|
||||
errors: [errors[0]],
|
||||
|
@ -422,7 +457,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
const options = setupOptions();
|
||||
|
||||
const result = await resolveSavedObjectsImportErrors(options);
|
||||
expect(result).toEqual({ success: true, successCount: 0 });
|
||||
expect(result).toEqual({ success: true, successCount: 0, warnings: [] });
|
||||
});
|
||||
|
||||
test('returns success=false if an error occurred', async () => {
|
||||
|
@ -434,15 +469,40 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
});
|
||||
|
||||
const result = await resolveSavedObjectsImportErrors(options);
|
||||
expect(result).toEqual({ success: false, successCount: 0, errors: [expect.any(Object)] });
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [expect.any(Object)],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('executes import hooks', async () => {
|
||||
const options = setupOptions();
|
||||
const collectedObjects = [createObject()];
|
||||
getMockFn(collectSavedObjects).mockResolvedValue({
|
||||
errors: [],
|
||||
collectedObjects,
|
||||
importIdMap: new Map(),
|
||||
});
|
||||
getMockFn(createSavedObjects).mockResolvedValueOnce({
|
||||
errors: [],
|
||||
createdObjects: collectedObjects,
|
||||
});
|
||||
const warnings: SavedObjectsImportWarning[] = [{ type: 'simple', message: 'foo' }];
|
||||
getMockFn(executeImportHooks).mockResolvedValue(warnings);
|
||||
|
||||
const result = await resolveSavedObjectsImportErrors(options);
|
||||
|
||||
expect(result.warnings).toEqual(warnings);
|
||||
});
|
||||
|
||||
test('handles a mix of successes and errors and injects metadata', async () => {
|
||||
const error1 = createError();
|
||||
const error2 = createError();
|
||||
const options = setupOptions([
|
||||
{ type: error2.type, id: error2.id, overwrite: true, replaceReferences: [] },
|
||||
]);
|
||||
const options = setupOptions({
|
||||
retries: [{ type: error2.type, id: error2.id, overwrite: true, replaceReferences: [] }],
|
||||
});
|
||||
const obj1 = createObject();
|
||||
const tmp = createObject();
|
||||
const obj2 = { ...tmp, destinationId: 'some-destinationId', originId: tmp.id };
|
||||
|
@ -483,22 +543,30 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
{ ...error1, meta: { ...error1.meta, icon: `${error1.type}-icon` } },
|
||||
{ ...error2, meta: { ...error2.meta, icon: `${error2.type}-icon` }, overwrite: true },
|
||||
];
|
||||
expect(result).toEqual({ success: false, successCount: 3, successResults, errors });
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
successCount: 3,
|
||||
successResults,
|
||||
errors,
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('uses `type.management.getTitle` to resolve the titles', async () => {
|
||||
const obj1 = createObject([], { type: 'foo' });
|
||||
const obj2 = createObject([], { type: 'bar', title: 'bar-title' });
|
||||
|
||||
const options = setupOptions([], false, (type) => {
|
||||
if (type === 'foo') {
|
||||
const options = setupOptions({
|
||||
getTypeImpl: (type) => {
|
||||
if (type === 'foo') {
|
||||
return {
|
||||
management: { getTitle: () => 'getTitle-foo', icon: `${type}-icon` },
|
||||
};
|
||||
}
|
||||
return {
|
||||
management: { getTitle: () => 'getTitle-foo', icon: `${type}-icon` },
|
||||
management: { icon: `${type}-icon` },
|
||||
};
|
||||
}
|
||||
return {
|
||||
management: { icon: `${type}-icon` },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
getMockFn(checkConflicts).mockResolvedValue({
|
||||
|
@ -532,6 +600,7 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
success: true,
|
||||
successCount: 2,
|
||||
successResults,
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -555,7 +624,12 @@ describe('#importSavedObjectsFromStream', () => {
|
|||
|
||||
const result = await resolveSavedObjectsImportErrors(options);
|
||||
const expectedErrors = errors.map(({ type, id }) => expect.objectContaining({ type, id }));
|
||||
expect(result).toEqual({ success: false, successCount: 0, errors: expectedErrors });
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: expectedErrors,
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ import { SavedObject, SavedObjectsClientContract, SavedObjectsImportRetry } from
|
|||
import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry';
|
||||
import {
|
||||
SavedObjectsImportFailure,
|
||||
SavedObjectsImportHook,
|
||||
SavedObjectsImportResponse,
|
||||
SavedObjectsImportSuccess,
|
||||
} from './types';
|
||||
|
@ -24,6 +25,7 @@ import {
|
|||
createSavedObjects,
|
||||
getImportIdMapForRetries,
|
||||
checkConflicts,
|
||||
executeImportHooks,
|
||||
} from './lib';
|
||||
|
||||
/**
|
||||
|
@ -38,6 +40,8 @@ export interface ResolveSavedObjectsImportErrorsOptions {
|
|||
savedObjectsClient: SavedObjectsClientContract;
|
||||
/** The registry of all known saved object types */
|
||||
typeRegistry: ISavedObjectTypeRegistry;
|
||||
/** List of registered import hooks */
|
||||
importHooks: Record<string, SavedObjectsImportHook[]>;
|
||||
/** saved object import references to retry */
|
||||
retries: SavedObjectsImportRetry[];
|
||||
/** if specified, will import in given namespace */
|
||||
|
@ -58,6 +62,7 @@ export async function resolveSavedObjectsImportErrors({
|
|||
retries,
|
||||
savedObjectsClient,
|
||||
typeRegistry,
|
||||
importHooks,
|
||||
namespace,
|
||||
createNewCopies,
|
||||
}: ResolveSavedObjectsImportErrorsOptions): Promise<SavedObjectsImportResponse> {
|
||||
|
@ -146,6 +151,7 @@ export async function resolveSavedObjectsImportErrors({
|
|||
|
||||
// Bulk create in two batches, overwrites and non-overwrites
|
||||
let successResults: SavedObjectsImportSuccess[] = [];
|
||||
let successObjects: SavedObject[] = [];
|
||||
const accumulatedErrors = [...errorAccumulator];
|
||||
const bulkCreateObjects = async (
|
||||
objects: Array<SavedObject<{ title?: string }>>,
|
||||
|
@ -162,6 +168,7 @@ export async function resolveSavedObjectsImportErrors({
|
|||
const { createdObjects, errors: bulkCreateErrors } = await createSavedObjects(
|
||||
createSavedObjectsParams
|
||||
);
|
||||
successObjects = [...successObjects, ...createdObjects];
|
||||
errorAccumulator = [...errorAccumulator, ...bulkCreateErrors];
|
||||
successCount += createdObjects.length;
|
||||
successResults = [
|
||||
|
@ -200,9 +207,15 @@ export async function resolveSavedObjectsImportErrors({
|
|||
};
|
||||
});
|
||||
|
||||
const warnings = await executeImportHooks({
|
||||
objects: successObjects,
|
||||
importHooks,
|
||||
});
|
||||
|
||||
return {
|
||||
successCount,
|
||||
success: errorAccumulator.length === 0,
|
||||
warnings,
|
||||
...(successResults.length && { successResults }),
|
||||
...(errorResults.length && { errors: errorResults }),
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
SavedObjectsImportResponse,
|
||||
SavedObjectsImportOptions,
|
||||
SavedObjectsResolveImportErrorsOptions,
|
||||
SavedObjectsImportHook,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
|
@ -29,6 +30,7 @@ export class SavedObjectsImporter {
|
|||
readonly #savedObjectsClient: SavedObjectsClientContract;
|
||||
readonly #typeRegistry: ISavedObjectTypeRegistry;
|
||||
readonly #importSizeLimit: number;
|
||||
readonly #importHooks: Record<string, SavedObjectsImportHook[]>;
|
||||
|
||||
constructor({
|
||||
savedObjectsClient,
|
||||
|
@ -42,6 +44,15 @@ export class SavedObjectsImporter {
|
|||
this.#savedObjectsClient = savedObjectsClient;
|
||||
this.#typeRegistry = typeRegistry;
|
||||
this.#importSizeLimit = importSizeLimit;
|
||||
this.#importHooks = typeRegistry.getAllTypes().reduce((hooks, type) => {
|
||||
if (type.management?.onImport) {
|
||||
return {
|
||||
...hooks,
|
||||
[type.name]: [type.management.onImport],
|
||||
};
|
||||
}
|
||||
return hooks;
|
||||
}, {} as Record<string, SavedObjectsImportHook[]>);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -64,6 +75,7 @@ export class SavedObjectsImporter {
|
|||
objectLimit: this.#importSizeLimit,
|
||||
savedObjectsClient: this.#savedObjectsClient,
|
||||
typeRegistry: this.#typeRegistry,
|
||||
importHooks: this.#importHooks,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -87,6 +99,7 @@ export class SavedObjectsImporter {
|
|||
objectLimit: this.#importSizeLimit,
|
||||
savedObjectsClient: this.#savedObjectsClient,
|
||||
typeRegistry: this.#typeRegistry,
|
||||
importHooks: this.#importHooks,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -142,6 +142,7 @@ export interface SavedObjectsImportResponse {
|
|||
success: boolean;
|
||||
successCount: number;
|
||||
successResults?: SavedObjectsImportSuccess[];
|
||||
warnings: SavedObjectsImportWarning[];
|
||||
errors?: SavedObjectsImportFailure[];
|
||||
}
|
||||
|
||||
|
@ -176,3 +177,72 @@ export interface SavedObjectsResolveImportErrorsOptions {
|
|||
}
|
||||
|
||||
export type CreatedObject<T> = SavedObject<T> & { destinationId?: string };
|
||||
|
||||
/**
|
||||
* A simple informative warning that will be displayed to the user.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface SavedObjectsImportSimpleWarning {
|
||||
type: 'simple';
|
||||
/** The translated message to display to the user */
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A warning meant to notify that a specific user action is required to finalize the import
|
||||
* of some type of object.
|
||||
*
|
||||
* @remark The `actionUrl` must be a path relative to the basePath, and not include it.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface SavedObjectsImportActionRequiredWarning {
|
||||
type: 'action_required';
|
||||
/** The translated message to display to the user. */
|
||||
message: string;
|
||||
/** The path (without the basePath) that the user should be redirect to to address this warning. */
|
||||
actionPath: string;
|
||||
/** An optional label to use for the link button. If unspecified, a default label will be used. */
|
||||
buttonLabel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Composite type of all the possible types of import warnings.
|
||||
*
|
||||
* See {@link SavedObjectsImportSimpleWarning} and {@link SavedObjectsImportActionRequiredWarning}
|
||||
* for more details.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SavedObjectsImportWarning =
|
||||
| SavedObjectsImportSimpleWarning
|
||||
| SavedObjectsImportActionRequiredWarning;
|
||||
|
||||
/**
|
||||
* Result from a {@link SavedObjectsImportHook | import hook}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface SavedObjectsImportHookResult {
|
||||
/**
|
||||
* An optional list of warnings to display in the UI when the import succeeds.
|
||||
*/
|
||||
warnings?: SavedObjectsImportWarning[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook associated with a specific saved object type, that will be invoked during
|
||||
* the import process. The hook will have access to the objects of the registered type.
|
||||
*
|
||||
* Currently, the only supported feature for import hooks is to return warnings to be displayed
|
||||
* in the UI when the import succeeds.
|
||||
*
|
||||
* @remark The only interactions the hook can have with the import process is via the hook's
|
||||
* response. Mutating the objects inside the hook's code will have no effect.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SavedObjectsImportHook<T = unknown> = (
|
||||
objects: Array<SavedObject<T>>
|
||||
) => SavedObjectsImportHookResult | Promise<SavedObjectsImportHookResult>;
|
||||
|
|
|
@ -23,6 +23,11 @@ export {
|
|||
SavedObjectsImportUnsupportedTypeError,
|
||||
SavedObjectsResolveImportErrorsOptions,
|
||||
SavedObjectsImportError,
|
||||
SavedObjectsImportHook,
|
||||
SavedObjectsImportHookResult,
|
||||
SavedObjectsImportSimpleWarning,
|
||||
SavedObjectsImportActionRequiredWarning,
|
||||
SavedObjectsImportWarning,
|
||||
} from './import';
|
||||
|
||||
export {
|
||||
|
|
|
@ -101,7 +101,7 @@ describe(`POST ${URL}`, () => {
|
|||
)
|
||||
.expect(200);
|
||||
|
||||
expect(result.body).toEqual({ success: true, successCount: 0 });
|
||||
expect(result.body).toEqual({ success: true, successCount: 0, warnings: [] });
|
||||
expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created
|
||||
expect(coreUsageStatsClient.incrementSavedObjectsImport).toHaveBeenCalledWith({
|
||||
request: expect.anything(),
|
||||
|
@ -138,6 +138,7 @@ describe(`POST ${URL}`, () => {
|
|||
meta: { title: 'my-pattern-*', icon: 'index-pattern-icon' },
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
|
||||
|
@ -187,6 +188,7 @@ describe(`POST ${URL}`, () => {
|
|||
meta: { title: mockDashboard.attributes.title, icon: 'dashboard-icon' },
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
|
||||
});
|
||||
|
@ -235,6 +237,7 @@ describe(`POST ${URL}`, () => {
|
|||
error: { type: 'conflict' },
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // successResults objects were not created because resolvable errors are present
|
||||
});
|
||||
|
@ -283,6 +286,7 @@ describe(`POST ${URL}`, () => {
|
|||
meta: { title: mockDashboard.attributes.title, icon: 'dashboard-icon' },
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
|
||||
});
|
||||
|
@ -336,6 +340,7 @@ describe(`POST ${URL}`, () => {
|
|||
meta: { title: mockDashboard.attributes.title, icon: 'dashboard-icon' },
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith(
|
||||
|
@ -406,6 +411,7 @@ describe(`POST ${URL}`, () => {
|
|||
meta: { title: mockDashboard.attributes.title, icon: 'dashboard-icon' },
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith(
|
||||
|
@ -470,6 +476,7 @@ describe(`POST ${URL}`, () => {
|
|||
meta: { title: mockDashboard.attributes.title, icon: 'dashboard-icon' },
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith(
|
||||
|
@ -534,6 +541,7 @@ describe(`POST ${URL}`, () => {
|
|||
destinationId: obj2.id,
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
|
||||
|
|
|
@ -113,7 +113,7 @@ describe(`POST ${URL}`, () => {
|
|||
)
|
||||
.expect(200);
|
||||
|
||||
expect(result.body).toEqual({ success: true, successCount: 0 });
|
||||
expect(result.body).toEqual({ success: true, successCount: 0, warnings: [] });
|
||||
expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created
|
||||
expect(coreUsageStatsClient.incrementSavedObjectsResolveImportErrors).toHaveBeenCalledWith({
|
||||
request: expect.anything(),
|
||||
|
@ -153,6 +153,7 @@ describe(`POST ${URL}`, () => {
|
|||
success: true,
|
||||
successCount: 1,
|
||||
successResults: [{ type, id, meta }],
|
||||
warnings: [],
|
||||
});
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
|
||||
|
@ -190,6 +191,7 @@ describe(`POST ${URL}`, () => {
|
|||
success: true,
|
||||
successCount: 1,
|
||||
successResults: [{ type, id, meta }],
|
||||
warnings: [],
|
||||
});
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
|
||||
|
@ -228,6 +230,7 @@ describe(`POST ${URL}`, () => {
|
|||
success: true,
|
||||
successCount: 1,
|
||||
successResults: [{ type, id, meta, overwrite: true }],
|
||||
warnings: [],
|
||||
});
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
|
||||
|
@ -271,6 +274,7 @@ describe(`POST ${URL}`, () => {
|
|||
meta: { title: 'Look at my visualization', icon: 'visualization-icon' },
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
|
||||
|
@ -319,6 +323,7 @@ describe(`POST ${URL}`, () => {
|
|||
meta: { title: 'Look at my visualization', icon: 'visualization-icon' },
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
|
||||
|
@ -383,6 +388,7 @@ describe(`POST ${URL}`, () => {
|
|||
destinationId: obj2.id,
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); // successResults objects were created because no resolvable errors are present
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith(
|
||||
|
|
|
@ -43,6 +43,7 @@ import { SavedObjectsImporter, ISavedObjectsImporter } from './import';
|
|||
import { registerRoutes } from './routes';
|
||||
import { ServiceStatus } from '../status';
|
||||
import { calculateStatus$ } from './status';
|
||||
|
||||
/**
|
||||
* Saved Objects is Kibana's data persistence mechanism allowing plugins to
|
||||
* use Elasticsearch for storing and querying state. The SavedObjectsServiceSetup API exposes methods
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import { SavedObjectsClient } from './service/saved_objects_client';
|
||||
import { SavedObjectsTypeMappingDefinition } from './mappings';
|
||||
import { SavedObjectMigrationMap } from './migrations';
|
||||
import { SavedObjectsImportHook } from './import/types';
|
||||
|
||||
export {
|
||||
SavedObjectsImportResponse,
|
||||
|
@ -20,6 +21,9 @@ export {
|
|||
SavedObjectsImportUnknownError,
|
||||
SavedObjectsImportFailure,
|
||||
SavedObjectsImportRetry,
|
||||
SavedObjectsImportActionRequiredWarning,
|
||||
SavedObjectsImportSimpleWarning,
|
||||
SavedObjectsImportWarning,
|
||||
} from './import/types';
|
||||
|
||||
import { SavedObject } from '../../types';
|
||||
|
@ -281,4 +285,46 @@ export interface SavedObjectsTypeManagementDefinition {
|
|||
* {@link Capabilities | uiCapabilities} to check if the user has permission to access the object.
|
||||
*/
|
||||
getInAppUrl?: (savedObject: SavedObject<any>) => { path: string; uiCapabilitiesPath: string };
|
||||
/**
|
||||
* An optional {@link SavedObjectsImportHook | import hook} to use when importing given type.
|
||||
*
|
||||
* Import hooks are executed during the savedObjects import process and allow to interact
|
||||
* with the imported objects. See the {@link SavedObjectsImportHook | hook documentation}
|
||||
* for more info.
|
||||
*
|
||||
* @example
|
||||
* Registering a hook displaying a warning about a specific type of object
|
||||
* ```ts
|
||||
* // src/plugins/my_plugin/server/plugin.ts
|
||||
* import { myType } from './saved_objects';
|
||||
*
|
||||
* export class Plugin() {
|
||||
* setup: (core: CoreSetup) => {
|
||||
* core.savedObjects.registerType({
|
||||
* ...myType,
|
||||
* management: {
|
||||
* ...myType.management,
|
||||
* onImport: (objects) => {
|
||||
* if(someActionIsNeeded(objects)) {
|
||||
* return {
|
||||
* warnings: [
|
||||
* {
|
||||
* type: 'action_required',
|
||||
* message: 'Objects need to be manually enabled after import',
|
||||
* actionPath: '/app/my-app/require-activation',
|
||||
* },
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
* return {};
|
||||
* }
|
||||
* },
|
||||
* });
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @remark messages returned in the warnings are user facing and must be translated.
|
||||
*/
|
||||
onImport?: SavedObjectsImportHook;
|
||||
}
|
||||
|
|
|
@ -2473,6 +2473,15 @@ export interface SavedObjectsFindResult<T = unknown> extends SavedObject<T> {
|
|||
score: number;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface SavedObjectsImportActionRequiredWarning {
|
||||
actionPath: string;
|
||||
buttonLabel?: string;
|
||||
message: string;
|
||||
// (undocumented)
|
||||
type: 'action_required';
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface SavedObjectsImportAmbiguousConflictError {
|
||||
// (undocumented)
|
||||
|
@ -2542,6 +2551,14 @@ export interface SavedObjectsImportFailure {
|
|||
type: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type SavedObjectsImportHook<T = unknown> = (objects: Array<SavedObject<T>>) => SavedObjectsImportHookResult | Promise<SavedObjectsImportHookResult>;
|
||||
|
||||
// @public
|
||||
export interface SavedObjectsImportHookResult {
|
||||
warnings?: SavedObjectsImportWarning[];
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface SavedObjectsImportMissingReferencesError {
|
||||
// (undocumented)
|
||||
|
@ -2571,6 +2588,8 @@ export interface SavedObjectsImportResponse {
|
|||
successCount: number;
|
||||
// (undocumented)
|
||||
successResults?: SavedObjectsImportSuccess[];
|
||||
// (undocumented)
|
||||
warnings: SavedObjectsImportWarning[];
|
||||
}
|
||||
|
||||
// @public
|
||||
|
@ -2592,6 +2611,13 @@ export interface SavedObjectsImportRetry {
|
|||
type: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface SavedObjectsImportSimpleWarning {
|
||||
message: string;
|
||||
// (undocumented)
|
||||
type: 'simple';
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface SavedObjectsImportSuccess {
|
||||
// @deprecated (undocumented)
|
||||
|
@ -2625,6 +2651,9 @@ export interface SavedObjectsImportUnsupportedTypeError {
|
|||
type: 'unsupported_type';
|
||||
}
|
||||
|
||||
// @public
|
||||
export type SavedObjectsImportWarning = SavedObjectsImportSimpleWarning | SavedObjectsImportActionRequiredWarning;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsIncrementCounterField {
|
||||
fieldName: string;
|
||||
|
@ -2790,6 +2819,7 @@ export interface SavedObjectsTypeManagementDefinition {
|
|||
getTitle?: (savedObject: SavedObject<any>) => string;
|
||||
icon?: string;
|
||||
importableAndExportable?: boolean;
|
||||
onImport?: SavedObjectsImportHook;
|
||||
}
|
||||
|
||||
// @public
|
||||
|
|
|
@ -6,15 +6,9 @@
|
|||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { HttpStart, SavedObjectsImportFailure } from 'src/core/public';
|
||||
import { HttpStart, SavedObjectsImportResponse } from 'src/core/public';
|
||||
import { ImportMode } from '../management_section/objects_table/components/import_mode_control';
|
||||
|
||||
interface ImportResponse {
|
||||
success: boolean;
|
||||
successCount: number;
|
||||
errors?: SavedObjectsImportFailure[];
|
||||
}
|
||||
|
||||
export async function importFile(
|
||||
http: HttpStart,
|
||||
file: File,
|
||||
|
@ -23,7 +17,7 @@ export async function importFile(
|
|||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const query = createNewCopies ? { createNewCopies } : { overwrite };
|
||||
return await http.post<ImportResponse>('/api/saved_objects/_import', {
|
||||
return await http.post<SavedObjectsImportResponse>('/api/saved_objects/_import', {
|
||||
body: formData,
|
||||
headers: {
|
||||
// Important to be undefined, it forces proper headers to be set for FormData
|
||||
|
|
|
@ -11,14 +11,16 @@ import {
|
|||
SavedObjectsImportAmbiguousConflictError,
|
||||
SavedObjectsImportUnknownError,
|
||||
SavedObjectsImportMissingReferencesError,
|
||||
SavedObjectsImportResponse,
|
||||
} from 'src/core/public';
|
||||
import { processImportResponse } from './process_import_response';
|
||||
|
||||
describe('processImportResponse()', () => {
|
||||
test('works when no errors exist in the response', () => {
|
||||
const response = {
|
||||
const response: SavedObjectsImportResponse = {
|
||||
success: true,
|
||||
successCount: 0,
|
||||
warnings: [],
|
||||
};
|
||||
const result = processImportResponse(response);
|
||||
expect(result.status).toBe('success');
|
||||
|
@ -26,7 +28,7 @@ describe('processImportResponse()', () => {
|
|||
});
|
||||
|
||||
test('conflict errors get added to failedImports and result in idle status', () => {
|
||||
const response = {
|
||||
const response: SavedObjectsImportResponse = {
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
|
@ -39,6 +41,7 @@ describe('processImportResponse()', () => {
|
|||
meta: {},
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
};
|
||||
const result = processImportResponse(response);
|
||||
expect(result.failedImports).toMatchInlineSnapshot(`
|
||||
|
@ -59,7 +62,7 @@ describe('processImportResponse()', () => {
|
|||
});
|
||||
|
||||
test('ambiguous conflict errors get added to failedImports and result in idle status', () => {
|
||||
const response = {
|
||||
const response: SavedObjectsImportResponse = {
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
|
@ -72,6 +75,7 @@ describe('processImportResponse()', () => {
|
|||
meta: {},
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
};
|
||||
const result = processImportResponse(response);
|
||||
expect(result.failedImports).toMatchInlineSnapshot(`
|
||||
|
@ -92,7 +96,7 @@ describe('processImportResponse()', () => {
|
|||
});
|
||||
|
||||
test('unknown errors get added to failedImports and result in success status', () => {
|
||||
const response = {
|
||||
const response: SavedObjectsImportResponse = {
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
|
@ -105,6 +109,7 @@ describe('processImportResponse()', () => {
|
|||
meta: {},
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
};
|
||||
const result = processImportResponse(response);
|
||||
expect(result.failedImports).toMatchInlineSnapshot(`
|
||||
|
@ -125,7 +130,7 @@ describe('processImportResponse()', () => {
|
|||
});
|
||||
|
||||
test('missing references get added to failedImports and result in idle status', () => {
|
||||
const response = {
|
||||
const response: SavedObjectsImportResponse = {
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
|
@ -144,6 +149,7 @@ describe('processImportResponse()', () => {
|
|||
meta: {},
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
};
|
||||
const result = processImportResponse(response);
|
||||
expect(result.failedImports).toMatchInlineSnapshot(`
|
||||
|
@ -170,7 +176,7 @@ describe('processImportResponse()', () => {
|
|||
});
|
||||
|
||||
test('missing references get added to unmatchedReferences, but are not duplicated', () => {
|
||||
const response = {
|
||||
const response: SavedObjectsImportResponse = {
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
|
@ -188,6 +194,7 @@ describe('processImportResponse()', () => {
|
|||
meta: {},
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
};
|
||||
const result = processImportResponse(response);
|
||||
expect(result.unmatchedReferences).toEqual([
|
||||
|
@ -197,10 +204,11 @@ describe('processImportResponse()', () => {
|
|||
});
|
||||
|
||||
test('success results get added to successfulImports and result in success status', () => {
|
||||
const response = {
|
||||
const response: SavedObjectsImportResponse = {
|
||||
success: true,
|
||||
successCount: 1,
|
||||
successResults: [{ type: 'a', id: '1', meta: {} }],
|
||||
warnings: [],
|
||||
};
|
||||
const result = processImportResponse(response);
|
||||
expect(result.successfulImports).toMatchInlineSnapshot(`
|
||||
|
@ -214,4 +222,22 @@ describe('processImportResponse()', () => {
|
|||
`);
|
||||
expect(result.status).toBe('success');
|
||||
});
|
||||
|
||||
test('warnings from the response get returned', () => {
|
||||
const response: SavedObjectsImportResponse = {
|
||||
success: true,
|
||||
successCount: 1,
|
||||
successResults: [{ type: 'a', id: '1', meta: {} }],
|
||||
warnings: [
|
||||
{
|
||||
type: 'action_required',
|
||||
message: 'foo',
|
||||
actionPath: '/somewhere',
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = processImportResponse(response);
|
||||
expect(result.status).toBe('success');
|
||||
expect(result.importWarnings).toEqual(response.warnings);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
SavedObjectsImportUnknownError,
|
||||
SavedObjectsImportFailure,
|
||||
SavedObjectsImportSuccess,
|
||||
SavedObjectsImportWarning,
|
||||
} from 'src/core/public';
|
||||
|
||||
export interface FailedImport {
|
||||
|
@ -41,6 +42,7 @@ export interface ProcessedImportResponse {
|
|||
importCount: number;
|
||||
conflictedSavedObjectsLinkedToSavedSearches: undefined;
|
||||
conflictedSearchDocs: undefined;
|
||||
importWarnings: SavedObjectsImportWarning[];
|
||||
}
|
||||
|
||||
const isAnyConflict = ({ type }: FailedImport['error']) =>
|
||||
|
@ -87,5 +89,6 @@ export function processImportResponse(
|
|||
importCount: response.successCount,
|
||||
conflictedSavedObjectsLinkedToSavedSearches: undefined,
|
||||
conflictedSearchDocs: undefined,
|
||||
importWarnings: response.warnings,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -226,6 +226,7 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = `
|
|||
"createNewCopies": false,
|
||||
"overwrite": true,
|
||||
},
|
||||
"importWarnings": undefined,
|
||||
"indexPatterns": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
|
@ -668,7 +669,18 @@ exports[`Flyout should render import step 1`] = `
|
|||
|
||||
exports[`Flyout summary should display summary when import is complete 1`] = `
|
||||
<ImportSummary
|
||||
basePath={
|
||||
BasePath {
|
||||
"basePath": "",
|
||||
"get": [Function],
|
||||
"prepend": [Function],
|
||||
"publicBaseUrl": undefined,
|
||||
"remove": [Function],
|
||||
"serverBasePath": "",
|
||||
}
|
||||
}
|
||||
failedImports={Symbol()}
|
||||
importWarnings={Array []}
|
||||
successfulImports={Symbol()}
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
|
||||
import React from 'react';
|
||||
import { shallowWithI18nProvider } from '@kbn/test/jest';
|
||||
import { coreMock } from '../../../../../../core/public/mocks';
|
||||
import { coreMock, httpServiceMock } from '../../../../../../core/public/mocks';
|
||||
import { serviceRegistryMock } from '../../../services/service_registry.mock';
|
||||
import { Flyout, FlyoutProps, FlyoutState } from './flyout';
|
||||
import { ShallowWrapper } from 'enzyme';
|
||||
|
@ -47,6 +47,7 @@ describe('Flyout', () => {
|
|||
beforeEach(() => {
|
||||
const { http, overlays } = coreMock.createStart();
|
||||
const search = dataPluginMock.createStartContract().search;
|
||||
const basePath = httpServiceMock.createBasePath();
|
||||
|
||||
defaultProps = {
|
||||
close: jest.fn(),
|
||||
|
@ -63,6 +64,7 @@ describe('Flyout', () => {
|
|||
allowedTypes: ['search', 'index-pattern', 'visualization'],
|
||||
serviceRegistry: serviceRegistryMock.create(),
|
||||
search,
|
||||
basePath,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { OverlayStart, HttpStart } from 'src/core/public';
|
||||
import { OverlayStart, HttpStart, IBasePath } from 'src/core/public';
|
||||
import {
|
||||
IndexPatternsContract,
|
||||
IIndexPattern,
|
||||
|
@ -69,6 +69,7 @@ export interface FlyoutProps {
|
|||
indexPatterns: IndexPatternsContract;
|
||||
overlays: OverlayStart;
|
||||
http: HttpStart;
|
||||
basePath: IBasePath;
|
||||
search: DataPublicPluginStart['search'];
|
||||
}
|
||||
|
||||
|
@ -81,6 +82,7 @@ export interface FlyoutState {
|
|||
failedImports?: ProcessedImportResponse['failedImports'];
|
||||
successfulImports?: ProcessedImportResponse['successfulImports'];
|
||||
conflictingRecord?: ConflictingRecord;
|
||||
importWarnings?: ProcessedImportResponse['importWarnings'];
|
||||
error?: string;
|
||||
file?: File;
|
||||
importCount: number;
|
||||
|
@ -616,6 +618,7 @@ export class Flyout extends Component<FlyoutProps, FlyoutState> {
|
|||
successfulImports = [],
|
||||
isLegacyFile,
|
||||
importMode,
|
||||
importWarnings,
|
||||
} = this.state;
|
||||
|
||||
if (status === 'loading') {
|
||||
|
@ -632,8 +635,15 @@ export class Flyout extends Component<FlyoutProps, FlyoutState> {
|
|||
);
|
||||
}
|
||||
|
||||
if (isLegacyFile === false && status === 'success') {
|
||||
return <ImportSummary failedImports={failedImports} successfulImports={successfulImports} />;
|
||||
if (!isLegacyFile && status === 'success') {
|
||||
return (
|
||||
<ImportSummary
|
||||
basePath={this.props.http.basePath}
|
||||
failedImports={failedImports}
|
||||
successfulImports={successfulImports}
|
||||
importWarnings={importWarnings ?? []}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Import summary for failed legacy import
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ShallowWrapper } from 'enzyme';
|
||||
import { shallowWithI18nProvider } from '@kbn/test/jest';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { mountWithI18nProvider } from '@kbn/test/jest';
|
||||
import { httpServiceMock } from '../../../../../../core/public/mocks';
|
||||
import { ImportSummary, ImportSummaryProps } from './import_summary';
|
||||
import { FailedImport } from '../../../lib';
|
||||
|
||||
|
@ -16,6 +17,20 @@ import { FailedImport } from '../../../lib';
|
|||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
|
||||
describe('ImportSummary', () => {
|
||||
let basePath: ReturnType<typeof httpServiceMock.createBasePath>;
|
||||
|
||||
const getProps = (parts: Partial<ImportSummaryProps>): ImportSummaryProps => ({
|
||||
basePath,
|
||||
failedImports: [],
|
||||
successfulImports: [],
|
||||
importWarnings: [],
|
||||
...parts,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
basePath = httpServiceMock.createBasePath();
|
||||
});
|
||||
|
||||
const errorUnsupportedType: FailedImport = {
|
||||
obj: { type: 'error-obj-type', id: 'error-obj-id', meta: { title: 'Error object' } },
|
||||
error: { type: 'unsupported_type' },
|
||||
|
@ -28,19 +43,20 @@ describe('ImportSummary', () => {
|
|||
overwrite: true,
|
||||
};
|
||||
|
||||
const findHeader = (wrapper: ShallowWrapper) => wrapper.find('h3');
|
||||
const findCountCreated = (wrapper: ShallowWrapper) =>
|
||||
const findHeader = (wrapper: ReactWrapper) => wrapper.find('h3');
|
||||
const findCountCreated = (wrapper: ReactWrapper) =>
|
||||
wrapper.find('h4.savedObjectsManagementImportSummary__createdCount');
|
||||
const findCountOverwritten = (wrapper: ShallowWrapper) =>
|
||||
const findCountOverwritten = (wrapper: ReactWrapper) =>
|
||||
wrapper.find('h4.savedObjectsManagementImportSummary__overwrittenCount');
|
||||
const findCountError = (wrapper: ShallowWrapper) =>
|
||||
const findCountError = (wrapper: ReactWrapper) =>
|
||||
wrapper.find('h4.savedObjectsManagementImportSummary__errorCount');
|
||||
const findObjectRow = (wrapper: ShallowWrapper) =>
|
||||
wrapper.find('.savedObjectsManagementImportSummary__row');
|
||||
const findObjectRow = (wrapper: ReactWrapper) =>
|
||||
wrapper.find('.savedObjectsManagementImportSummary__row').hostNodes();
|
||||
const findWarnings = (wrapper: ReactWrapper) => wrapper.find('ImportWarning');
|
||||
|
||||
it('should render as expected with no results', async () => {
|
||||
const props: ImportSummaryProps = { failedImports: [], successfulImports: [] };
|
||||
const wrapper = shallowWithI18nProvider(<ImportSummary {...props} />);
|
||||
const props = getProps({ failedImports: [], successfulImports: [] });
|
||||
const wrapper = mountWithI18nProvider(<ImportSummary {...props} />);
|
||||
|
||||
expect(findHeader(wrapper).childAt(0).props()).toEqual(
|
||||
expect.objectContaining({ values: { importCount: 0 } })
|
||||
|
@ -52,14 +68,14 @@ describe('ImportSummary', () => {
|
|||
});
|
||||
|
||||
it('should render as expected with a newly created object', async () => {
|
||||
const props: ImportSummaryProps = {
|
||||
const props = getProps({
|
||||
failedImports: [],
|
||||
successfulImports: [successNew],
|
||||
};
|
||||
const wrapper = shallowWithI18nProvider(<ImportSummary {...props} />);
|
||||
});
|
||||
const wrapper = mountWithI18nProvider(<ImportSummary {...props} />);
|
||||
|
||||
expect(findHeader(wrapper).childAt(0).props()).toEqual(
|
||||
expect.not.objectContaining({ values: expect.anything() }) // no importCount for singular
|
||||
expect.objectContaining({ values: { importCount: 1 } })
|
||||
);
|
||||
const countCreated = findCountCreated(wrapper);
|
||||
expect(countCreated).toHaveLength(1);
|
||||
|
@ -68,18 +84,19 @@ describe('ImportSummary', () => {
|
|||
);
|
||||
expect(findCountOverwritten(wrapper)).toHaveLength(0);
|
||||
expect(findCountError(wrapper)).toHaveLength(0);
|
||||
|
||||
expect(findObjectRow(wrapper)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should render as expected with an overwritten object', async () => {
|
||||
const props: ImportSummaryProps = {
|
||||
const props = getProps({
|
||||
failedImports: [],
|
||||
successfulImports: [successOverwritten],
|
||||
};
|
||||
const wrapper = shallowWithI18nProvider(<ImportSummary {...props} />);
|
||||
});
|
||||
const wrapper = mountWithI18nProvider(<ImportSummary {...props} />);
|
||||
|
||||
expect(findHeader(wrapper).childAt(0).props()).toEqual(
|
||||
expect.not.objectContaining({ values: expect.anything() }) // no importCount for singular
|
||||
expect.objectContaining({ values: { importCount: 1 } })
|
||||
);
|
||||
expect(findCountCreated(wrapper)).toHaveLength(0);
|
||||
const countOverwritten = findCountOverwritten(wrapper);
|
||||
|
@ -92,14 +109,14 @@ describe('ImportSummary', () => {
|
|||
});
|
||||
|
||||
it('should render as expected with an error object', async () => {
|
||||
const props: ImportSummaryProps = {
|
||||
const props = getProps({
|
||||
failedImports: [errorUnsupportedType],
|
||||
successfulImports: [],
|
||||
};
|
||||
const wrapper = shallowWithI18nProvider(<ImportSummary {...props} />);
|
||||
});
|
||||
const wrapper = mountWithI18nProvider(<ImportSummary {...props} />);
|
||||
|
||||
expect(findHeader(wrapper).childAt(0).props()).toEqual(
|
||||
expect.not.objectContaining({ values: expect.anything() }) // no importCount for singular
|
||||
expect.objectContaining({ values: { importCount: 1 } })
|
||||
);
|
||||
expect(findCountCreated(wrapper)).toHaveLength(0);
|
||||
expect(findCountOverwritten(wrapper)).toHaveLength(0);
|
||||
|
@ -112,11 +129,11 @@ describe('ImportSummary', () => {
|
|||
});
|
||||
|
||||
it('should render as expected with mixed objects', async () => {
|
||||
const props: ImportSummaryProps = {
|
||||
const props = getProps({
|
||||
failedImports: [errorUnsupportedType],
|
||||
successfulImports: [successNew, successOverwritten],
|
||||
};
|
||||
const wrapper = shallowWithI18nProvider(<ImportSummary {...props} />);
|
||||
});
|
||||
const wrapper = mountWithI18nProvider(<ImportSummary {...props} />);
|
||||
|
||||
expect(findHeader(wrapper).childAt(0).props()).toEqual(
|
||||
expect.objectContaining({ values: { importCount: 3 } })
|
||||
|
@ -138,4 +155,24 @@ describe('ImportSummary', () => {
|
|||
);
|
||||
expect(findObjectRow(wrapper)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should render warnings when present', async () => {
|
||||
const props = getProps({
|
||||
successfulImports: [successNew],
|
||||
importWarnings: [
|
||||
{
|
||||
type: 'simple',
|
||||
message: 'foo',
|
||||
},
|
||||
{
|
||||
type: 'action_required',
|
||||
message: 'bar',
|
||||
actionPath: '/app/lost',
|
||||
},
|
||||
],
|
||||
});
|
||||
const wrapper = mountWithI18nProvider(<ImportSummary {...props} />);
|
||||
|
||||
expect(findWarnings(wrapper)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,11 +8,13 @@
|
|||
|
||||
import './import_summary.scss';
|
||||
import _ from 'lodash';
|
||||
import React, { Fragment } from 'react';
|
||||
import React, { Fragment, FC, useMemo } from 'react';
|
||||
import {
|
||||
EuiText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiCallOut,
|
||||
EuiButton,
|
||||
EuiToolTip,
|
||||
EuiIcon,
|
||||
EuiIconTip,
|
||||
|
@ -22,15 +24,20 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { SavedObjectsImportSuccess } from 'kibana/public';
|
||||
import { FailedImport } from '../../..';
|
||||
import { getDefaultTitle, getSavedObjectLabel } from '../../../lib';
|
||||
import type {
|
||||
SavedObjectsImportSuccess,
|
||||
SavedObjectsImportWarning,
|
||||
IBasePath,
|
||||
} from 'kibana/public';
|
||||
import { getDefaultTitle, getSavedObjectLabel, FailedImport } from '../../../lib';
|
||||
|
||||
const DEFAULT_ICON = 'apps';
|
||||
|
||||
export interface ImportSummaryProps {
|
||||
failedImports: FailedImport[];
|
||||
successfulImports: SavedObjectsImportSuccess[];
|
||||
importWarnings: SavedObjectsImportWarning[];
|
||||
basePath: IBasePath;
|
||||
}
|
||||
|
||||
interface ImportItem {
|
||||
|
@ -72,7 +79,7 @@ const mapImportSuccess = (obj: SavedObjectsImportSuccess): ImportItem => {
|
|||
return { type, id, title, icon, outcome };
|
||||
};
|
||||
|
||||
const getCountIndicators = (importItems: ImportItem[]) => {
|
||||
const CountIndicators: FC<{ importItems: ImportItem[] }> = ({ importItems }) => {
|
||||
if (!importItems.length) {
|
||||
return null;
|
||||
}
|
||||
|
@ -130,7 +137,8 @@ const getCountIndicators = (importItems: ImportItem[]) => {
|
|||
);
|
||||
};
|
||||
|
||||
const getStatusIndicator = ({ outcome, errorMessage = 'Error' }: ImportItem) => {
|
||||
const StatusIndicator: FC<{ item: ImportItem }> = ({ item }) => {
|
||||
const { outcome, errorMessage = 'Error' } = item;
|
||||
switch (outcome) {
|
||||
case 'created':
|
||||
return (
|
||||
|
@ -165,13 +173,85 @@ const getStatusIndicator = ({ outcome, errorMessage = 'Error' }: ImportItem) =>
|
|||
}
|
||||
};
|
||||
|
||||
export const ImportSummary = ({ failedImports, successfulImports }: ImportSummaryProps) => {
|
||||
const importItems: ImportItem[] = _.sortBy(
|
||||
[
|
||||
...failedImports.map((x) => mapFailedImport(x)),
|
||||
...successfulImports.map((x) => mapImportSuccess(x)),
|
||||
],
|
||||
['type', 'title']
|
||||
const ImportWarnings: FC<{ warnings: SavedObjectsImportWarning[]; basePath: IBasePath }> = ({
|
||||
warnings,
|
||||
basePath,
|
||||
}) => {
|
||||
if (!warnings.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
{warnings.map((warning, index) => (
|
||||
<Fragment key={`warning-${index}`}>
|
||||
<ImportWarning warning={warning} basePath={basePath} />
|
||||
{index < warnings.length - 1 && <EuiSpacer size="s" />}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ImportWarning: FC<{ warning: SavedObjectsImportWarning; basePath: IBasePath }> = ({
|
||||
warning,
|
||||
basePath,
|
||||
}) => {
|
||||
const warningContent = useMemo(() => {
|
||||
if (warning.type === 'action_required') {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="flexEnd" justifyContent="flexEnd" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
size="s"
|
||||
color="warning"
|
||||
href={basePath.prepend(warning.actionPath)}
|
||||
target="_blank"
|
||||
>
|
||||
{warning.buttonLabel || (
|
||||
<FormattedMessage
|
||||
id="savedObjectsManagement.importSummary.warnings.defaultButtonLabel"
|
||||
defaultMessage="Go"
|
||||
/>
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [warning, basePath]);
|
||||
|
||||
return (
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
size="s"
|
||||
iconType="alert"
|
||||
data-test-subj="importSavedObjectsWarning"
|
||||
title={warning.message}
|
||||
>
|
||||
{warningContent}
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
||||
|
||||
export const ImportSummary: FC<ImportSummaryProps> = ({
|
||||
failedImports,
|
||||
successfulImports,
|
||||
importWarnings,
|
||||
basePath,
|
||||
}) => {
|
||||
const importItems: ImportItem[] = useMemo(
|
||||
() =>
|
||||
_.sortBy(
|
||||
[
|
||||
...failedImports.map((x) => mapFailedImport(x)),
|
||||
...successfulImports.map((x) => mapImportSuccess(x)),
|
||||
],
|
||||
['type', 'title']
|
||||
),
|
||||
[successfulImports, failedImports]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -183,22 +263,16 @@ export const ImportSummary = ({ failedImports, successfulImports }: ImportSummar
|
|||
}
|
||||
>
|
||||
<h3>
|
||||
{importItems.length === 1 ? (
|
||||
<FormattedMessage
|
||||
id="savedObjectsManagement.importSummary.headerLabelSingular"
|
||||
defaultMessage="1 object imported"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="savedObjectsManagement.importSummary.headerLabelPlural"
|
||||
defaultMessage="{importCount} objects imported"
|
||||
values={{ importCount: importItems.length }}
|
||||
/>
|
||||
)}
|
||||
<FormattedMessage
|
||||
id="savedObjectsManagement.importSummary.headerLabel"
|
||||
defaultMessage="{importCount, plural, one {1 object} other {# objects}} imported"
|
||||
values={{ importCount: importItems.length }}
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
{getCountIndicators(importItems)}
|
||||
<EuiSpacer size="s" />
|
||||
<CountIndicators importItems={importItems} />
|
||||
<ImportWarnings warnings={importWarnings} basePath={basePath} />
|
||||
<EuiHorizontalRule />
|
||||
{importItems.map((item, index) => {
|
||||
const { type, title, icon } = item;
|
||||
|
@ -223,7 +297,9 @@ export const ImportSummary = ({ failedImports, successfulImports }: ImportSummar
|
|||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<div className="eui-textRight">{getStatusIndicator(item)}</div>
|
||||
<div className="eui-textRight">
|
||||
<StatusIndicator item={item} />
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -501,6 +501,7 @@ export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedOb
|
|||
newIndexPatternUrl={newIndexPatternUrl}
|
||||
allowedTypes={this.props.allowedTypes}
|
||||
overlays={this.props.overlays}
|
||||
basePath={this.props.http.basePath}
|
||||
search={this.props.search}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -74,6 +74,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
createConflictError(visualization),
|
||||
createConflictError(dashboard),
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -93,6 +94,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
{ ...visualization, overwrite: true },
|
||||
{ ...dashboard, overwrite: true },
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -119,6 +121,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
error: { type: 'unsupported_type' },
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -157,6 +160,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
type: 'dashboard',
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -227,6 +231,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -46,6 +46,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(resp.body).to.eql({
|
||||
success: true,
|
||||
successCount: 0,
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -84,6 +85,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
{ ...visualization, overwrite: true },
|
||||
{ ...dashboard, overwrite: true },
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -125,6 +127,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
error: { type: 'unsupported_type' },
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -198,6 +201,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -215,7 +219,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({ success: true, successCount: 0 });
|
||||
expect(resp.body).to.eql({ success: true, successCount: 0, warnings: [] });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -253,6 +257,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
{ ...visualization, overwrite: true },
|
||||
{ ...dashboard, overwrite: true },
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -277,6 +282,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
success: true,
|
||||
successCount: 1,
|
||||
successResults: [{ ...visualization, overwrite: true }],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -328,6 +334,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
meta: { title: 'My favorite vis', icon: 'visualizeApp' },
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
await supertest
|
||||
|
|
|
@ -283,6 +283,22 @@ export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProv
|
|||
await testSubjects.click('confirmModalConfirmButton');
|
||||
await this.waitTableIsLoaded();
|
||||
}
|
||||
|
||||
async getImportWarnings() {
|
||||
const elements = await testSubjects.findAll('importSavedObjectsWarning');
|
||||
return Promise.all(
|
||||
elements.map(async (element) => {
|
||||
const message = await element
|
||||
.findByClassName('euiCallOutHeader__title')
|
||||
.then((titleEl) => titleEl.getVisibleText());
|
||||
const buttons = await element.findAllByClassName('euiButton');
|
||||
return {
|
||||
message,
|
||||
type: buttons.length ? 'action_required' : 'simple',
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new SavedObjectsPage();
|
||||
|
|
|
@ -29,6 +29,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
require.resolve('./test_suites/doc_views'),
|
||||
require.resolve('./test_suites/application_links'),
|
||||
require.resolve('./test_suites/data_plugin'),
|
||||
require.resolve('./test_suites/saved_objects_management'),
|
||||
],
|
||||
services: {
|
||||
...functionalConfig.get('services'),
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"id": "savedObjectHooks",
|
||||
"version": "0.0.1",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["saved_object_hooks"],
|
||||
"server": true,
|
||||
"ui": false
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "saved_object_hooks",
|
||||
"version": "1.0.0",
|
||||
"main": "target/test/plugin_functional/plugins/saved_object_hooks",
|
||||
"kibana": {
|
||||
"version": "kibana",
|
||||
"templateVersion": "1.0.0"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"kbn": "node ../../../../scripts/kbn.js",
|
||||
"build": "rm -rf './target' && ../../../../node_modules/.bin/tsc"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { SavedObjectHooksPlugin } from './plugin';
|
||||
|
||||
export const plugin = () => new SavedObjectHooksPlugin();
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Plugin, CoreSetup } from 'kibana/server';
|
||||
|
||||
export class SavedObjectHooksPlugin implements Plugin {
|
||||
public setup({ savedObjects }: CoreSetup, deps: {}) {
|
||||
savedObjects.registerType({
|
||||
name: 'test_import_warning_1',
|
||||
hidden: false,
|
||||
namespaceType: 'single',
|
||||
mappings: {
|
||||
properties: {
|
||||
title: { type: 'text' },
|
||||
},
|
||||
},
|
||||
management: {
|
||||
defaultSearchField: 'title',
|
||||
importableAndExportable: true,
|
||||
getTitle: (obj) => obj.attributes.title,
|
||||
onImport: (objects) => {
|
||||
return {
|
||||
warnings: [{ type: 'simple', message: 'warning for test_import_warning_1' }],
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
savedObjects.registerType({
|
||||
name: 'test_import_warning_2',
|
||||
hidden: false,
|
||||
namespaceType: 'single',
|
||||
mappings: {
|
||||
properties: {
|
||||
title: { type: 'text' },
|
||||
},
|
||||
},
|
||||
management: {
|
||||
defaultSearchField: 'title',
|
||||
importableAndExportable: true,
|
||||
getTitle: (obj) => obj.attributes.title,
|
||||
onImport: (objects) => {
|
||||
return {
|
||||
warnings: [
|
||||
{
|
||||
type: 'action_required',
|
||||
message: 'warning for test_import_warning_2',
|
||||
actionPath: '/some/url',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public start() {}
|
||||
public stop() {}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"public/**/*.ts",
|
||||
"public/**/*.tsx",
|
||||
"server/**/*.ts",
|
||||
"../../../../typings/**/*",
|
||||
],
|
||||
"exclude": [],
|
||||
"references": [
|
||||
{ "path": "../../../../src/core/tsconfig.json" }
|
||||
]
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
{"attributes":{"title": "Test Import warnings 1"},"id":"08ff1d6a-a2e7-11e7-bb30-2e3be9be6a73","migrationVersion":{"visualization":"7.0.0"},"references":[],"type":"test_import_warning_1","version":1}
|
||||
{"attributes":{"title": "Test Import warnings 2"},"id":"77bb1e6a-a2e7-11e7-bb30-2e3be9be6a73","migrationVersion":{"visualization":"7.0.0"},"references":[],"type":"test_import_warning_2","version":1}
|
|
@ -0,0 +1 @@
|
|||
{"attributes":{"title": "Test Import warnings 1"},"id":"08ff1d6a-a2e7-11e7-bb30-2e3be9be6a73","migrationVersion":{"visualization":"7.0.0"},"references":[],"type":"test_import_warning_1","version":1}
|
|
@ -0,0 +1 @@
|
|||
{"attributes":{"title": "Test Import warnings 2"},"id":"77bb1e6a-a2e7-11e7-bb30-2e3be9be6a73","migrationVersion":{"visualization":"7.0.0"},"references":[],"type":"test_import_warning_2","version":1}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import expect from '@kbn/expect';
|
||||
import { PluginFunctionalProviderContext } from '../../services';
|
||||
|
||||
export default function ({ getPageObjects }: PluginFunctionalProviderContext) {
|
||||
const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']);
|
||||
|
||||
describe('import warnings', () => {
|
||||
beforeEach(async () => {
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
});
|
||||
|
||||
it('should display simple warnings', async () => {
|
||||
await PageObjects.savedObjects.importFile(
|
||||
path.join(__dirname, 'exports', '_import_type_1.ndjson')
|
||||
);
|
||||
|
||||
await PageObjects.savedObjects.checkImportSucceeded();
|
||||
const warnings = await PageObjects.savedObjects.getImportWarnings();
|
||||
|
||||
expect(warnings).to.eql([
|
||||
{
|
||||
message: 'warning for test_import_warning_1',
|
||||
type: 'simple',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should display action warnings', async () => {
|
||||
await PageObjects.savedObjects.importFile(
|
||||
path.join(__dirname, 'exports', '_import_type_2.ndjson')
|
||||
);
|
||||
|
||||
await PageObjects.savedObjects.checkImportSucceeded();
|
||||
const warnings = await PageObjects.savedObjects.getImportWarnings();
|
||||
|
||||
expect(warnings).to.eql([
|
||||
{
|
||||
type: 'action_required',
|
||||
message: 'warning for test_import_warning_2',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should display warnings coming from multiple types', async () => {
|
||||
await PageObjects.savedObjects.importFile(
|
||||
path.join(__dirname, 'exports', '_import_both_types.ndjson')
|
||||
);
|
||||
|
||||
await PageObjects.savedObjects.checkImportSucceeded();
|
||||
const warnings = await PageObjects.savedObjects.getImportWarnings();
|
||||
|
||||
expect(warnings).to.eql([
|
||||
{
|
||||
message: 'warning for test_import_warning_1',
|
||||
type: 'simple',
|
||||
},
|
||||
{
|
||||
type: 'action_required',
|
||||
message: 'warning for test_import_warning_2',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PluginFunctionalProviderContext } from '../../services';
|
||||
|
||||
export default function ({ loadTestFile }: PluginFunctionalProviderContext) {
|
||||
describe('Saved Objects Management', function () {
|
||||
loadTestFile(require.resolve('./import_warnings'));
|
||||
});
|
||||
}
|
|
@ -177,6 +177,7 @@ describe('CopyToSpaceFlyout', () => {
|
|||
'space-1': {
|
||||
success: true,
|
||||
successCount: 3,
|
||||
warnings: [],
|
||||
},
|
||||
'space-2': {
|
||||
success: false,
|
||||
|
@ -195,6 +196,7 @@ describe('CopyToSpaceFlyout', () => {
|
|||
meta: {},
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -259,10 +261,12 @@ describe('CopyToSpaceFlyout', () => {
|
|||
'space-1': {
|
||||
success: true,
|
||||
successCount: 3,
|
||||
warnings: [],
|
||||
},
|
||||
'space-2': {
|
||||
success: true,
|
||||
successCount: 3,
|
||||
warnings: [],
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -319,6 +323,7 @@ describe('CopyToSpaceFlyout', () => {
|
|||
'space-1': {
|
||||
success: true,
|
||||
successCount: 5,
|
||||
warnings: [],
|
||||
},
|
||||
'space-2': {
|
||||
success: false,
|
||||
|
@ -359,6 +364,7 @@ describe('CopyToSpaceFlyout', () => {
|
|||
meta: {},
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -366,6 +372,7 @@ describe('CopyToSpaceFlyout', () => {
|
|||
'space-2': {
|
||||
success: true,
|
||||
successCount: 2,
|
||||
warnings: [],
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -490,6 +497,7 @@ describe('CopyToSpaceFlyout', () => {
|
|||
},
|
||||
],
|
||||
successResults: [{ type: savedObjectToCopy.type, id: savedObjectToCopy.id, meta: {} }],
|
||||
warnings: [],
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -571,6 +579,7 @@ describe('CopyToSpaceFlyout', () => {
|
|||
'space-1': {
|
||||
success: true,
|
||||
successCount: 3,
|
||||
warnings: [],
|
||||
},
|
||||
'space-2': {
|
||||
success: false,
|
||||
|
@ -583,6 +592,7 @@ describe('CopyToSpaceFlyout', () => {
|
|||
meta: {},
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -109,6 +109,7 @@ describe('copySavedObjectsToSpaces', () => {
|
|||
success: true,
|
||||
successCount: filteredObjects.length,
|
||||
successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess],
|
||||
warnings: [],
|
||||
};
|
||||
|
||||
return Promise.resolve(response);
|
||||
|
@ -201,6 +202,7 @@ describe('copySavedObjectsToSpaces', () => {
|
|||
success: true,
|
||||
successCount: filteredObjects.length,
|
||||
successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess],
|
||||
warnings: [],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -109,6 +109,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
|
|||
success: true,
|
||||
successCount: filteredObjects.length,
|
||||
successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess],
|
||||
warnings: [],
|
||||
};
|
||||
|
||||
return response;
|
||||
|
@ -209,6 +210,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
|
|||
success: true,
|
||||
successCount: filteredObjects.length,
|
||||
successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess],
|
||||
warnings: [],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -3184,8 +3184,6 @@
|
|||
"savedObjectsManagement.importSummary.createdOutcomeLabel": "作成済み",
|
||||
"savedObjectsManagement.importSummary.errorCountHeader": "{errorCount}件のエラー",
|
||||
"savedObjectsManagement.importSummary.errorOutcomeLabel": "{errorMessage}",
|
||||
"savedObjectsManagement.importSummary.headerLabelPlural": "{importCount}個のオブジェクトがインポートされました",
|
||||
"savedObjectsManagement.importSummary.headerLabelSingular": "1個のオブジェクトがインポートされました",
|
||||
"savedObjectsManagement.importSummary.overwrittenCountHeader": "{overwrittenCount}件上書きされました",
|
||||
"savedObjectsManagement.importSummary.overwrittenOutcomeLabel": "上書き",
|
||||
"savedObjectsManagement.indexPattern.confirmOverwriteButton": "上書き",
|
||||
|
|
|
@ -3188,8 +3188,6 @@
|
|||
"savedObjectsManagement.importSummary.createdOutcomeLabel": "已创建",
|
||||
"savedObjectsManagement.importSummary.errorCountHeader": "{errorCount} 个错误",
|
||||
"savedObjectsManagement.importSummary.errorOutcomeLabel": "{errorMessage}",
|
||||
"savedObjectsManagement.importSummary.headerLabelPlural": "{importCount} 个对象已导入",
|
||||
"savedObjectsManagement.importSummary.headerLabelSingular": "1 个对象已导入",
|
||||
"savedObjectsManagement.importSummary.overwrittenCountHeader": "{overwrittenCount} 个已覆盖",
|
||||
"savedObjectsManagement.importSummary.overwrittenOutcomeLabel": "已覆盖",
|
||||
"savedObjectsManagement.indexPattern.confirmOverwriteButton": "覆盖",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue