mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* SavedObjects tagging MVP (#79096) * create xpack plugin skeleton, start to implement management section * add tag creation modal * first implementation of the tags table * use InMemoryTable * add edit modal and delete action * update plugin list * add tag list, fix types * add capabilities check on client-side * add tag combo box component * add missing i18n keys * fix privilege FTR tests * add base structure for FTR tests * fix feature ftr test * use string literals for i18n * create savedObjectsTaggingOss plugin, move API types to oss plugin, start to wire to SO management page. * update plugin list * fix types * allow to use `_find` with multiple references * add FTR test for _find API on references fields * add _find integration tests * update generated doc * start to implement tag filtering on SO management section * update generated docs * wire tagging API to dashboard listing page * fix i18n namespace * fix type & tests * update dashboard listing snapshots * adapt FTR listingTable service to search for parsable queries * wite tagging API to visualize listing * update tagging plugin limits * add server-side and client-side validation for tag create/edit * rename title field to name * fix types * fix types bis * add removeReferencesTo API to SOR/SOC * update generated doc * add server-side unit test for `savedObjectsTagging` plugin * move tagging API types to its own file * add savedObjectsTaggingOss mock * add tags_cache tests * add tests for client-side tag client * extract uiApi to distinct files * various API improvements * add more tests * add link between tag and so management sections + add connection counts * add base functional test suite for tagging * add more FTR tests * improve feature control func test * update codeowners * update generated doc * fix access to proxy modal * adapt SO save modal to allow to add tag field * add SO decorator registry and tag implementation * add unit tests for SO tag decorator * add functional tests for visualize integration * add tag SO read permission for vis/dash feature * add RBAC api integ tests * add API integration tests * add test for getTagConnectionsUrl * add SOM test suite * add dashboard integration suite * remove test line * add missing unit tests * improve API types doc * fix create modal save button label * remove console.log * improve doc * self review * add refresh interval for tag cache * improve page object doc * minor cleanup * address review comments * small layout fixes * add initial focus * use lazy accessor for tag request handler context * adapt SOM export and export route to handle references * remove icon from feature config due to master changes * fix SO table tests * update generated docs * sort tags by name in filter dropdown and listing component * wire SO tagging to dashboard save modal * fix types * - add 'create tag' action in tag selector - add notifications on update/create/delete from management - delete modal wording * add description max length validation * remove real-time validation * fix i18n bundle id * update expected size of savedObjectsTagging plugin * use own useIfMounted * update limit again, contract components cannot be lazy loaded atm. * math is hard * remove single usage of lodash for bundle size * add async imports for create/edit modal * add FTR test for 'create tag' action from tag selector * allow 'create new' option to prepopulate name field * extract savedObjectToTag * add advancedSettings read user for security api_integ suite * add audit login for security client wrapper * use import type when possible * wire SO tagging to lens visualization * fix lens jest test * Fix `create tag` option being selected when closing the selector dropdown * add sorting to tag column from getTableColumnDef * address some of restrry comments * rename tag selector's setSelected option to onTagsSelected * fix audit logging even type for saved_object_remove_references * update plugin size limit to current size * adapt maxlength validation wording * remove selection column until we have batch action menu * remove connections link when user lack read privilege to savedObjectManagement * forbid registering multiple SO decorators with the same priority * add so decorator test * extract getTagFindReferences and create API mock * update audit-logging ascidoc * doc nit * throw conflict error if update returns any failure * use refresh=true as default * wording nits * export: rename `references` to `hasReference` * update generated doc * set description max length to 100 * do not initialize tag cache on anonymous pages * split fetchObjectsToExport into two distinct functions * change tag client `delete` call order * tsdoc nits * more nits * add README for oss plugin * add oss plugin start tests * SavedObject.find: rename `references` to `hasReference` * change section description label * remove url prefix constants * last nits and comments * update generated doc # Conflicts: # .github/CODEOWNERS # packages/kbn-optimizer/limits.yml # x-pack/scripts/functional_tests.js * fix FTR mapping files for 7.x
This commit is contained in:
parent
29f0ea2363
commit
d449d8cbdb
311 changed files with 15700 additions and 423 deletions
|
@ -160,6 +160,11 @@ Content is fetched from the remote (https://feeds.elastic.co and https://feeds-s
|
|||
|WARNING: Missing README.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/src/plugins/saved_objects_tagging_oss/README.md[savedObjectsTaggingOss]
|
||||
|Bridge plugin for consumption of the saved object tagging feature from
|
||||
oss plugins.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/src/plugins/security_oss/README.md[securityOss]
|
||||
|securityOss is responsible for educating users about Elastic's free security features,
|
||||
so they can properly protect the data within their clusters.
|
||||
|
@ -462,6 +467,10 @@ Elastic.
|
|||
|Welcome to the Kibana rollup plugin! This plugin provides Kibana support for Elasticsearch's rollup feature. Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/saved_objects_tagging/README.md[savedObjectsTagging]
|
||||
|Add tagging capability to saved objects
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/searchprofiler/README.md[searchprofiler]
|
||||
|The search profiler consumes the Profile API
|
||||
by sending a search API with profile: true enabled in the request body. The response contains
|
||||
|
|
|
@ -98,6 +98,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [SavedObjectsBulkUpdateOptions](./kibana-plugin-core-public.savedobjectsbulkupdateoptions.md) | |
|
||||
| [SavedObjectsCreateOptions](./kibana-plugin-core-public.savedobjectscreateoptions.md) | |
|
||||
| [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. |
|
||||
| [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. |
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
## SavedObjectsFindOptions.defaultSearchOperator property
|
||||
|
||||
The search operator to use with the provided filter. Defaults to `OR`
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
|
|
|
@ -4,11 +4,10 @@
|
|||
|
||||
## SavedObjectsFindOptions.hasReference property
|
||||
|
||||
Search for documents having a reference to the specified objects. Use `hasReferenceOperator` to specify the operator to use when searching for multiple references.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
hasReference?: {
|
||||
type: string;
|
||||
id: string;
|
||||
};
|
||||
hasReference?: SavedObjectsFindOptionsReference | SavedObjectsFindOptionsReference[];
|
||||
```
|
||||
|
|
|
@ -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) > [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) > [hasReferenceOperator](./kibana-plugin-core-public.savedobjectsfindoptions.hasreferenceoperator.md)
|
||||
|
||||
## SavedObjectsFindOptions.hasReferenceOperator property
|
||||
|
||||
The operator to use when searching by multiple references using the `hasReference` option. Defaults to `OR`
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
hasReferenceOperator?: 'AND' | 'OR';
|
||||
```
|
|
@ -15,10 +15,11 @@ export interface SavedObjectsFindOptions
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [defaultSearchOperator](./kibana-plugin-core-public.savedobjectsfindoptions.defaultsearchoperator.md) | <code>'AND' | 'OR'</code> | |
|
||||
| [defaultSearchOperator](./kibana-plugin-core-public.savedobjectsfindoptions.defaultsearchoperator.md) | <code>'AND' | 'OR'</code> | The search operator to use with the provided filter. Defaults to <code>OR</code> |
|
||||
| [fields](./kibana-plugin-core-public.savedobjectsfindoptions.fields.md) | <code>string[]</code> | An array of fields to include in the results |
|
||||
| [filter](./kibana-plugin-core-public.savedobjectsfindoptions.filter.md) | <code>string | KueryNode</code> | |
|
||||
| [hasReference](./kibana-plugin-core-public.savedobjectsfindoptions.hasreference.md) | <code>{</code><br/><code> type: string;</code><br/><code> id: string;</code><br/><code> }</code> | |
|
||||
| [hasReference](./kibana-plugin-core-public.savedobjectsfindoptions.hasreference.md) | <code>SavedObjectsFindOptionsReference | SavedObjectsFindOptionsReference[]</code> | Search for documents having a reference to the specified objects. Use <code>hasReferenceOperator</code> to specify the operator to use when searching for multiple references. |
|
||||
| [hasReferenceOperator](./kibana-plugin-core-public.savedobjectsfindoptions.hasreferenceoperator.md) | <code>'AND' | 'OR'</code> | The operator to use when searching by multiple references using the <code>hasReference</code> option. Defaults to <code>OR</code> |
|
||||
| [namespaces](./kibana-plugin-core-public.savedobjectsfindoptions.namespaces.md) | <code>string[]</code> | |
|
||||
| [page](./kibana-plugin-core-public.savedobjectsfindoptions.page.md) | <code>number</code> | |
|
||||
| [perPage](./kibana-plugin-core-public.savedobjectsfindoptions.perpage.md) | <code>number</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) > [SavedObjectsFindOptionsReference](./kibana-plugin-core-public.savedobjectsfindoptionsreference.md) > [id](./kibana-plugin-core-public.savedobjectsfindoptionsreference.id.md)
|
||||
|
||||
## SavedObjectsFindOptionsReference.id property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
id: string;
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsFindOptionsReference](./kibana-plugin-core-public.savedobjectsfindoptionsreference.md)
|
||||
|
||||
## SavedObjectsFindOptionsReference interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SavedObjectsFindOptionsReference
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [id](./kibana-plugin-core-public.savedobjectsfindoptionsreference.id.md) | <code>string</code> | |
|
||||
| [type](./kibana-plugin-core-public.savedobjectsfindoptionsreference.type.md) | <code>string</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) > [SavedObjectsFindOptionsReference](./kibana-plugin-core-public.savedobjectsfindoptionsreference.md) > [type](./kibana-plugin-core-public.savedobjectsfindoptionsreference.type.md)
|
||||
|
||||
## SavedObjectsFindOptionsReference.type property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
type: string;
|
||||
```
|
|
@ -9,14 +9,14 @@ Generates sorted saved object stream to be used for export. See the [options](./
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare function exportSavedObjectsToStream({ types, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, }: SavedObjectsExportOptions): Promise<import("stream").Readable>;
|
||||
export declare function exportSavedObjectsToStream({ types, hasReference, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, }: SavedObjectsExportOptions): Promise<import("stream").Readable>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| { types, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, } | <code>SavedObjectsExportOptions</code> | |
|
||||
| { types, hasReference, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, } | <code>SavedObjectsExportOptions</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
|
||||
| Function | Description |
|
||||
| --- | --- |
|
||||
| [exportSavedObjectsToStream({ types, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, })](./kibana-plugin-core-server.exportsavedobjectstostream.md) | Generates sorted saved object stream to be used for export. See the [options](./kibana-plugin-core-server.savedobjectsexportoptions.md) for more detailed information. |
|
||||
| [exportSavedObjectsToStream({ types, hasReference, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, })](./kibana-plugin-core-server.exportsavedobjectstostream.md) | Generates sorted saved object stream to be used for export. See the [options](./kibana-plugin-core-server.savedobjectsexportoptions.md) for more detailed information. |
|
||||
| [importSavedObjectsFromStream({ readStream, objectLimit, overwrite, createNewCopies, savedObjectsClient, typeRegistry, namespace, })](./kibana-plugin-core-server.importsavedobjectsfromstream.md) | Import saved objects from given stream. See the [options](./kibana-plugin-core-server.savedobjectsimportoptions.md) for more detailed information. |
|
||||
| [resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, createNewCopies, })](./kibana-plugin-core-server.resolvesavedobjectsimporterrors.md) | Resolve and return saved object import errors. See the [options](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. |
|
||||
|
||||
|
@ -163,6 +163,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) | Options controlling the export operation. |
|
||||
| [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry |
|
||||
| [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) | |
|
||||
| [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) | |
|
||||
| [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. |
|
||||
|
@ -180,6 +181,8 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [SavedObjectsMigrationLogger](./kibana-plugin-core-server.savedobjectsmigrationlogger.md) | |
|
||||
| [SavedObjectsMigrationVersion](./kibana-plugin-core-server.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
|
||||
| [SavedObjectsRawDoc](./kibana-plugin-core-server.savedobjectsrawdoc.md) | A raw document as represented directly in the saved object index. |
|
||||
| [SavedObjectsRemoveReferencesToOptions](./kibana-plugin-core-server.savedobjectsremovereferencestooptions.md) | |
|
||||
| [SavedObjectsRemoveReferencesToResponse](./kibana-plugin-core-server.savedobjectsremovereferencestoresponse.md) | |
|
||||
| [SavedObjectsRepositoryFactory](./kibana-plugin-core-server.savedobjectsrepositoryfactory.md) | Factory provided when invoking a [client factory provider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) See [SavedObjectsServiceSetup.setClientFactoryProvider](./kibana-plugin-core-server.savedobjectsservicesetup.setclientfactoryprovider.md) |
|
||||
| [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) | Options to control the "resolve import" operation. |
|
||||
| [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | Saved Objects is Kibana's data persistence mechanism allowing plugins to use Elasticsearch for storing and querying state. The SavedObjectsServiceSetup API exposes methods for registering Saved Object types, creating and registering Saved Object client wrappers and factories. |
|
||||
|
|
|
@ -35,5 +35,6 @@ The constructor for this class is marked as internal. Third-party code should no
|
|||
| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md) | | Removes namespaces from a SavedObject |
|
||||
| [find(options)](./kibana-plugin-core-server.savedobjectsclient.find.md) | | Find all SavedObjects matching the search query |
|
||||
| [get(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.get.md) | | Retrieves a single object |
|
||||
| [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.removereferencesto.md) | | Updates all objects containing a reference to the given {<!-- -->type, id<!-- -->} tuple to remove the said reference. |
|
||||
| [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.update.md) | | Updates an SavedObject |
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [removeReferencesTo](./kibana-plugin-core-server.savedobjectsclient.removereferencesto.md)
|
||||
|
||||
## SavedObjectsClient.removeReferencesTo() method
|
||||
|
||||
Updates all objects containing a reference to the given {<!-- -->type, id<!-- -->} tuple to remove the said reference.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise<SavedObjectsRemoveReferencesToResponse>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| type | <code>string</code> | |
|
||||
| id | <code>string</code> | |
|
||||
| options | <code>SavedObjectsRemoveReferencesToOptions</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`Promise<SavedObjectsRemoveReferencesToResponse>`
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
static createConflictError(type: string, id: string): DecoratedError;
|
||||
static createConflictError(type: string, id: string, reason?: string): DecoratedError;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
@ -16,6 +16,7 @@ static createConflictError(type: string, id: string): DecoratedError;
|
|||
| --- | --- | --- |
|
||||
| type | <code>string</code> | |
|
||||
| id | <code>string</code> | |
|
||||
| reason | <code>string</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ export declare class SavedObjectsErrorHelpers
|
|||
| Method | Modifiers | Description |
|
||||
| --- | --- | --- |
|
||||
| [createBadRequestError(reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createbadrequesterror.md) | <code>static</code> | |
|
||||
| [createConflictError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md) | <code>static</code> | |
|
||||
| [createConflictError(type, id, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md) | <code>static</code> | |
|
||||
| [createGenericNotFoundError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md) | <code>static</code> | |
|
||||
| [createInvalidVersionError(versionInput)](./kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md) | <code>static</code> | |
|
||||
| [createTooManyRequestsError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.createtoomanyrequestserror.md) | <code>static</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) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [hasReference](./kibana-plugin-core-server.savedobjectsexportoptions.hasreference.md)
|
||||
|
||||
## SavedObjectsExportOptions.hasReference property
|
||||
|
||||
optional array of references to search object for when exporting by types
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
hasReference?: SavedObjectsFindOptionsReference[];
|
||||
```
|
|
@ -18,6 +18,7 @@ export interface SavedObjectsExportOptions
|
|||
| --- | --- | --- |
|
||||
| [excludeExportDetails](./kibana-plugin-core-server.savedobjectsexportoptions.excludeexportdetails.md) | <code>boolean</code> | flag to not append [export details](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) to the end of the export stream. |
|
||||
| [exportSizeLimit](./kibana-plugin-core-server.savedobjectsexportoptions.exportsizelimit.md) | <code>number</code> | the maximum number of objects to export. |
|
||||
| [hasReference](./kibana-plugin-core-server.savedobjectsexportoptions.hasreference.md) | <code>SavedObjectsFindOptionsReference[]</code> | optional array of references to search object for when exporting by types |
|
||||
| [includeReferencesDeep](./kibana-plugin-core-server.savedobjectsexportoptions.includereferencesdeep.md) | <code>boolean</code> | flag to also include all related saved objects in the export stream. |
|
||||
| [namespace](./kibana-plugin-core-server.savedobjectsexportoptions.namespace.md) | <code>string</code> | optional namespace to override the namespace used by the savedObjectsClient. |
|
||||
| [objects](./kibana-plugin-core-server.savedobjectsexportoptions.objects.md) | <code>Array<{</code><br/><code> id: string;</code><br/><code> type: string;</code><br/><code> }></code> | optional array of objects to export. |
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
## SavedObjectsFindOptions.defaultSearchOperator property
|
||||
|
||||
The search operator to use with the provided filter. Defaults to `OR`
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
|
|
|
@ -4,11 +4,10 @@
|
|||
|
||||
## SavedObjectsFindOptions.hasReference property
|
||||
|
||||
Search for documents having a reference to the specified objects. Use `hasReferenceOperator` to specify the operator to use when searching for multiple references.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
hasReference?: {
|
||||
type: string;
|
||||
id: string;
|
||||
};
|
||||
hasReference?: SavedObjectsFindOptionsReference | SavedObjectsFindOptionsReference[];
|
||||
```
|
||||
|
|
|
@ -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) > [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) > [hasReferenceOperator](./kibana-plugin-core-server.savedobjectsfindoptions.hasreferenceoperator.md)
|
||||
|
||||
## SavedObjectsFindOptions.hasReferenceOperator property
|
||||
|
||||
The operator to use when searching by multiple references using the `hasReference` option. Defaults to `OR`
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
hasReferenceOperator?: 'AND' | 'OR';
|
||||
```
|
|
@ -15,10 +15,11 @@ export interface SavedObjectsFindOptions
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [defaultSearchOperator](./kibana-plugin-core-server.savedobjectsfindoptions.defaultsearchoperator.md) | <code>'AND' | 'OR'</code> | |
|
||||
| [defaultSearchOperator](./kibana-plugin-core-server.savedobjectsfindoptions.defaultsearchoperator.md) | <code>'AND' | 'OR'</code> | The search operator to use with the provided filter. Defaults to <code>OR</code> |
|
||||
| [fields](./kibana-plugin-core-server.savedobjectsfindoptions.fields.md) | <code>string[]</code> | An array of fields to include in the results |
|
||||
| [filter](./kibana-plugin-core-server.savedobjectsfindoptions.filter.md) | <code>string | KueryNode</code> | |
|
||||
| [hasReference](./kibana-plugin-core-server.savedobjectsfindoptions.hasreference.md) | <code>{</code><br/><code> type: string;</code><br/><code> id: string;</code><br/><code> }</code> | |
|
||||
| [hasReference](./kibana-plugin-core-server.savedobjectsfindoptions.hasreference.md) | <code>SavedObjectsFindOptionsReference | SavedObjectsFindOptionsReference[]</code> | Search for documents having a reference to the specified objects. Use <code>hasReferenceOperator</code> to specify the operator to use when searching for multiple references. |
|
||||
| [hasReferenceOperator](./kibana-plugin-core-server.savedobjectsfindoptions.hasreferenceoperator.md) | <code>'AND' | 'OR'</code> | The operator to use when searching by multiple references using the <code>hasReference</code> option. Defaults to <code>OR</code> |
|
||||
| [namespaces](./kibana-plugin-core-server.savedobjectsfindoptions.namespaces.md) | <code>string[]</code> | |
|
||||
| [page](./kibana-plugin-core-server.savedobjectsfindoptions.page.md) | <code>number</code> | |
|
||||
| [perPage](./kibana-plugin-core-server.savedobjectsfindoptions.perpage.md) | <code>number</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) > [SavedObjectsFindOptionsReference](./kibana-plugin-core-server.savedobjectsfindoptionsreference.md) > [id](./kibana-plugin-core-server.savedobjectsfindoptionsreference.id.md)
|
||||
|
||||
## SavedObjectsFindOptionsReference.id property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
id: string;
|
||||
```
|
|
@ -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) > [SavedObjectsFindOptionsReference](./kibana-plugin-core-server.savedobjectsfindoptionsreference.md)
|
||||
|
||||
## SavedObjectsFindOptionsReference interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SavedObjectsFindOptionsReference
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [id](./kibana-plugin-core-server.savedobjectsfindoptionsreference.id.md) | <code>string</code> | |
|
||||
| [type](./kibana-plugin-core-server.savedobjectsfindoptionsreference.type.md) | <code>string</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) > [SavedObjectsFindOptionsReference](./kibana-plugin-core-server.savedobjectsfindoptionsreference.md) > [type](./kibana-plugin-core-server.savedobjectsfindoptionsreference.type.md)
|
||||
|
||||
## SavedObjectsFindOptionsReference.type property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
type: string;
|
||||
```
|
|
@ -0,0 +1,19 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRemoveReferencesToOptions](./kibana-plugin-core-server.savedobjectsremovereferencestooptions.md)
|
||||
|
||||
## SavedObjectsRemoveReferencesToOptions interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SavedObjectsRemoveReferencesToOptions extends SavedObjectsBaseOptions
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [refresh](./kibana-plugin-core-server.savedobjectsremovereferencestooptions.refresh.md) | <code>boolean</code> | The Elasticsearch Refresh setting for this operation. Defaults to <code>true</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) > [SavedObjectsRemoveReferencesToOptions](./kibana-plugin-core-server.savedobjectsremovereferencestooptions.md) > [refresh](./kibana-plugin-core-server.savedobjectsremovereferencestooptions.refresh.md)
|
||||
|
||||
## SavedObjectsRemoveReferencesToOptions.refresh property
|
||||
|
||||
The Elasticsearch Refresh setting for this operation. Defaults to `true`
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
refresh?: boolean;
|
||||
```
|
|
@ -0,0 +1,19 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRemoveReferencesToResponse](./kibana-plugin-core-server.savedobjectsremovereferencestoresponse.md)
|
||||
|
||||
## SavedObjectsRemoveReferencesToResponse interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SavedObjectsRemoveReferencesToResponse extends SavedObjectsBaseOptions
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [updated](./kibana-plugin-core-server.savedobjectsremovereferencestoresponse.updated.md) | <code>number</code> | The number of objects that have been updated by this operation |
|
||||
|
|
@ -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) > [SavedObjectsRemoveReferencesToResponse](./kibana-plugin-core-server.savedobjectsremovereferencestoresponse.md) > [updated](./kibana-plugin-core-server.savedobjectsremovereferencestoresponse.updated.md)
|
||||
|
||||
## SavedObjectsRemoveReferencesToResponse.updated property
|
||||
|
||||
The number of objects that have been updated by this operation
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
updated: number;
|
||||
```
|
|
@ -27,5 +27,6 @@ export declare class SavedObjectsRepository
|
|||
| [find(options)](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | |
|
||||
| [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object |
|
||||
| [incrementCounter(type, id, counterFieldName, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increases a counter field by one. Creates the document if one doesn't exist for the given id. |
|
||||
| [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.removereferencesto.md) | | Updates all objects containing a reference to the given {<!-- -->type, id<!-- -->} tuple to remove the said reference. |
|
||||
| [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object |
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [removeReferencesTo](./kibana-plugin-core-server.savedobjectsrepository.removereferencesto.md)
|
||||
|
||||
## SavedObjectsRepository.removeReferencesTo() method
|
||||
|
||||
Updates all objects containing a reference to the given {<!-- -->type, id<!-- -->} tuple to remove the said reference.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise<SavedObjectsRemoveReferencesToResponse>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| type | <code>string</code> | |
|
||||
| id | <code>string</code> | |
|
||||
| options | <code>SavedObjectsRemoveReferencesToOptions</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`Promise<SavedObjectsRemoveReferencesToResponse>`
|
||||
|
||||
## Remarks
|
||||
|
||||
Will throw a conflict error if the `update_by_query` operation returns any failure. In that case some references might have been removed, and some were not. It is the caller's responsibility to handle and fix this situation if it was to happen.
|
||||
|
|
@ -12,7 +12,7 @@ start(core: CoreStart): {
|
|||
fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise<import("../common").FieldFormatsRegistry>;
|
||||
};
|
||||
indexPatterns: {
|
||||
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "update" | "find" | "get" | "delete" | "errors" | "create" | "bulkCreate" | "checkConflicts" | "bulkGet" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate">) => Promise<import("../public").IndexPatternsService>;
|
||||
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "update" | "find" | "get" | "delete" | "errors" | "create" | "bulkCreate" | "checkConflicts" | "bulkGet" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo">) => Promise<import("../public").IndexPatternsService>;
|
||||
};
|
||||
search: ISearchStart<import("./search").IEsSearchRequest, import("./search").IEsSearchResponse<any>>;
|
||||
};
|
||||
|
@ -31,7 +31,7 @@ start(core: CoreStart): {
|
|||
fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise<import("../common").FieldFormatsRegistry>;
|
||||
};
|
||||
indexPatterns: {
|
||||
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "update" | "find" | "get" | "delete" | "errors" | "create" | "bulkCreate" | "checkConflicts" | "bulkGet" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate">) => Promise<import("../public").IndexPatternsService>;
|
||||
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "update" | "find" | "get" | "delete" | "errors" | "create" | "bulkCreate" | "checkConflicts" | "bulkGet" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo">) => Promise<import("../public").IndexPatternsService>;
|
||||
};
|
||||
search: ISearchStart<import("./search").IEsSearchRequest, import("./search").IEsSearchResponse<any>>;
|
||||
}`
|
||||
|
|
|
@ -104,6 +104,11 @@ Refer to the corresponding {es} logs for potential write errors.
|
|||
| `unknown` | User is removing a saved object from other spaces.
|
||||
| `failure` | User is not authorized to remove a saved object from other spaces.
|
||||
|
||||
.2+| `saved_object_remove_references`
|
||||
| `unknown` | User is removing references to a saved object.
|
||||
| `failure` | User is not authorized to remove references to a saved object.
|
||||
|
||||
|
||||
3+a|
|
||||
====== Type: deletion
|
||||
|
||||
|
|
|
@ -90,6 +90,8 @@ async function fetchKibanaIndices(client: Client) {
|
|||
return kibanaIndices.map((x: { index: string }) => x.index).filter(isKibanaIndex);
|
||||
}
|
||||
|
||||
const delay = (delayInMs: number) => new Promise((resolve) => setTimeout(resolve, delayInMs));
|
||||
|
||||
export async function cleanKibanaIndices({
|
||||
client,
|
||||
stats,
|
||||
|
@ -133,6 +135,7 @@ export async function cleanKibanaIndices({
|
|||
resp.deleted,
|
||||
resp.total
|
||||
);
|
||||
await delay(200);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -63,9 +63,11 @@ pageLoadAssetSize:
|
|||
remoteClusters: 51327
|
||||
reporting: 183418
|
||||
rollup: 97204
|
||||
savedObjects: 108662
|
||||
savedObjectsManagement: 100503
|
||||
searchprofiler: 67224
|
||||
savedObjects: 108518
|
||||
savedObjectsManagement: 101836
|
||||
savedObjectsTagging: 59482
|
||||
savedObjectsTaggingOss: 20590
|
||||
searchprofiler: 67080
|
||||
security: 189428
|
||||
securityOss: 30806
|
||||
securitySolution: 283440
|
||||
|
|
|
@ -132,6 +132,7 @@ export {
|
|||
SavedObjectReference,
|
||||
SavedObjectsBaseOptions,
|
||||
SavedObjectsFindOptions,
|
||||
SavedObjectsFindOptionsReference,
|
||||
SavedObjectsMigrationVersion,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsClient,
|
||||
|
|
|
@ -97,7 +97,7 @@ function createCoreStartMock({ basePath = '' } = {}) {
|
|||
return mock;
|
||||
}
|
||||
|
||||
function pluginInitializerContextMock() {
|
||||
function pluginInitializerContextMock(config: any = {}) {
|
||||
const mock: PluginInitializerContext = {
|
||||
opaqueId: Symbol(),
|
||||
env: {
|
||||
|
@ -115,7 +115,7 @@ function pluginInitializerContextMock() {
|
|||
},
|
||||
},
|
||||
config: {
|
||||
get: <T>() => ({} as T),
|
||||
get: <T>() => config as T,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1056,18 +1056,14 @@ export interface SavedObjectsCreateOptions {
|
|||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsFindOptions {
|
||||
// (undocumented)
|
||||
defaultSearchOperator?: 'AND' | 'OR';
|
||||
fields?: string[];
|
||||
// Warning: (ae-forgotten-export) The symbol "KueryNode" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
filter?: string | KueryNode;
|
||||
// (undocumented)
|
||||
hasReference?: {
|
||||
type: string;
|
||||
id: string;
|
||||
};
|
||||
hasReference?: SavedObjectsFindOptionsReference | SavedObjectsFindOptionsReference[];
|
||||
hasReferenceOperator?: 'AND' | 'OR';
|
||||
// (undocumented)
|
||||
namespaces?: string[];
|
||||
// (undocumented)
|
||||
|
@ -1087,6 +1083,14 @@ export interface SavedObjectsFindOptions {
|
|||
typeToNamespacesMap?: Map<string, string[] | undefined>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsFindOptionsReference {
|
||||
// (undocumented)
|
||||
id: string;
|
||||
// (undocumented)
|
||||
type: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface SavedObjectsFindResponsePublic<T = unknown> extends SavedObjectsBatchResponse<T> {
|
||||
// (undocumented)
|
||||
|
|
|
@ -34,6 +34,7 @@ export { SavedObjectsStart, SavedObjectsService } from './saved_objects_service'
|
|||
export {
|
||||
SavedObjectsBaseOptions,
|
||||
SavedObjectsFindOptions,
|
||||
SavedObjectsFindOptionsReference,
|
||||
SavedObjectsMigrationVersion,
|
||||
SavedObjectsImportResponse,
|
||||
SavedObjectsImportSuccess,
|
||||
|
|
|
@ -407,10 +407,7 @@ describe('SavedObjectsClient', () => {
|
|||
"fields": Array [
|
||||
"title",
|
||||
],
|
||||
"has_reference": Object {
|
||||
"id": "1",
|
||||
"type": "reference",
|
||||
},
|
||||
"has_reference": "{\\"id\\":\\"1\\",\\"type\\":\\"reference\\"}",
|
||||
"page": 10,
|
||||
"per_page": 100,
|
||||
"search": "what is the meaning of life?|life",
|
||||
|
|
|
@ -305,6 +305,7 @@ export class SavedObjectsClient {
|
|||
defaultSearchOperator: 'default_search_operator',
|
||||
fields: 'fields',
|
||||
hasReference: 'has_reference',
|
||||
hasReferenceOperator: 'has_reference_operator',
|
||||
page: 'page',
|
||||
perPage: 'per_page',
|
||||
search: 'search',
|
||||
|
@ -317,7 +318,16 @@ export class SavedObjectsClient {
|
|||
};
|
||||
|
||||
const renamedQuery = renameKeys<SavedObjectsFindOptions, any>(renameMap, options);
|
||||
const query = pick.apply(null, [renamedQuery, ...Object.values<string>(renameMap)]);
|
||||
const query = pick.apply(null, [renamedQuery, ...Object.values<string>(renameMap)]) as Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
|
||||
// `has_references` is a structured object. we need to stringify it before sending it, as `fetch`
|
||||
// is not doing it implicitly.
|
||||
if (query.has_reference) {
|
||||
query.has_reference = JSON.stringify(query.has_reference);
|
||||
}
|
||||
|
||||
const request: ReturnType<SavedObjectsApi['find']> = this.savedObjectsFetch(path, {
|
||||
method: 'GET',
|
||||
|
|
|
@ -284,6 +284,8 @@ export {
|
|||
SavedObjectsAddToNamespacesResponse,
|
||||
SavedObjectsDeleteFromNamespacesOptions,
|
||||
SavedObjectsDeleteFromNamespacesResponse,
|
||||
SavedObjectsRemoveReferencesToOptions,
|
||||
SavedObjectsRemoveReferencesToResponse,
|
||||
SavedObjectsServiceStart,
|
||||
SavedObjectsServiceSetup,
|
||||
SavedObjectStatusMeta,
|
||||
|
@ -347,6 +349,7 @@ export {
|
|||
MutatingOperationRefreshSetting,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsFindOptions,
|
||||
SavedObjectsFindOptionsReference,
|
||||
SavedObjectsMigrationVersion,
|
||||
} from './types';
|
||||
|
||||
|
|
|
@ -107,6 +107,8 @@ describe('getSortedObjectsForExport()', () => {
|
|||
"calls": Array [
|
||||
Array [
|
||||
Object {
|
||||
"hasReference": undefined,
|
||||
"hasReferenceOperator": undefined,
|
||||
"namespaces": undefined,
|
||||
"perPage": 500,
|
||||
"search": undefined,
|
||||
|
@ -197,6 +199,8 @@ describe('getSortedObjectsForExport()', () => {
|
|||
"calls": Array [
|
||||
Array [
|
||||
Object {
|
||||
"hasReference": undefined,
|
||||
"hasReferenceOperator": undefined,
|
||||
"namespaces": undefined,
|
||||
"perPage": 500,
|
||||
"search": undefined,
|
||||
|
@ -347,6 +351,8 @@ describe('getSortedObjectsForExport()', () => {
|
|||
"calls": Array [
|
||||
Array [
|
||||
Object {
|
||||
"hasReference": undefined,
|
||||
"hasReferenceOperator": undefined,
|
||||
"namespaces": undefined,
|
||||
"perPage": 500,
|
||||
"search": "foo",
|
||||
|
@ -367,6 +373,94 @@ describe('getSortedObjectsForExport()', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
test('exports selected types with references when present', async () => {
|
||||
savedObjectsClient.find.mockResolvedValueOnce({
|
||||
total: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '2',
|
||||
type: 'search',
|
||||
attributes: {},
|
||||
score: 1,
|
||||
references: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
per_page: 1,
|
||||
page: 0,
|
||||
});
|
||||
const exportStream = await exportSavedObjectsToStream({
|
||||
savedObjectsClient,
|
||||
exportSizeLimit: 500,
|
||||
types: ['index-pattern', 'search'],
|
||||
hasReference: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const response = await readStreamToCompletion(exportStream);
|
||||
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "name",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"exportedCount": 1,
|
||||
"missingRefCount": 0,
|
||||
"missingReferences": Array [],
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(savedObjectsClient.find).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Object {
|
||||
"hasReference": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"hasReferenceOperator": "OR",
|
||||
"namespaces": undefined,
|
||||
"perPage": 500,
|
||||
"search": undefined,
|
||||
"type": Array [
|
||||
"index-pattern",
|
||||
"search",
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('exports from the provided namespace when present', async () => {
|
||||
savedObjectsClient.find.mockResolvedValueOnce({
|
||||
total: 2,
|
||||
|
@ -436,6 +530,8 @@ describe('getSortedObjectsForExport()', () => {
|
|||
"calls": Array [
|
||||
Array [
|
||||
Object {
|
||||
"hasReference": undefined,
|
||||
"hasReferenceOperator": undefined,
|
||||
"namespaces": Array [
|
||||
"foo",
|
||||
],
|
||||
|
@ -843,4 +939,17 @@ describe('getSortedObjectsForExport()', () => {
|
|||
`"Can't specify both \\"search\\" and \\"objects\\" properties when exporting"`
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects when both objects and references are passed in', () => {
|
||||
const exportOpts = {
|
||||
exportSizeLimit: 1,
|
||||
savedObjectsClient,
|
||||
objects: [{ type: 'index-pattern', id: '1' }],
|
||||
hasReference: [{ type: 'index-pattern', id: '1' }],
|
||||
};
|
||||
|
||||
expect(exportSavedObjectsToStream(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Can't specify both \\"references\\" and \\"objects\\" properties when exporting"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,7 +19,11 @@
|
|||
|
||||
import Boom from '@hapi/boom';
|
||||
import { createListStream } from '../../utils/streams';
|
||||
import { SavedObjectsClientContract, SavedObject } from '../types';
|
||||
import {
|
||||
SavedObjectsClientContract,
|
||||
SavedObject,
|
||||
SavedObjectsFindOptionsReference,
|
||||
} from '../types';
|
||||
import { fetchNestedDependencies } from './inject_nested_depdendencies';
|
||||
import { sortObjects } from './sort_objects';
|
||||
|
||||
|
@ -30,6 +34,8 @@ import { sortObjects } from './sort_objects';
|
|||
export interface SavedObjectsExportOptions {
|
||||
/** optional array of saved object types. */
|
||||
types?: string[];
|
||||
/** optional array of references to search object for when exporting by types */
|
||||
hasReference?: SavedObjectsFindOptionsReference[];
|
||||
/** optional array of objects to export. */
|
||||
objects?: Array<{
|
||||
/** the saved object id. */
|
||||
|
@ -51,6 +57,43 @@ export interface SavedObjectsExportOptions {
|
|||
namespace?: string;
|
||||
}
|
||||
|
||||
interface SavedObjectsFetchByTypeOptions {
|
||||
/** array of saved object types. */
|
||||
types: string[];
|
||||
/** optional array of references to search object for when exporting by types */
|
||||
hasReference?: SavedObjectsFindOptionsReference[];
|
||||
/** optional query string to filter exported objects. */
|
||||
search?: string;
|
||||
/** an instance of the SavedObjectsClient. */
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
/** the maximum number of objects to export. */
|
||||
exportSizeLimit: number;
|
||||
/** optional namespace to override the namespace used by the savedObjectsClient. */
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
interface SavedObjectsFetchByObjectOptions {
|
||||
/** optional array of objects to export. */
|
||||
objects: Array<{
|
||||
/** the saved object id. */
|
||||
id: string;
|
||||
/** the saved object type. */
|
||||
type: string;
|
||||
}>;
|
||||
/** an instance of the SavedObjectsClient. */
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
/** the maximum number of objects to export. */
|
||||
exportSizeLimit: number;
|
||||
/** optional namespace to override the namespace used by the savedObjectsClient. */
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
const isFetchByTypeOptions = (
|
||||
options: SavedObjectsFetchByTypeOptions | SavedObjectsFetchByObjectOptions
|
||||
): options is SavedObjectsFetchByTypeOptions => {
|
||||
return Boolean((options as SavedObjectsFetchByTypeOptions).types);
|
||||
};
|
||||
|
||||
/**
|
||||
* Structure of the export result details entry
|
||||
* @public
|
||||
|
@ -69,21 +112,67 @@ export interface SavedObjectsExportResultDetails {
|
|||
}>;
|
||||
}
|
||||
|
||||
async function fetchObjectsToExport({
|
||||
objects,
|
||||
async function fetchByType({
|
||||
types,
|
||||
search,
|
||||
exportSizeLimit,
|
||||
savedObjectsClient,
|
||||
namespace,
|
||||
}: {
|
||||
objects?: SavedObjectsExportOptions['objects'];
|
||||
types?: string[];
|
||||
search?: string;
|
||||
exportSizeLimit: number;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
namespace?: string;
|
||||
}) {
|
||||
exportSizeLimit,
|
||||
hasReference,
|
||||
search,
|
||||
savedObjectsClient,
|
||||
}: SavedObjectsFetchByTypeOptions) {
|
||||
const findResponse = await savedObjectsClient.find({
|
||||
type: types,
|
||||
hasReference,
|
||||
hasReferenceOperator: hasReference ? 'OR' : undefined,
|
||||
search,
|
||||
perPage: exportSizeLimit,
|
||||
namespaces: namespace ? [namespace] : undefined,
|
||||
});
|
||||
if (findResponse.total > exportSizeLimit) {
|
||||
throw Boom.badRequest(`Can't export more than ${exportSizeLimit} objects`);
|
||||
}
|
||||
|
||||
// sorts server-side by _id, since it's only available in fielddata
|
||||
return (
|
||||
findResponse.saved_objects
|
||||
// exclude the find-specific `score` property from the exported objects
|
||||
.map(({ score, ...obj }) => obj)
|
||||
.sort((a: SavedObject, b: SavedObject) => (a.id > b.id ? 1 : -1))
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchByObjects({
|
||||
objects,
|
||||
exportSizeLimit,
|
||||
namespace,
|
||||
savedObjectsClient,
|
||||
}: SavedObjectsFetchByObjectOptions) {
|
||||
if (objects.length > exportSizeLimit) {
|
||||
throw Boom.badRequest(`Can't export more than ${exportSizeLimit} objects`);
|
||||
}
|
||||
const bulkGetResult = await savedObjectsClient.bulkGet(objects, { namespace });
|
||||
const erroredObjects = bulkGetResult.saved_objects.filter((obj) => !!obj.error);
|
||||
if (erroredObjects.length) {
|
||||
const err = Boom.badRequest();
|
||||
err.output.payload.attributes = {
|
||||
objects: erroredObjects,
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
return bulkGetResult.saved_objects;
|
||||
}
|
||||
|
||||
const validateOptions = ({
|
||||
objects,
|
||||
search,
|
||||
hasReference,
|
||||
exportSizeLimit,
|
||||
namespace,
|
||||
savedObjectsClient,
|
||||
types,
|
||||
}: SavedObjectsExportOptions):
|
||||
| SavedObjectsFetchByTypeOptions
|
||||
| SavedObjectsFetchByObjectOptions => {
|
||||
if ((types?.length ?? 0) > 0 && (objects?.length ?? 0) > 0) {
|
||||
throw Boom.badRequest(`Can't specify both "types" and "objects" properties when exporting`);
|
||||
}
|
||||
|
@ -94,38 +183,30 @@ async function fetchObjectsToExport({
|
|||
if (typeof search === 'string') {
|
||||
throw Boom.badRequest(`Can't specify both "search" and "objects" properties when exporting`);
|
||||
}
|
||||
const bulkGetResult = await savedObjectsClient.bulkGet(objects, { namespace });
|
||||
const erroredObjects = bulkGetResult.saved_objects.filter((obj) => !!obj.error);
|
||||
if (erroredObjects.length) {
|
||||
const err = Boom.badRequest();
|
||||
err.output.payload.attributes = {
|
||||
objects: erroredObjects,
|
||||
};
|
||||
throw err;
|
||||
if (hasReference && hasReference.length) {
|
||||
throw Boom.badRequest(
|
||||
`Can't specify both "references" and "objects" properties when exporting`
|
||||
);
|
||||
}
|
||||
return bulkGetResult.saved_objects;
|
||||
return {
|
||||
objects,
|
||||
exportSizeLimit,
|
||||
savedObjectsClient,
|
||||
namespace,
|
||||
} as SavedObjectsFetchByObjectOptions;
|
||||
} else if (types && types.length > 0) {
|
||||
const findResponse = await savedObjectsClient.find({
|
||||
type: types,
|
||||
return {
|
||||
types,
|
||||
hasReference,
|
||||
search,
|
||||
perPage: exportSizeLimit,
|
||||
namespaces: namespace ? [namespace] : undefined,
|
||||
});
|
||||
if (findResponse.total > exportSizeLimit) {
|
||||
throw Boom.badRequest(`Can't export more than ${exportSizeLimit} objects`);
|
||||
}
|
||||
|
||||
// sorts server-side by _id, since it's only available in fielddata
|
||||
return (
|
||||
findResponse.saved_objects
|
||||
// exclude the find-specific `score` property from the exported objects
|
||||
.map(({ score, ...obj }) => obj)
|
||||
.sort((a: SavedObject, b: SavedObject) => (a.id > b.id ? 1 : -1))
|
||||
);
|
||||
exportSizeLimit,
|
||||
savedObjectsClient,
|
||||
namespace,
|
||||
} as SavedObjectsFetchByTypeOptions;
|
||||
} else {
|
||||
throw Boom.badRequest('Either `type` or `objects` are required.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates sorted saved object stream to be used for export.
|
||||
|
@ -135,6 +216,7 @@ async function fetchObjectsToExport({
|
|||
*/
|
||||
export async function exportSavedObjectsToStream({
|
||||
types,
|
||||
hasReference,
|
||||
objects,
|
||||
search,
|
||||
savedObjectsClient,
|
||||
|
@ -143,14 +225,22 @@ export async function exportSavedObjectsToStream({
|
|||
excludeExportDetails = false,
|
||||
namespace,
|
||||
}: SavedObjectsExportOptions) {
|
||||
const rootObjects = await fetchObjectsToExport({
|
||||
types,
|
||||
objects,
|
||||
search,
|
||||
const fetchOptions = validateOptions({
|
||||
savedObjectsClient,
|
||||
exportSizeLimit,
|
||||
namespace,
|
||||
exportSizeLimit,
|
||||
hasReference,
|
||||
search,
|
||||
objects,
|
||||
excludeExportDetails,
|
||||
includeReferencesDeep,
|
||||
types,
|
||||
});
|
||||
|
||||
const rootObjects = isFetchByTypeOptions(fetchOptions)
|
||||
? await fetchByType(fetchOptions)
|
||||
: await fetchByObjects(fetchOptions);
|
||||
|
||||
let exportedObjects: Array<SavedObject<unknown>> = [];
|
||||
let missingReferences: SavedObjectsExportResultDetails['missingReferences'] = [];
|
||||
|
||||
|
|
|
@ -28,12 +28,20 @@ import { validateTypes, validateObjects } from './utils';
|
|||
export const registerExportRoute = (router: IRouter, config: SavedObjectConfig) => {
|
||||
const { maxImportExportSize } = config;
|
||||
|
||||
const referenceSchema = schema.object({
|
||||
type: schema.string(),
|
||||
id: schema.string(),
|
||||
});
|
||||
|
||||
router.post(
|
||||
{
|
||||
path: '/_export',
|
||||
validate: {
|
||||
body: schema.object({
|
||||
type: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])),
|
||||
hasReference: schema.maybe(
|
||||
schema.oneOf([referenceSchema, schema.arrayOf(referenceSchema)])
|
||||
),
|
||||
objects: schema.maybe(
|
||||
schema.arrayOf(
|
||||
schema.object({
|
||||
|
@ -51,7 +59,14 @@ export const registerExportRoute = (router: IRouter, config: SavedObjectConfig)
|
|||
},
|
||||
router.handleLegacyErrors(async (context, req, res) => {
|
||||
const savedObjectsClient = context.core.savedObjects.client;
|
||||
const { type, objects, search, excludeExportDetails, includeReferencesDeep } = req.body;
|
||||
const {
|
||||
type,
|
||||
hasReference,
|
||||
objects,
|
||||
search,
|
||||
excludeExportDetails,
|
||||
includeReferencesDeep,
|
||||
} = req.body;
|
||||
const types = typeof type === 'string' ? [type] : type;
|
||||
|
||||
// need to access the registry for type validation, can't use the schema for this
|
||||
|
@ -82,6 +97,7 @@ export const registerExportRoute = (router: IRouter, config: SavedObjectConfig)
|
|||
const exportStream = await exportSavedObjectsToStream({
|
||||
savedObjectsClient,
|
||||
types,
|
||||
hasReference: hasReference && !Array.isArray(hasReference) ? [hasReference] : hasReference,
|
||||
search,
|
||||
objects,
|
||||
exportSizeLimit: maxImportExportSize,
|
||||
|
|
|
@ -21,6 +21,14 @@ import { schema } from '@kbn/config-schema';
|
|||
import { IRouter } from '../../http';
|
||||
|
||||
export const registerFindRoute = (router: IRouter) => {
|
||||
const referenceSchema = schema.object({
|
||||
type: schema.string(),
|
||||
id: schema.string(),
|
||||
});
|
||||
const searchOperatorSchema = schema.oneOf([schema.literal('OR'), schema.literal('AND')], {
|
||||
defaultValue: 'OR',
|
||||
});
|
||||
|
||||
router.get(
|
||||
{
|
||||
path: '/_find',
|
||||
|
@ -30,19 +38,15 @@ export const registerFindRoute = (router: IRouter) => {
|
|||
page: schema.number({ min: 0, defaultValue: 1 }),
|
||||
type: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]),
|
||||
search: schema.maybe(schema.string()),
|
||||
default_search_operator: schema.oneOf([schema.literal('OR'), schema.literal('AND')], {
|
||||
defaultValue: 'OR',
|
||||
}),
|
||||
default_search_operator: searchOperatorSchema,
|
||||
search_fields: schema.maybe(
|
||||
schema.oneOf([schema.string(), schema.arrayOf(schema.string())])
|
||||
),
|
||||
sort_field: schema.maybe(schema.string()),
|
||||
has_reference: schema.maybe(
|
||||
schema.object({
|
||||
type: schema.string(),
|
||||
id: schema.string(),
|
||||
})
|
||||
schema.oneOf([referenceSchema, schema.arrayOf(referenceSchema)])
|
||||
),
|
||||
has_reference_operator: searchOperatorSchema,
|
||||
fields: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])),
|
||||
filter: schema.maybe(schema.string()),
|
||||
namespaces: schema.maybe(
|
||||
|
@ -67,6 +71,7 @@ export const registerFindRoute = (router: IRouter) => {
|
|||
typeof query.search_fields === 'string' ? [query.search_fields] : query.search_fields,
|
||||
sortField: query.sort_field,
|
||||
hasReference: query.has_reference,
|
||||
hasReferenceOperator: query.has_reference_operator,
|
||||
fields: typeof query.fields === 'string' ? [query.fields] : query.fields,
|
||||
filter: query.filter,
|
||||
namespaces,
|
||||
|
|
|
@ -118,6 +118,7 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
page: 1,
|
||||
type: ['foo', 'bar'],
|
||||
defaultSearchOperator: 'OR',
|
||||
hasReferenceOperator: 'OR',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -129,7 +130,7 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
|
||||
|
||||
const options = savedObjectsClient.find.mock.calls[0][0];
|
||||
expect(options).toEqual({ perPage: 10, page: 50, type: ['foo'], defaultSearchOperator: 'OR' });
|
||||
expect(options).toEqual(expect.objectContaining({ perPage: 10, page: 50 }));
|
||||
});
|
||||
|
||||
it('accepts the optional query parameter has_reference', async () => {
|
||||
|
@ -141,7 +142,7 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
expect(options.hasReference).toBe(undefined);
|
||||
});
|
||||
|
||||
it('accepts the query parameter has_reference', async () => {
|
||||
it('accepts the query parameter has_reference as an object', async () => {
|
||||
const references = querystring.escape(
|
||||
JSON.stringify({
|
||||
id: '1',
|
||||
|
@ -161,6 +162,53 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('accepts the query parameter has_reference as an array', async () => {
|
||||
const references = querystring.escape(
|
||||
JSON.stringify([
|
||||
{
|
||||
id: '1',
|
||||
type: 'reference',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'reference',
|
||||
},
|
||||
])
|
||||
);
|
||||
await supertest(httpSetup.server.listener)
|
||||
.get(`/api/saved_objects/_find?type=foo&has_reference=${references}`)
|
||||
.expect(200);
|
||||
|
||||
expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
|
||||
|
||||
const options = savedObjectsClient.find.mock.calls[0][0];
|
||||
expect(options.hasReference).toEqual([
|
||||
{
|
||||
id: '1',
|
||||
type: 'reference',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'reference',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('accepts the query parameter has_reference_operator', async () => {
|
||||
await supertest(httpSetup.server.listener)
|
||||
.get('/api/saved_objects/_find?type=foo&has_reference_operator=AND')
|
||||
.expect(200);
|
||||
|
||||
expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
|
||||
|
||||
const options = savedObjectsClient.find.mock.calls[0][0];
|
||||
expect(options).toEqual(
|
||||
expect.objectContaining({
|
||||
hasReferenceOperator: 'AND',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts the query parameter search_fields', async () => {
|
||||
await supertest(httpSetup.server.listener)
|
||||
.get('/api/saved_objects/_find?type=foo&search_fields=title')
|
||||
|
@ -169,13 +217,11 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
|
||||
|
||||
const options = savedObjectsClient.find.mock.calls[0][0];
|
||||
expect(options).toEqual({
|
||||
perPage: 20,
|
||||
page: 1,
|
||||
searchFields: ['title'],
|
||||
type: ['foo'],
|
||||
defaultSearchOperator: 'OR',
|
||||
});
|
||||
expect(options).toEqual(
|
||||
expect.objectContaining({
|
||||
searchFields: ['title'],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts the query parameter fields as a string', async () => {
|
||||
|
@ -186,13 +232,11 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
|
||||
|
||||
const options = savedObjectsClient.find.mock.calls[0][0];
|
||||
expect(options).toEqual({
|
||||
perPage: 20,
|
||||
page: 1,
|
||||
fields: ['title'],
|
||||
type: ['foo'],
|
||||
defaultSearchOperator: 'OR',
|
||||
});
|
||||
expect(options).toEqual(
|
||||
expect.objectContaining({
|
||||
fields: ['title'],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts the query parameter fields as an array', async () => {
|
||||
|
@ -203,13 +247,11 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
|
||||
|
||||
const options = savedObjectsClient.find.mock.calls[0][0];
|
||||
expect(options).toEqual({
|
||||
perPage: 20,
|
||||
page: 1,
|
||||
fields: ['title', 'description'],
|
||||
type: ['foo'],
|
||||
defaultSearchOperator: 'OR',
|
||||
});
|
||||
expect(options).toEqual(
|
||||
expect.objectContaining({
|
||||
fields: ['title', 'description'],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts the query parameter type as a string', async () => {
|
||||
|
@ -220,12 +262,11 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
|
||||
|
||||
const options = savedObjectsClient.find.mock.calls[0][0];
|
||||
expect(options).toEqual({
|
||||
perPage: 20,
|
||||
page: 1,
|
||||
type: ['index-pattern'],
|
||||
defaultSearchOperator: 'OR',
|
||||
});
|
||||
expect(options).toEqual(
|
||||
expect.objectContaining({
|
||||
type: ['index-pattern'],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts the query parameter type as an array', async () => {
|
||||
|
@ -236,12 +277,11 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
|
||||
|
||||
const options = savedObjectsClient.find.mock.calls[0][0];
|
||||
expect(options).toEqual({
|
||||
perPage: 20,
|
||||
page: 1,
|
||||
type: ['index-pattern', 'visualization'],
|
||||
defaultSearchOperator: 'OR',
|
||||
});
|
||||
expect(options).toEqual(
|
||||
expect.objectContaining({
|
||||
type: ['index-pattern', 'visualization'],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts the query parameter namespaces as a string', async () => {
|
||||
|
@ -252,13 +292,11 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
|
||||
|
||||
const options = savedObjectsClient.find.mock.calls[0][0];
|
||||
expect(options).toEqual({
|
||||
perPage: 20,
|
||||
page: 1,
|
||||
type: ['index-pattern'],
|
||||
namespaces: ['foo'],
|
||||
defaultSearchOperator: 'OR',
|
||||
});
|
||||
expect(options).toEqual(
|
||||
expect.objectContaining({
|
||||
namespaces: ['foo'],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts the query parameter namespaces as an array', async () => {
|
||||
|
@ -269,12 +307,10 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
|
||||
|
||||
const options = savedObjectsClient.find.mock.calls[0][0];
|
||||
expect(options).toEqual({
|
||||
perPage: 20,
|
||||
page: 1,
|
||||
type: ['index-pattern'],
|
||||
namespaces: ['default', 'foo'],
|
||||
defaultSearchOperator: 'OR',
|
||||
});
|
||||
expect(options).toEqual(
|
||||
expect.objectContaining({
|
||||
namespaces: ['default', 'foo'],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -154,9 +154,10 @@ export class SavedObjectsErrorHelpers {
|
|||
return decorate(error, CODE_CONFLICT, 409, reason);
|
||||
}
|
||||
|
||||
public static createConflictError(type: string, id: string) {
|
||||
public static createConflictError(type: string, id: string, reason?: string) {
|
||||
return SavedObjectsErrorHelpers.decorateConflictError(
|
||||
Boom.conflict(`Saved object [${type}/${id}] conflict`)
|
||||
Boom.conflict(`Saved object [${type}/${id}] conflict`),
|
||||
reason
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ const create = (): jest.Mocked<ISavedObjectsRepository> => ({
|
|||
deleteFromNamespaces: jest.fn(),
|
||||
deleteByNamespace: jest.fn(),
|
||||
incrementCounter: jest.fn(),
|
||||
removeReferencesTo: jest.fn(),
|
||||
});
|
||||
|
||||
export const savedObjectsRepositoryMock = { create };
|
||||
|
|
|
@ -2446,6 +2446,161 @@ describe('SavedObjectsRepository', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#removeReferencesTo', () => {
|
||||
const type = 'type';
|
||||
const id = 'id';
|
||||
const defaultOptions = {};
|
||||
|
||||
const updatedCount = 42;
|
||||
|
||||
const removeReferencesToSuccess = async (options = defaultOptions) => {
|
||||
client.updateByQuery.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise({
|
||||
updated: updatedCount,
|
||||
})
|
||||
);
|
||||
return await savedObjectsRepository.removeReferencesTo(type, id, options);
|
||||
};
|
||||
|
||||
describe('client calls', () => {
|
||||
it('should use the ES updateByQuery action', async () => {
|
||||
await removeReferencesToSuccess();
|
||||
expect(client.updateByQuery).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('uses the correct default `refresh` value', async () => {
|
||||
await removeReferencesToSuccess();
|
||||
expect(client.updateByQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
refresh: true,
|
||||
}),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('merges output of getSearchDsl into es request body', async () => {
|
||||
const query = { query: 1, aggregations: 2 };
|
||||
getSearchDslNS.getSearchDsl.mockReturnValue(query);
|
||||
await removeReferencesToSuccess({ type });
|
||||
|
||||
expect(client.updateByQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({ ...query }),
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('should set index to all known SO indices on the request', async () => {
|
||||
await removeReferencesToSuccess();
|
||||
expect(client.updateByQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
index: ['.kibana-test', 'custom'],
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('should use the `refresh` option in the request', async () => {
|
||||
const refresh = Symbol();
|
||||
|
||||
await removeReferencesToSuccess({ refresh });
|
||||
expect(client.updateByQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
refresh,
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass the correct parameters to the update script', async () => {
|
||||
await removeReferencesToSuccess();
|
||||
expect(client.updateByQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
script: expect.objectContaining({
|
||||
params: {
|
||||
type,
|
||||
id,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('search dsl', () => {
|
||||
it(`passes mappings and registry to getSearchDsl`, async () => {
|
||||
await removeReferencesToSuccess();
|
||||
expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(
|
||||
mappings,
|
||||
registry,
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('passes namespace to getSearchDsl', async () => {
|
||||
await removeReferencesToSuccess({ namespace: 'some-ns' });
|
||||
expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(
|
||||
mappings,
|
||||
registry,
|
||||
expect.objectContaining({
|
||||
namespaces: ['some-ns'],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('passes hasReference to getSearchDsl', async () => {
|
||||
await removeReferencesToSuccess();
|
||||
expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(
|
||||
mappings,
|
||||
registry,
|
||||
expect.objectContaining({
|
||||
hasReference: {
|
||||
type,
|
||||
id,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('passes all known types to getSearchDsl', async () => {
|
||||
await removeReferencesToSuccess();
|
||||
expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(
|
||||
mappings,
|
||||
registry,
|
||||
expect.objectContaining({
|
||||
type: registry.getAllTypes().map((type) => type.name),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('returns', () => {
|
||||
it('returns the updated count from the ES response', async () => {
|
||||
const response = await removeReferencesToSuccess();
|
||||
expect(response.updated).toBe(updatedCount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('errors', () => {
|
||||
it(`throws when ES returns failures`, async () => {
|
||||
client.updateByQuery.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise({
|
||||
updated: 7,
|
||||
failures: ['failure', 'another-failure'],
|
||||
})
|
||||
);
|
||||
|
||||
await expect(
|
||||
savedObjectsRepository.removeReferencesTo(type, id, defaultOptions)
|
||||
).rejects.toThrowError(createConflictError(type, id));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#find', () => {
|
||||
const generateSearchResults = (namespace) => {
|
||||
return {
|
||||
|
@ -2811,6 +2966,19 @@ describe('SavedObjectsRepository', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it(`accepts hasReferenceOperator`, async () => {
|
||||
const relevantOpts = {
|
||||
...commonOptions,
|
||||
hasReferenceOperator: 'AND',
|
||||
};
|
||||
|
||||
await findSuccess(relevantOpts, namespace);
|
||||
expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, {
|
||||
...relevantOpts,
|
||||
hasReferenceOperator: 'AND',
|
||||
});
|
||||
});
|
||||
|
||||
it(`accepts KQL expression filter and passes KueryNode to getSearchDsl`, async () => {
|
||||
const findOpts = {
|
||||
namespaces: [namespace],
|
||||
|
|
|
@ -57,6 +57,8 @@ import {
|
|||
SavedObjectsAddToNamespacesResponse,
|
||||
SavedObjectsDeleteFromNamespacesOptions,
|
||||
SavedObjectsDeleteFromNamespacesResponse,
|
||||
SavedObjectsRemoveReferencesToOptions,
|
||||
SavedObjectsRemoveReferencesToResponse,
|
||||
} from '../saved_objects_client';
|
||||
import {
|
||||
SavedObject,
|
||||
|
@ -708,6 +710,7 @@ export class SavedObjectsRepository {
|
|||
searchFields,
|
||||
rootSearchFields,
|
||||
hasReference,
|
||||
hasReferenceOperator,
|
||||
page = FIND_DEFAULT_PAGE,
|
||||
perPage = FIND_DEFAULT_PER_PAGE,
|
||||
sortField,
|
||||
|
@ -790,6 +793,7 @@ export class SavedObjectsRepository {
|
|||
namespaces,
|
||||
typeToNamespacesMap,
|
||||
hasReference,
|
||||
hasReferenceOperator,
|
||||
kueryNode,
|
||||
}),
|
||||
},
|
||||
|
@ -1445,6 +1449,71 @@ export class SavedObjectsRepository {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all objects containing a reference to the given {type, id} tuple to remove the said reference.
|
||||
*
|
||||
* @remarks Will throw a conflict error if the `update_by_query` operation returns any failure. In that case
|
||||
* some references might have been removed, and some were not. It is the caller's responsibility
|
||||
* to handle and fix this situation if it was to happen.
|
||||
*/
|
||||
async removeReferencesTo(
|
||||
type: string,
|
||||
id: string,
|
||||
options: SavedObjectsRemoveReferencesToOptions = {}
|
||||
): Promise<SavedObjectsRemoveReferencesToResponse> {
|
||||
const { namespace, refresh = true } = options;
|
||||
const allTypes = this._registry.getAllTypes().map((t) => t.name);
|
||||
|
||||
// we need to target all SO indices as all types of objects may have references to the given SO.
|
||||
const targetIndices = this.getIndicesForTypes(allTypes);
|
||||
|
||||
const { body } = await this.client.updateByQuery(
|
||||
{
|
||||
index: targetIndices,
|
||||
refresh,
|
||||
body: {
|
||||
script: {
|
||||
source: `
|
||||
if (ctx._source.containsKey('references')) {
|
||||
def items_to_remove = [];
|
||||
for (item in ctx._source.references) {
|
||||
if ( (item['type'] == params['type']) && (item['id'] == params['id']) ) {
|
||||
items_to_remove.add(item);
|
||||
}
|
||||
}
|
||||
ctx._source.references.removeAll(items_to_remove);
|
||||
}
|
||||
`,
|
||||
params: {
|
||||
type,
|
||||
id,
|
||||
},
|
||||
lang: 'painless',
|
||||
},
|
||||
conflicts: 'proceed',
|
||||
...getSearchDsl(this._mappings, this._registry, {
|
||||
namespaces: namespace ? [namespace] : undefined,
|
||||
type: allTypes,
|
||||
hasReference: { type, id },
|
||||
}),
|
||||
},
|
||||
},
|
||||
{ ignore: [404] }
|
||||
);
|
||||
|
||||
if (body.failures?.length) {
|
||||
throw SavedObjectsErrorHelpers.createConflictError(
|
||||
type,
|
||||
id,
|
||||
`${body.failures.length} references could not be removed`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
updated: body.updated,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases a counter field by one. Creates the document if one doesn't exist for the given id.
|
||||
*
|
||||
|
|
|
@ -23,7 +23,7 @@ type KueryNode = any;
|
|||
|
||||
import { typeRegistryMock } from '../../../saved_objects_type_registry.mock';
|
||||
import { ALL_NAMESPACES_STRING } from '../utils';
|
||||
import { getQueryParams } from './query_params';
|
||||
import { getQueryParams, getClauseForReference } from './query_params';
|
||||
|
||||
const registry = typeRegistryMock.create();
|
||||
|
||||
|
@ -93,7 +93,7 @@ describe('#getQueryParams', () => {
|
|||
const mappings = MAPPINGS;
|
||||
type Result = ReturnType<typeof getQueryParams>;
|
||||
|
||||
describe('kueryNode filter clause (query.bool.filter[...]', () => {
|
||||
describe('kueryNode filter clause', () => {
|
||||
const expectResult = (result: Result, expected: any) => {
|
||||
expect(result.query.bool.filter).toEqual(expect.arrayContaining([expected]));
|
||||
};
|
||||
|
@ -150,12 +150,17 @@ describe('#getQueryParams', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('reference filter clause (query.bool.filter[bool.must])', () => {
|
||||
describe('reference filter clause', () => {
|
||||
describe('`hasReference` parameter', () => {
|
||||
const expectResult = (result: Result, expected: any) => {
|
||||
expect(result.query.bool.filter).toEqual(
|
||||
expect.arrayContaining([{ bool: expect.objectContaining({ must: expected }) }])
|
||||
);
|
||||
const getReferencesFilter = (result: any) => {
|
||||
const filters = result.query.bool.filter;
|
||||
return filters.find((filter: any) => {
|
||||
const clauses = filter.bool?.must ?? filter.bool?.should;
|
||||
if (!clauses) {
|
||||
return false;
|
||||
}
|
||||
return clauses[0].nested?.path === 'references' ?? false;
|
||||
});
|
||||
};
|
||||
|
||||
it('does not include the clause when `hasReference` is not specified', () => {
|
||||
|
@ -164,36 +169,96 @@ describe('#getQueryParams', () => {
|
|||
registry,
|
||||
hasReference: undefined,
|
||||
});
|
||||
expectResult(result, undefined);
|
||||
|
||||
expect(getReferencesFilter(result)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('creates a clause with query for specified reference', () => {
|
||||
it('creates a should clause for specified reference when operator is `OR`', () => {
|
||||
const hasReference = { id: 'foo', type: 'bar' };
|
||||
const result = getQueryParams({
|
||||
mappings,
|
||||
registry,
|
||||
hasReference,
|
||||
hasReferenceOperator: 'OR',
|
||||
});
|
||||
expect(getReferencesFilter(result)).toEqual({
|
||||
bool: {
|
||||
should: [getClauseForReference(hasReference)],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a must clause for specified reference when operator is `AND`', () => {
|
||||
const hasReference = { id: 'foo', type: 'bar' };
|
||||
const result = getQueryParams({
|
||||
mappings,
|
||||
registry,
|
||||
hasReference,
|
||||
hasReferenceOperator: 'AND',
|
||||
});
|
||||
expect(getReferencesFilter(result)).toEqual({
|
||||
bool: {
|
||||
must: [getClauseForReference(hasReference)],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('handles multiple references when operator is `OR`', () => {
|
||||
const hasReference = [
|
||||
{ id: 'foo', type: 'bar' },
|
||||
{ id: 'hello', type: 'dolly' },
|
||||
];
|
||||
const result = getQueryParams({
|
||||
mappings,
|
||||
registry,
|
||||
hasReference,
|
||||
hasReferenceOperator: 'OR',
|
||||
});
|
||||
expect(getReferencesFilter(result)).toEqual({
|
||||
bool: {
|
||||
should: hasReference.map(getClauseForReference),
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('handles multiple references when operator is `AND`', () => {
|
||||
const hasReference = [
|
||||
{ id: 'foo', type: 'bar' },
|
||||
{ id: 'hello', type: 'dolly' },
|
||||
];
|
||||
const result = getQueryParams({
|
||||
mappings,
|
||||
registry,
|
||||
hasReference,
|
||||
hasReferenceOperator: 'AND',
|
||||
});
|
||||
expect(getReferencesFilter(result)).toEqual({
|
||||
bool: {
|
||||
must: hasReference.map(getClauseForReference),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults to `OR` when operator is not specified', () => {
|
||||
const hasReference = { id: 'foo', type: 'bar' };
|
||||
const result = getQueryParams({
|
||||
mappings,
|
||||
registry,
|
||||
hasReference,
|
||||
});
|
||||
expectResult(result, [
|
||||
{
|
||||
nested: {
|
||||
path: 'references',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'references.id': hasReference.id } },
|
||||
{ term: { 'references.type': hasReference.type } },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
expect(getReferencesFilter(result)).toEqual({
|
||||
bool: {
|
||||
should: [getClauseForReference(hasReference)],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('type filter clauses (query.bool.filter[bool.should])', () => {
|
||||
describe('type filter clauses', () => {
|
||||
describe('`type` parameter', () => {
|
||||
const expectResult = (result: Result, ...types: string[]) => {
|
||||
expect(result.query.bool.filter).toEqual(
|
||||
|
|
|
@ -122,11 +122,13 @@ function getClauseForType(
|
|||
};
|
||||
}
|
||||
|
||||
interface HasReferenceQueryParams {
|
||||
export interface HasReferenceQueryParams {
|
||||
type: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type SearchOperator = 'AND' | 'OR';
|
||||
|
||||
interface QueryParams {
|
||||
mappings: IndexMapping;
|
||||
registry: ISavedObjectTypeRegistry;
|
||||
|
@ -134,13 +136,58 @@ interface QueryParams {
|
|||
type?: string | string[];
|
||||
typeToNamespacesMap?: Map<string, string[] | undefined>;
|
||||
search?: string;
|
||||
defaultSearchOperator?: SearchOperator;
|
||||
searchFields?: string[];
|
||||
rootSearchFields?: string[];
|
||||
defaultSearchOperator?: string;
|
||||
hasReference?: HasReferenceQueryParams;
|
||||
hasReference?: HasReferenceQueryParams | HasReferenceQueryParams[];
|
||||
hasReferenceOperator?: SearchOperator;
|
||||
kueryNode?: KueryNode;
|
||||
}
|
||||
|
||||
function getReferencesFilter(
|
||||
references: HasReferenceQueryParams[],
|
||||
operator: SearchOperator = 'OR'
|
||||
) {
|
||||
if (operator === 'AND') {
|
||||
return {
|
||||
bool: {
|
||||
must: references.map(getClauseForReference),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
bool: {
|
||||
should: references.map(getClauseForReference),
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getClauseForReference(reference: HasReferenceQueryParams) {
|
||||
return {
|
||||
nested: {
|
||||
path: 'references',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'references.id': reference.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'references.type': reference.type,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "query" related keys for the search body
|
||||
*/
|
||||
|
@ -155,6 +202,7 @@ export function getQueryParams({
|
|||
rootSearchFields,
|
||||
defaultSearchOperator,
|
||||
hasReference,
|
||||
hasReferenceOperator,
|
||||
kueryNode,
|
||||
}: QueryParams) {
|
||||
const types = getTypes(
|
||||
|
@ -162,6 +210,10 @@ export function getQueryParams({
|
|||
typeToNamespacesMap ? Array.from(typeToNamespacesMap.keys()) : type
|
||||
);
|
||||
|
||||
if (hasReference && !Array.isArray(hasReference)) {
|
||||
hasReference = [hasReference];
|
||||
}
|
||||
|
||||
// A de-duplicated set of namespaces makes for a more effecient query.
|
||||
//
|
||||
// Additonally, we treat the `*` namespace as the `default` namespace.
|
||||
|
@ -181,33 +233,11 @@ export function getQueryParams({
|
|||
const bool: any = {
|
||||
filter: [
|
||||
...(kueryNode != null ? [esKuery.toElasticsearchQuery(kueryNode)] : []),
|
||||
...(hasReference && hasReference.length
|
||||
? [getReferencesFilter(hasReference, hasReferenceOperator)]
|
||||
: []),
|
||||
{
|
||||
bool: {
|
||||
must: hasReference
|
||||
? [
|
||||
{
|
||||
nested: {
|
||||
path: 'references',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'references.id': hasReference.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'references.type': hasReference.type,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
should: types.map((shouldType) => {
|
||||
const normalizedNamespaces = normalizeNamespaces(
|
||||
typeToNamespacesMap ? typeToNamespacesMap.get(shouldType) : namespaces
|
||||
|
|
|
@ -57,7 +57,7 @@ describe('getSearchDsl', () => {
|
|||
});
|
||||
|
||||
describe('passes control', () => {
|
||||
it('passes (mappings, schema, namespaces, type, typeToNamespacesMap, search, searchFields, rootSearchFields, hasReference) to getQueryParams', () => {
|
||||
it('passes (mappings, schema, namespaces, type, typeToNamespacesMap, search, searchFields, rootSearchFields, hasReference, hasReferenceOperator) to getQueryParams', () => {
|
||||
const opts = {
|
||||
namespaces: ['foo-namespace'],
|
||||
type: 'foo',
|
||||
|
@ -65,11 +65,12 @@ describe('getSearchDsl', () => {
|
|||
search: 'bar',
|
||||
searchFields: ['baz'],
|
||||
rootSearchFields: ['qux'],
|
||||
defaultSearchOperator: 'AND',
|
||||
defaultSearchOperator: 'AND' as queryParamsNS.SearchOperator,
|
||||
hasReference: {
|
||||
type: 'bar',
|
||||
id: '1',
|
||||
},
|
||||
hasReferenceOperator: 'AND' as queryParamsNS.SearchOperator,
|
||||
};
|
||||
|
||||
getSearchDsl(mappings, registry, opts);
|
||||
|
@ -85,6 +86,7 @@ describe('getSearchDsl', () => {
|
|||
rootSearchFields: opts.rootSearchFields,
|
||||
defaultSearchOperator: opts.defaultSearchOperator,
|
||||
hasReference: opts.hasReference,
|
||||
hasReferenceOperator: opts.hasReferenceOperator,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
import Boom from '@hapi/boom';
|
||||
|
||||
import { IndexMapping } from '../../../mappings';
|
||||
import { getQueryParams } from './query_params';
|
||||
import { getQueryParams, HasReferenceQueryParams, SearchOperator } from './query_params';
|
||||
import { getSortingParams } from './sorting_params';
|
||||
import { ISavedObjectTypeRegistry } from '../../../saved_objects_type_registry';
|
||||
|
||||
|
@ -29,17 +29,15 @@ type KueryNode = any;
|
|||
interface GetSearchDslOptions {
|
||||
type: string | string[];
|
||||
search?: string;
|
||||
defaultSearchOperator?: string;
|
||||
defaultSearchOperator?: SearchOperator;
|
||||
searchFields?: string[];
|
||||
rootSearchFields?: string[];
|
||||
sortField?: string;
|
||||
sortOrder?: string;
|
||||
namespaces?: string[];
|
||||
typeToNamespacesMap?: Map<string, string[] | undefined>;
|
||||
hasReference?: {
|
||||
type: string;
|
||||
id: string;
|
||||
};
|
||||
hasReference?: HasReferenceQueryParams | HasReferenceQueryParams[];
|
||||
hasReferenceOperator?: SearchOperator;
|
||||
kueryNode?: KueryNode;
|
||||
}
|
||||
|
||||
|
@ -59,6 +57,7 @@ export function getSearchDsl(
|
|||
namespaces,
|
||||
typeToNamespacesMap,
|
||||
hasReference,
|
||||
hasReferenceOperator,
|
||||
kueryNode,
|
||||
} = options;
|
||||
|
||||
|
@ -82,6 +81,7 @@ export function getSearchDsl(
|
|||
rootSearchFields,
|
||||
defaultSearchOperator,
|
||||
hasReference,
|
||||
hasReferenceOperator,
|
||||
kueryNode,
|
||||
}),
|
||||
...getSortingParams(mappings, type, sortField, sortOrder),
|
||||
|
|
|
@ -34,6 +34,7 @@ const create = () =>
|
|||
update: jest.fn(),
|
||||
addToNamespaces: jest.fn(),
|
||||
deleteFromNamespaces: jest.fn(),
|
||||
removeReferencesTo: jest.fn(),
|
||||
} as unknown) as jest.Mocked<SavedObjectsClientContract>);
|
||||
|
||||
export const savedObjectsClientMock = { create };
|
||||
|
|
|
@ -196,3 +196,19 @@ test(`#deleteFromNamespaces`, async () => {
|
|||
expect(mockRepository.deleteFromNamespaces).toHaveBeenCalledWith(type, id, namespaces, options);
|
||||
expect(result).toBe(returnValue);
|
||||
});
|
||||
|
||||
test(`#removeReferencesTo`, async () => {
|
||||
const returnValue = Symbol();
|
||||
const mockRepository = {
|
||||
removeReferencesTo: jest.fn().mockResolvedValue(returnValue),
|
||||
};
|
||||
const client = new SavedObjectsClient(mockRepository);
|
||||
|
||||
const type = Symbol();
|
||||
const id = Symbol();
|
||||
const options = Symbol();
|
||||
const result = await client.removeReferencesTo(type, id, options);
|
||||
|
||||
expect(mockRepository.removeReferencesTo).toHaveBeenCalledWith(type, id, options);
|
||||
expect(result).toBe(returnValue);
|
||||
});
|
||||
|
|
|
@ -209,6 +209,24 @@ export interface SavedObjectsDeleteFromNamespacesResponse {
|
|||
namespaces: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface SavedObjectsRemoveReferencesToOptions extends SavedObjectsBaseOptions {
|
||||
/** The Elasticsearch Refresh setting for this operation. Defaults to `true` */
|
||||
refresh?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface SavedObjectsRemoveReferencesToResponse extends SavedObjectsBaseOptions {
|
||||
/** The number of objects that have been updated by this operation */
|
||||
updated: number;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @public
|
||||
|
@ -433,4 +451,15 @@ export class SavedObjectsClient {
|
|||
): Promise<SavedObjectsBulkUpdateResponse<T>> {
|
||||
return await this._repository.bulkUpdate(objects, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all objects containing a reference to the given {type, id} tuple to remove the said reference.
|
||||
*/
|
||||
async removeReferencesTo(
|
||||
type: string,
|
||||
id: string,
|
||||
options?: SavedObjectsRemoveReferencesToOptions
|
||||
) {
|
||||
return await this._repository.removeReferencesTo(type, id, options);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,6 +60,14 @@ export interface SavedObjectStatusMeta {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface SavedObjectsFindOptionsReference {
|
||||
type: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @public
|
||||
|
@ -85,7 +93,20 @@ export interface SavedObjectsFindOptions {
|
|||
* be modified. If used in conjunction with `searchFields`, both are concatenated together.
|
||||
*/
|
||||
rootSearchFields?: string[];
|
||||
hasReference?: { type: string; id: string };
|
||||
|
||||
/**
|
||||
* Search for documents having a reference to the specified objects.
|
||||
* Use `hasReferenceOperator` to specify the operator to use when searching for multiple references.
|
||||
*/
|
||||
hasReference?: SavedObjectsFindOptionsReference | SavedObjectsFindOptionsReference[];
|
||||
/**
|
||||
* The operator to use when searching by multiple references using the `hasReference` option. Defaults to `OR`
|
||||
*/
|
||||
hasReferenceOperator?: 'AND' | 'OR';
|
||||
|
||||
/**
|
||||
* The search operator to use with the provided filter. Defaults to `OR`
|
||||
*/
|
||||
defaultSearchOperator?: 'AND' | 'OR';
|
||||
filter?: string | KueryNode;
|
||||
namespaces?: string[];
|
||||
|
|
|
@ -728,7 +728,7 @@ export interface Explanation {
|
|||
}
|
||||
|
||||
// @public
|
||||
export function exportSavedObjectsToStream({ types, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, }: SavedObjectsExportOptions): Promise<import("stream").Readable>;
|
||||
export function exportSavedObjectsToStream({ types, hasReference, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, }: SavedObjectsExportOptions): Promise<import("stream").Readable>;
|
||||
|
||||
// @public
|
||||
export interface FakeRequest {
|
||||
|
@ -1987,6 +1987,7 @@ export class SavedObjectsClient {
|
|||
errors: typeof SavedObjectsErrorHelpers;
|
||||
find<T = unknown>(options: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T>>;
|
||||
get<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObject<T>>;
|
||||
removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise<SavedObjectsRemoveReferencesToResponse>;
|
||||
update<T = unknown>(type: string, id: string, attributes: Partial<T>, options?: SavedObjectsUpdateOptions): Promise<SavedObjectsUpdateResponse<T>>;
|
||||
}
|
||||
|
||||
|
@ -2094,7 +2095,7 @@ export class SavedObjectsErrorHelpers {
|
|||
// (undocumented)
|
||||
static createBadRequestError(reason?: string): DecoratedError;
|
||||
// (undocumented)
|
||||
static createConflictError(type: string, id: string): DecoratedError;
|
||||
static createConflictError(type: string, id: string, reason?: string): DecoratedError;
|
||||
// (undocumented)
|
||||
static createGenericNotFoundError(type?: string | null, id?: string | null): DecoratedError;
|
||||
// (undocumented)
|
||||
|
@ -2151,6 +2152,7 @@ export class SavedObjectsErrorHelpers {
|
|||
export interface SavedObjectsExportOptions {
|
||||
excludeExportDetails?: boolean;
|
||||
exportSizeLimit: number;
|
||||
hasReference?: SavedObjectsFindOptionsReference[];
|
||||
includeReferencesDeep?: boolean;
|
||||
namespace?: string;
|
||||
objects?: Array<{
|
||||
|
@ -2177,18 +2179,14 @@ export type SavedObjectsFieldMapping = SavedObjectsCoreFieldMapping | SavedObjec
|
|||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsFindOptions {
|
||||
// (undocumented)
|
||||
defaultSearchOperator?: 'AND' | 'OR';
|
||||
fields?: string[];
|
||||
// Warning: (ae-forgotten-export) The symbol "KueryNode" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
filter?: string | KueryNode;
|
||||
// (undocumented)
|
||||
hasReference?: {
|
||||
type: string;
|
||||
id: string;
|
||||
};
|
||||
hasReference?: SavedObjectsFindOptionsReference | SavedObjectsFindOptionsReference[];
|
||||
hasReferenceOperator?: 'AND' | 'OR';
|
||||
// (undocumented)
|
||||
namespaces?: string[];
|
||||
// (undocumented)
|
||||
|
@ -2208,6 +2206,14 @@ export interface SavedObjectsFindOptions {
|
|||
typeToNamespacesMap?: Map<string, string[] | undefined>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsFindOptionsReference {
|
||||
// (undocumented)
|
||||
id: string;
|
||||
// (undocumented)
|
||||
type: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface SavedObjectsFindResponse<T = unknown> {
|
||||
// (undocumented)
|
||||
|
@ -2401,6 +2407,16 @@ export interface SavedObjectsRawDoc {
|
|||
_type?: string;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsRemoveReferencesToOptions extends SavedObjectsBaseOptions {
|
||||
refresh?: boolean;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsRemoveReferencesToResponse extends SavedObjectsBaseOptions {
|
||||
updated: number;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export class SavedObjectsRepository {
|
||||
addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<SavedObjectsAddToNamespacesResponse>;
|
||||
|
@ -2420,6 +2436,7 @@ export class SavedObjectsRepository {
|
|||
find<T = unknown>(options: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T>>;
|
||||
get<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObject<T>>;
|
||||
incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject>;
|
||||
removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise<SavedObjectsRemoveReferencesToResponse>;
|
||||
update<T = unknown>(type: string, id: string, attributes: Partial<T>, options?: SavedObjectsUpdateOptions): Promise<SavedObjectsUpdateResponse<T>>;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
"uiActions",
|
||||
"savedObjects"
|
||||
],
|
||||
"optionalPlugins": ["home", "share", "usageCollection"],
|
||||
"optionalPlugins": ["home", "share", "usageCollection", "savedObjectsTaggingOss"],
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredBundles": ["kibanaUtils", "kibanaReact", "home"]
|
||||
|
|
|
@ -44,6 +44,7 @@ import { SharePluginStart } from '../../../share/public';
|
|||
import { KibanaLegacyStart, configureAppAngularModule } from '../../../kibana_legacy/public';
|
||||
import { UrlForwardingStart } from '../../../url_forwarding/public';
|
||||
import { SavedObjectLoader, SavedObjectsStart } from '../../../saved_objects/public';
|
||||
import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public';
|
||||
|
||||
// required for i18nIdDirective
|
||||
import 'angular-sanitize';
|
||||
|
@ -76,6 +77,7 @@ export interface RenderDeps {
|
|||
scopedHistory: () => ScopedHistory;
|
||||
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
|
||||
savedObjects: SavedObjectsStart;
|
||||
savedObjectsTagging?: SavedObjectsTaggingApi;
|
||||
restorePreviousUrl: () => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,8 @@ import {
|
|||
distinctUntilChanged,
|
||||
} from 'rxjs/operators';
|
||||
import { History } from 'history';
|
||||
import { SavedObjectSaveOpts } from 'src/plugins/saved_objects/public';
|
||||
import { SavedObjectSaveOpts, SavedObject } from 'src/plugins/saved_objects/public';
|
||||
import type { TagDecoratedSavedObject } from 'src/plugins/saved_objects_tagging_oss/public';
|
||||
import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public';
|
||||
import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen';
|
||||
|
||||
|
@ -70,7 +71,7 @@ import {
|
|||
import { NavAction, SavedDashboardPanel } from '../types';
|
||||
|
||||
import { showOptionsPopover } from './top_nav/show_options_popover';
|
||||
import { DashboardSaveModal } from './top_nav/save_modal';
|
||||
import { DashboardSaveModal, SaveOptions } from './top_nav/save_modal';
|
||||
import { showCloneModal } from './top_nav/show_clone_modal';
|
||||
import { saveDashboard } from './lib';
|
||||
import { DashboardStateManager } from './dashboard_state_manager';
|
||||
|
@ -155,6 +156,7 @@ export class DashboardAppController {
|
|||
kbnUrlStateStorage,
|
||||
usageCollection,
|
||||
navigation,
|
||||
savedObjectsTagging,
|
||||
}: DashboardAppControllerDependencies) {
|
||||
const filterManager = queryService.filterManager;
|
||||
const timefilter = queryService.timefilter.timefilter;
|
||||
|
@ -180,6 +182,11 @@ export class DashboardAppController {
|
|||
.getStateTransfer(scopedHistory())
|
||||
.getIncomingEmbeddablePackage();
|
||||
|
||||
// TS is picky with type guards, we can't just inline `() => false`
|
||||
function defaultTaggingGuard(obj: SavedObject): obj is TagDecoratedSavedObject {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dashboardStateManager = new DashboardStateManager({
|
||||
savedDashboard: dash,
|
||||
hideWriteControls: dashboardConfig.getHideWriteControls(),
|
||||
|
@ -187,6 +194,7 @@ export class DashboardAppController {
|
|||
kbnUrlStateStorage,
|
||||
history,
|
||||
usageCollection,
|
||||
hasTaggingCapabilities: savedObjectsTagging?.ui.hasTagDecoration ?? defaultTaggingGuard,
|
||||
});
|
||||
|
||||
// sync initial app filters from state to filterManager
|
||||
|
@ -882,6 +890,15 @@ export class DashboardAppController {
|
|||
const currentTitle = dashboardStateManager.getTitle();
|
||||
const currentDescription = dashboardStateManager.getDescription();
|
||||
const currentTimeRestore = dashboardStateManager.getTimeRestore();
|
||||
|
||||
let currentTags: string[] = [];
|
||||
if (savedObjectsTagging) {
|
||||
const dashboard = dashboardStateManager.savedDashboard;
|
||||
if (savedObjectsTagging.ui.hasTagDecoration(dashboard)) {
|
||||
currentTags = dashboard.getTags();
|
||||
}
|
||||
}
|
||||
|
||||
const onSave = ({
|
||||
newTitle,
|
||||
newDescription,
|
||||
|
@ -889,18 +906,16 @@ export class DashboardAppController {
|
|||
newTimeRestore,
|
||||
isTitleDuplicateConfirmed,
|
||||
onTitleDuplicate,
|
||||
}: {
|
||||
newTitle: string;
|
||||
newDescription: string;
|
||||
newCopyOnSave: boolean;
|
||||
newTimeRestore: boolean;
|
||||
isTitleDuplicateConfirmed: boolean;
|
||||
onTitleDuplicate: () => void;
|
||||
}) => {
|
||||
newTags,
|
||||
}: SaveOptions) => {
|
||||
dashboardStateManager.setTitle(newTitle);
|
||||
dashboardStateManager.setDescription(newDescription);
|
||||
dashboardStateManager.savedDashboard.copyOnSave = newCopyOnSave;
|
||||
dashboardStateManager.setTimeRestore(newTimeRestore);
|
||||
if (savedObjectsTagging && newTags) {
|
||||
dashboardStateManager.setTags(newTags);
|
||||
}
|
||||
|
||||
const saveOptions = {
|
||||
confirmOverwrite: false,
|
||||
isTitleDuplicateConfirmed,
|
||||
|
@ -912,6 +927,9 @@ export class DashboardAppController {
|
|||
dashboardStateManager.setTitle(currentTitle);
|
||||
dashboardStateManager.setDescription(currentDescription);
|
||||
dashboardStateManager.setTimeRestore(currentTimeRestore);
|
||||
if (savedObjectsTagging) {
|
||||
dashboardStateManager.setTags(currentTags);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
});
|
||||
|
@ -923,6 +941,8 @@ export class DashboardAppController {
|
|||
onClose={() => {}}
|
||||
title={currentTitle}
|
||||
description={currentDescription}
|
||||
tags={currentTags}
|
||||
savedObjectsTagging={savedObjectsTagging}
|
||||
timeRestore={currentTimeRestore}
|
||||
showCopyOnSave={dash.id ? true : false}
|
||||
/>
|
||||
|
|
|
@ -41,6 +41,11 @@ describe('DashboardState', function () {
|
|||
},
|
||||
} as TimefilterContract;
|
||||
|
||||
// TS is *very* picky with type guards / predicates. can't just use jest.fn()
|
||||
function mockHasTaggingCapabilities(obj: any): obj is any {
|
||||
return false;
|
||||
}
|
||||
|
||||
function initDashboardState() {
|
||||
dashboardState = new DashboardStateManager({
|
||||
savedDashboard,
|
||||
|
@ -48,6 +53,7 @@ describe('DashboardState', function () {
|
|||
kibanaVersion: '7.0.0',
|
||||
kbnUrlStateStorage: createKbnUrlStateStorage(),
|
||||
history: createBrowserHistory(),
|
||||
hasTaggingCapabilities: mockHasTaggingCapabilities,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import { History } from 'history';
|
|||
|
||||
import { Filter, Query, TimefilterContract as Timefilter } from 'src/plugins/data/public';
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
||||
import type { SavedObjectTagDecoratorTypeGuard } from 'src/plugins/saved_objects_tagging_oss/public';
|
||||
import { migrateLegacyQuery } from './lib/migrate_legacy_query';
|
||||
|
||||
import { ViewMode } from '../embeddable_plugin';
|
||||
|
@ -86,6 +87,7 @@ export class DashboardStateManager {
|
|||
private readonly stateSyncRef: ISyncStateRef;
|
||||
private readonly history: History;
|
||||
private readonly usageCollection: UsageCollectionSetup | undefined;
|
||||
public readonly hasTaggingCapabilities: SavedObjectTagDecoratorTypeGuard;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -101,6 +103,7 @@ export class DashboardStateManager {
|
|||
kbnUrlStateStorage,
|
||||
history,
|
||||
usageCollection,
|
||||
hasTaggingCapabilities,
|
||||
}: {
|
||||
savedDashboard: SavedObjectDashboard;
|
||||
hideWriteControls: boolean;
|
||||
|
@ -108,16 +111,18 @@ export class DashboardStateManager {
|
|||
kbnUrlStateStorage: IKbnUrlStateStorage;
|
||||
history: History;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
hasTaggingCapabilities: SavedObjectTagDecoratorTypeGuard;
|
||||
}) {
|
||||
this.history = history;
|
||||
this.kibanaVersion = kibanaVersion;
|
||||
this.savedDashboard = savedDashboard;
|
||||
this.hideWriteControls = hideWriteControls;
|
||||
this.usageCollection = usageCollection;
|
||||
this.hasTaggingCapabilities = hasTaggingCapabilities;
|
||||
|
||||
// get state defaults from saved dashboard, make sure it is migrated
|
||||
this.stateDefaults = migrateAppState(
|
||||
getAppStateDefaults(this.savedDashboard, this.hideWriteControls),
|
||||
getAppStateDefaults(this.savedDashboard, this.hideWriteControls, this.hasTaggingCapabilities),
|
||||
kibanaVersion,
|
||||
usageCollection
|
||||
);
|
||||
|
@ -313,7 +318,7 @@ export class DashboardStateManager {
|
|||
// clone, but given how much code uses the state object, I determined that to be too risky of a change for
|
||||
// now. TODO: revisit this!
|
||||
this.stateDefaults = migrateAppState(
|
||||
getAppStateDefaults(this.savedDashboard, this.hideWriteControls),
|
||||
getAppStateDefaults(this.savedDashboard, this.hideWriteControls, this.hasTaggingCapabilities),
|
||||
this.kibanaVersion,
|
||||
this.usageCollection
|
||||
);
|
||||
|
@ -355,6 +360,10 @@ export class DashboardStateManager {
|
|||
return this.appState.description;
|
||||
}
|
||||
|
||||
public getTags() {
|
||||
return this.appState.tags;
|
||||
}
|
||||
|
||||
public setDescription(description: string) {
|
||||
this.stateContainer.transitions.set('description', description);
|
||||
}
|
||||
|
@ -364,6 +373,10 @@ export class DashboardStateManager {
|
|||
this.stateContainer.transitions.set('title', title);
|
||||
}
|
||||
|
||||
public setTags(tags: string[]) {
|
||||
this.stateContainer.transitions.set('tags', tags);
|
||||
}
|
||||
|
||||
public getAppState() {
|
||||
return this.stateContainer.get();
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ export function initDashboardApp(app, deps) {
|
|||
['hideWriteControls', { watchDepth: 'reference' }],
|
||||
['initialFilter', { watchDepth: 'reference' }],
|
||||
['initialPageSize', { watchDepth: 'reference' }],
|
||||
['taggingApi', { watchDepth: 'reference' }],
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -113,11 +114,26 @@ export function initDashboardApp(app, deps) {
|
|||
|
||||
$scope.listingLimit = deps.savedObjects.settings.getListingLimit();
|
||||
$scope.initialPageSize = deps.savedObjects.settings.getPerPage();
|
||||
$scope.taggingApi = deps.savedObjectsTagging;
|
||||
$scope.create = () => {
|
||||
history.push(DashboardConstants.CREATE_NEW_DASHBOARD_URL);
|
||||
};
|
||||
$scope.find = (search) => {
|
||||
return service.find(search, $scope.listingLimit);
|
||||
$scope.find = async (search) => {
|
||||
let searchTerm = search;
|
||||
let references = undefined;
|
||||
|
||||
if (deps.savedObjectsTagging) {
|
||||
const parsed = deps.savedObjectsTagging.ui.parseSearchQuery(search, {
|
||||
useName: true,
|
||||
});
|
||||
searchTerm = parsed.searchTerm;
|
||||
references = parsed.tagReferences;
|
||||
}
|
||||
|
||||
return service.find(searchTerm, {
|
||||
size: $scope.listingLimit,
|
||||
hasReference: references,
|
||||
});
|
||||
};
|
||||
$scope.editItem = ({ id }) => {
|
||||
history.push(`${createDashboardEditUrl(id)}?_a=(viewMode:edit)`);
|
||||
|
|
|
@ -17,18 +17,21 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import type { SavedObjectTagDecoratorTypeGuard } from 'src/plugins/saved_objects_tagging_oss/public';
|
||||
import { ViewMode } from '../../embeddable_plugin';
|
||||
import { SavedObjectDashboard } from '../../saved_dashboards';
|
||||
import { DashboardAppStateDefaults } from '../../types';
|
||||
|
||||
export function getAppStateDefaults(
|
||||
savedDashboard: SavedObjectDashboard,
|
||||
hideWriteControls: boolean
|
||||
hideWriteControls: boolean,
|
||||
hasTaggingCapabilities: SavedObjectTagDecoratorTypeGuard
|
||||
): DashboardAppStateDefaults {
|
||||
return {
|
||||
fullScreenMode: false,
|
||||
title: savedDashboard.title,
|
||||
description: savedDashboard.description || '',
|
||||
tags: hasTaggingCapabilities(savedDashboard) ? savedDashboard.getTags() : [],
|
||||
timeRestore: savedDashboard.timeRestore,
|
||||
panels: savedDashboard.panelsJSON ? JSON.parse(savedDashboard.panelsJSON) : [],
|
||||
options: savedDashboard.optionsJSON ? JSON.parse(savedDashboard.optionsJSON) : {},
|
||||
|
|
|
@ -38,8 +38,9 @@ export function saveDashboard(
|
|||
): Promise<string> {
|
||||
const savedDashboard = dashboardStateManager.savedDashboard;
|
||||
const appState = dashboardStateManager.appState;
|
||||
const hasTaggingCapabilities = dashboardStateManager.hasTaggingCapabilities;
|
||||
|
||||
updateSavedDashboard(savedDashboard, appState, timeFilter, toJson);
|
||||
updateSavedDashboard(savedDashboard, appState, timeFilter, hasTaggingCapabilities, toJson);
|
||||
|
||||
return savedDashboard.save(saveOptions).then((id: string) => {
|
||||
if (id) {
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
import { RefreshInterval, TimefilterContract } from 'src/plugins/data/public';
|
||||
import type { SavedObjectTagDecoratorTypeGuard } from 'src/plugins/saved_objects_tagging_oss/public';
|
||||
import { FilterUtils } from './filter_utils';
|
||||
import { SavedObjectDashboard } from '../../saved_dashboards';
|
||||
import { DashboardAppState } from '../../types';
|
||||
|
@ -28,6 +29,7 @@ export function updateSavedDashboard(
|
|||
savedDashboard: SavedObjectDashboard,
|
||||
appState: DashboardAppState,
|
||||
timeFilter: TimefilterContract,
|
||||
hasTaggingCapabilities: SavedObjectTagDecoratorTypeGuard,
|
||||
toJson: <T>(object: T) => string
|
||||
) {
|
||||
savedDashboard.title = appState.title;
|
||||
|
@ -36,6 +38,10 @@ export function updateSavedDashboard(
|
|||
savedDashboard.panelsJSON = toJson(appState.panels);
|
||||
savedDashboard.optionsJSON = toJson(appState.options);
|
||||
|
||||
if (hasTaggingCapabilities(savedDashboard)) {
|
||||
savedDashboard.setTags(appState.tags);
|
||||
}
|
||||
|
||||
savedDashboard.timeFrom = savedDashboard.timeRestore
|
||||
? FilterUtils.convertTimeToUTCString(timeFilter.getTime().from)
|
||||
: undefined;
|
||||
|
|
|
@ -31,6 +31,7 @@ exports[`after fetch hideWriteControls 1`] = `
|
|||
/>
|
||||
</div>
|
||||
}
|
||||
searchFilters={Array []}
|
||||
tableColumns={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -133,6 +134,7 @@ exports[`after fetch initialFilter 1`] = `
|
|||
/>
|
||||
</div>
|
||||
}
|
||||
searchFilters={Array []}
|
||||
tableColumns={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -235,6 +237,7 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `
|
|||
/>
|
||||
</div>
|
||||
}
|
||||
searchFilters={Array []}
|
||||
tableColumns={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -337,6 +340,7 @@ exports[`after fetch renders table rows 1`] = `
|
|||
/>
|
||||
</div>
|
||||
}
|
||||
searchFilters={Array []}
|
||||
tableColumns={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -439,6 +443,7 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
|
|||
/>
|
||||
</div>
|
||||
}
|
||||
searchFilters={Array []}
|
||||
tableColumns={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -540,6 +545,7 @@ exports[`renders empty page in before initial fetch to avoid flickering 1`] = `
|
|||
/>
|
||||
</div>
|
||||
}
|
||||
searchFilters={Array []}
|
||||
tableColumns={
|
||||
Array [
|
||||
Object {
|
||||
|
|
|
@ -63,6 +63,11 @@ export class DashboardListing extends React.Component {
|
|||
})}
|
||||
toastNotifications={this.props.core.notifications.toasts}
|
||||
uiSettings={this.props.core.uiSettings}
|
||||
searchFilters={
|
||||
this.props.taggingApi
|
||||
? [this.props.taggingApi.ui.getSearchBarFilter({ useName: true })]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
@ -150,6 +155,8 @@ export class DashboardListing extends React.Component {
|
|||
}
|
||||
|
||||
getTableColumns() {
|
||||
const { taggingApi } = this.props;
|
||||
|
||||
const tableColumns = [
|
||||
{
|
||||
field: 'title',
|
||||
|
@ -174,6 +181,7 @@ export class DashboardListing extends React.Component {
|
|||
dataType: 'string',
|
||||
sortable: true,
|
||||
},
|
||||
...(taggingApi ? [taggingApi.ui.getTableColumnDefinition()] : []),
|
||||
];
|
||||
return tableColumns;
|
||||
}
|
||||
|
@ -189,6 +197,7 @@ DashboardListing.propTypes = {
|
|||
hideWriteControls: PropTypes.bool.isRequired,
|
||||
initialFilter: PropTypes.string,
|
||||
initialPageSize: PropTypes.number.isRequired,
|
||||
taggingApi: PropTypes.object,
|
||||
};
|
||||
|
||||
DashboardListing.defaultProps = {
|
||||
|
|
|
@ -9,4 +9,5 @@
|
|||
hide-write-controls="hideWriteControls"
|
||||
initial-filter="initialFilter"
|
||||
initial-page-size="initialPageSize"
|
||||
tagging-api="taggingApi"
|
||||
></dashboard-listing>
|
||||
|
|
|
@ -21,11 +21,13 @@ import React, { Fragment } from 'react';
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiFormRow, EuiTextArea, EuiSwitch } from '@elastic/eui';
|
||||
|
||||
import type { SavedObjectsTaggingApi } from '../../../../saved_objects_tagging_oss/public';
|
||||
import { SavedObjectSaveModal } from '../../../../saved_objects/public';
|
||||
|
||||
interface SaveOptions {
|
||||
export interface SaveOptions {
|
||||
newTitle: string;
|
||||
newDescription: string;
|
||||
newTags?: string[];
|
||||
newCopyOnSave: boolean;
|
||||
newTimeRestore: boolean;
|
||||
isTitleDuplicateConfirmed: boolean;
|
||||
|
@ -33,23 +35,19 @@ interface SaveOptions {
|
|||
}
|
||||
|
||||
interface Props {
|
||||
onSave: ({
|
||||
newTitle,
|
||||
newDescription,
|
||||
newCopyOnSave,
|
||||
newTimeRestore,
|
||||
isTitleDuplicateConfirmed,
|
||||
onTitleDuplicate,
|
||||
}: SaveOptions) => void;
|
||||
onSave: (options: SaveOptions) => void;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
description: string;
|
||||
tags?: string[];
|
||||
timeRestore: boolean;
|
||||
showCopyOnSave: boolean;
|
||||
savedObjectsTagging?: SavedObjectsTaggingApi;
|
||||
}
|
||||
|
||||
interface State {
|
||||
description: string;
|
||||
tags: string[];
|
||||
timeRestore: boolean;
|
||||
}
|
||||
|
||||
|
@ -57,6 +55,7 @@ export class DashboardSaveModal extends React.Component<Props, State> {
|
|||
state: State = {
|
||||
description: this.props.description,
|
||||
timeRestore: this.props.timeRestore,
|
||||
tags: this.props.tags ?? [],
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
|
@ -81,6 +80,7 @@ export class DashboardSaveModal extends React.Component<Props, State> {
|
|||
newTimeRestore: this.state.timeRestore,
|
||||
isTitleDuplicateConfirmed,
|
||||
onTitleDuplicate,
|
||||
newTags: this.state.tags,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -97,6 +97,18 @@ export class DashboardSaveModal extends React.Component<Props, State> {
|
|||
};
|
||||
|
||||
renderDashboardSaveOptions() {
|
||||
const { savedObjectsTagging } = this.props;
|
||||
const tagSelector = savedObjectsTagging ? (
|
||||
<savedObjectsTagging.ui.components.SavedObjectSaveModalTagSelector
|
||||
initialSelection={this.state.tags}
|
||||
onTagsSelected={(tags) => {
|
||||
this.setState({
|
||||
tags,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
|
@ -114,6 +126,8 @@ export class DashboardSaveModal extends React.Component<Props, State> {
|
|||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{tagSelector}
|
||||
|
||||
<EuiFormRow
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
|
|
|
@ -63,6 +63,7 @@ import {
|
|||
KibanaLegacyStart,
|
||||
} from '../../kibana_legacy/public';
|
||||
import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../../plugins/home/public';
|
||||
import type { SavedObjectTaggingOssPluginStart } from '../../saved_objects_tagging_oss/public';
|
||||
import { DEFAULT_APP_CATEGORIES } from '../../../core/public';
|
||||
|
||||
import {
|
||||
|
@ -135,6 +136,7 @@ interface StartDependencies {
|
|||
share?: SharePluginStart;
|
||||
uiActions: UiActionsStart;
|
||||
savedObjects: SavedObjectsStart;
|
||||
savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart;
|
||||
}
|
||||
|
||||
export type DashboardSetup = void;
|
||||
|
@ -299,6 +301,7 @@ export class DashboardPlugin
|
|||
kibanaLegacy: { dashboardConfig },
|
||||
urlForwarding: { navigateToDefaultApp, navigateToLegacyKibanaUrl },
|
||||
savedObjects,
|
||||
savedObjectsTaggingOss,
|
||||
} = pluginsStart;
|
||||
|
||||
const deps: RenderDeps = {
|
||||
|
@ -327,6 +330,7 @@ export class DashboardPlugin
|
|||
scopedHistory: () => this.currentHistory!,
|
||||
setHeaderActionMenu: params.setHeaderActionMenu,
|
||||
savedObjects,
|
||||
savedObjectsTagging: savedObjectsTaggingOss?.getTaggingApi(),
|
||||
restorePreviousUrl,
|
||||
};
|
||||
// make sure the index pattern list is up to date
|
||||
|
|
|
@ -81,6 +81,7 @@ export interface DashboardAppState {
|
|||
fullScreenMode: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
timeRestore: boolean;
|
||||
options: {
|
||||
hidePanelTitles: boolean;
|
||||
|
|
|
@ -895,7 +895,7 @@ export class Plugin implements Plugin_2<PluginSetup, PluginStart, DataPluginSetu
|
|||
fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise<import("../common").FieldFormatsRegistry>;
|
||||
};
|
||||
indexPatterns: {
|
||||
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "update" | "find" | "get" | "delete" | "errors" | "create" | "bulkCreate" | "checkConflicts" | "bulkGet" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate">) => Promise<import("../public").IndexPatternsService>;
|
||||
indexPatternsServiceFactory: (savedObjectsClient: Pick<import("../../../core/server").SavedObjectsClient, "update" | "find" | "get" | "delete" | "errors" | "create" | "bulkCreate" | "checkConflicts" | "bulkGet" | "addToNamespaces" | "deleteFromNamespaces" | "bulkUpdate" | "removeReferencesTo">) => Promise<import("../public").IndexPatternsService>;
|
||||
};
|
||||
search: ISearchStart<import("./search").IEsSearchRequest, import("./search").IEsSearchResponse<any>>;
|
||||
};
|
||||
|
|
|
@ -36,16 +36,13 @@ import {
|
|||
EuiConfirmModal,
|
||||
EuiCallOut,
|
||||
EuiBasicTableColumn,
|
||||
EuiTableActionsColumnType,
|
||||
SearchFilterConfig,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { HttpFetchError, ToastsStart } from 'kibana/public';
|
||||
import { toMountPoint } from '../util';
|
||||
|
||||
interface Column {
|
||||
name: string;
|
||||
width?: string;
|
||||
actions?: object[];
|
||||
}
|
||||
|
||||
interface Item {
|
||||
id?: string;
|
||||
}
|
||||
|
@ -61,8 +58,7 @@ export interface TableListViewProps {
|
|||
initialFilter: string;
|
||||
initialPageSize: number;
|
||||
noItemsFragment: JSX.Element;
|
||||
// update possible column types to something like (FieldDataColumn | ComputedColumn | ActionsColumn)[] when they have been added to EUI
|
||||
tableColumns: Column[];
|
||||
tableColumns: Array<EuiBasicTableColumn<any>>;
|
||||
tableListTitle: string;
|
||||
toastNotifications: ToastsStart;
|
||||
/**
|
||||
|
@ -70,6 +66,7 @@ export interface TableListViewProps {
|
|||
* If the table is not empty, this component renders its own h1 element using the same id.
|
||||
*/
|
||||
headingId?: string;
|
||||
searchFilters?: SearchFilterConfig[];
|
||||
}
|
||||
|
||||
export interface TableListViewState {
|
||||
|
@ -402,6 +399,8 @@ class TableListView extends React.Component<TableListViewProps, TableListViewSta
|
|||
}
|
||||
|
||||
renderTable() {
|
||||
const { searchFilters } = this.props;
|
||||
|
||||
const selection = this.props.deleteItems
|
||||
? {
|
||||
onSelectionChange: (obj: Item[]) => {
|
||||
|
@ -414,7 +413,7 @@ class TableListView extends React.Component<TableListViewProps, TableListViewSta
|
|||
}
|
||||
: undefined;
|
||||
|
||||
const actions = [
|
||||
const actions: EuiTableActionsColumnType<any>['actions'] = [
|
||||
{
|
||||
name: i18n.translate('kibana-react.tableListView.listing.table.editActionName', {
|
||||
defaultMessage: 'Edit',
|
||||
|
@ -439,6 +438,7 @@ class TableListView extends React.Component<TableListViewProps, TableListViewSta
|
|||
box: {
|
||||
incremental: true,
|
||||
},
|
||||
filters: searchFilters ?? [],
|
||||
};
|
||||
|
||||
const columns = this.props.tableColumns.slice();
|
||||
|
@ -463,7 +463,7 @@ class TableListView extends React.Component<TableListViewProps, TableListViewSta
|
|||
<EuiInMemoryTable
|
||||
itemId="id"
|
||||
items={this.state.items}
|
||||
columns={(columns as unknown) as Array<EuiBasicTableColumn<object>>} // EuiBasicTableColumn is stricter than Column
|
||||
columns={columns}
|
||||
pagination={this.pagination}
|
||||
loading={this.state.isFetchingItems}
|
||||
message={noItemsMessage}
|
||||
|
|
|
@ -23,6 +23,7 @@ export {
|
|||
OnSaveProps,
|
||||
SavedObjectSaveModal,
|
||||
SavedObjectSaveModalOrigin,
|
||||
OriginSaveModalProps,
|
||||
SaveModalState,
|
||||
SaveResult,
|
||||
showSaveModal,
|
||||
|
@ -30,12 +31,16 @@ export {
|
|||
export { getSavedObjectFinder, SavedObjectFinderUi, SavedObjectMetaData } from './finder';
|
||||
export {
|
||||
SavedObjectLoader,
|
||||
SavedObjectLoaderFindOptions,
|
||||
checkForDuplicateTitle,
|
||||
saveWithConfirmation,
|
||||
isErrorNonFatal,
|
||||
SavedObjectDecorator,
|
||||
SavedObjectDecoratorFactory,
|
||||
SavedObjectDecoratorConfig,
|
||||
} from './saved_object';
|
||||
export { SavedObjectSaveOpts, SavedObjectKibanaServices, SavedObject } from './types';
|
||||
export { SavedObjectSaveOpts, SavedObject, SavedObjectConfig } from './types';
|
||||
export { PER_PAGE_SETTING, LISTING_LIMIT_SETTING } from '../common';
|
||||
export { SavedObjectsStart } from './plugin';
|
||||
export { SavedObjectsStart, SavedObjectSetup } from './plugin';
|
||||
|
||||
export const plugin = () => new SavedObjectsPublicPlugin();
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { SavedObjectsStart } from './plugin';
|
||||
import { SavedObjectsStart, SavedObjectSetup } from './plugin';
|
||||
|
||||
const createStartContract = (): SavedObjectsStart => {
|
||||
return {
|
||||
|
@ -29,6 +29,13 @@ const createStartContract = (): SavedObjectsStart => {
|
|||
};
|
||||
};
|
||||
|
||||
const createSetupContract = (): jest.Mocked<SavedObjectSetup> => {
|
||||
return {
|
||||
registerDecorator: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
export const savedObjectsPluginMock = {
|
||||
createStartContract,
|
||||
createSetupContract,
|
||||
};
|
||||
|
|
|
@ -20,11 +20,19 @@
|
|||
import { CoreStart, Plugin } from 'src/core/public';
|
||||
|
||||
import './index.scss';
|
||||
import { createSavedObjectClass } from './saved_object';
|
||||
import {
|
||||
createSavedObjectClass,
|
||||
SavedObjectDecoratorRegistry,
|
||||
SavedObjectDecoratorConfig,
|
||||
} from './saved_object';
|
||||
import { DataPublicPluginStart } from '../../data/public';
|
||||
import { PER_PAGE_SETTING, LISTING_LIMIT_SETTING } from '../common';
|
||||
import { SavedObject } from './types';
|
||||
|
||||
export interface SavedObjectSetup {
|
||||
registerDecorator: (config: SavedObjectDecoratorConfig<any>) => void;
|
||||
}
|
||||
|
||||
export interface SavedObjectsStart {
|
||||
SavedObjectClass: new (raw: Record<string, any>) => SavedObject;
|
||||
settings: {
|
||||
|
@ -38,17 +46,26 @@ export interface SavedObjectsStartDeps {
|
|||
}
|
||||
|
||||
export class SavedObjectsPublicPlugin
|
||||
implements Plugin<void, SavedObjectsStart, object, SavedObjectsStartDeps> {
|
||||
public setup() {}
|
||||
implements Plugin<SavedObjectSetup, SavedObjectsStart, object, SavedObjectsStartDeps> {
|
||||
private decoratorRegistry = new SavedObjectDecoratorRegistry();
|
||||
|
||||
public setup(): SavedObjectSetup {
|
||||
return {
|
||||
registerDecorator: (config) => this.decoratorRegistry.register(config),
|
||||
};
|
||||
}
|
||||
public start(core: CoreStart, { data }: SavedObjectsStartDeps) {
|
||||
return {
|
||||
SavedObjectClass: createSavedObjectClass({
|
||||
indexPatterns: data.indexPatterns,
|
||||
savedObjectsClient: core.savedObjects.client,
|
||||
search: data.search,
|
||||
chrome: core.chrome,
|
||||
overlays: core.overlays,
|
||||
}),
|
||||
SavedObjectClass: createSavedObjectClass(
|
||||
{
|
||||
indexPatterns: data.indexPatterns,
|
||||
savedObjectsClient: core.savedObjects.client,
|
||||
search: data.search,
|
||||
chrome: core.chrome,
|
||||
overlays: core.overlays,
|
||||
},
|
||||
this.decoratorRegistry
|
||||
),
|
||||
settings: {
|
||||
getPerPage: () => core.uiSettings.get(PER_PAGE_SETTING),
|
||||
getListingLimit: () => core.uiSettings.get(LISTING_LIMIT_SETTING),
|
||||
|
|
|
@ -18,5 +18,5 @@
|
|||
*/
|
||||
|
||||
export { SavedObjectSaveModal, OnSaveProps, SaveModalState } from './saved_object_save_modal';
|
||||
export { SavedObjectSaveModalOrigin } from './saved_object_save_modal_origin';
|
||||
export { SavedObjectSaveModalOrigin, OriginSaveModalProps } from './saved_object_save_modal_origin';
|
||||
export { showSaveModal, SaveResult } from './show_saved_object_save_modal';
|
||||
|
|
|
@ -30,7 +30,7 @@ interface SaveModalDocumentInfo {
|
|||
description?: string;
|
||||
}
|
||||
|
||||
interface OriginSaveModalProps {
|
||||
export interface OriginSaveModalProps {
|
||||
originatingApp?: string;
|
||||
getAppNameFromId?: (appId: string) => string | undefined;
|
||||
originatingAppName?: string;
|
||||
|
@ -38,6 +38,7 @@ interface OriginSaveModalProps {
|
|||
documentInfo: SaveModalDocumentInfo;
|
||||
objectType: string;
|
||||
onClose: () => void;
|
||||
options?: React.ReactNode | ((state: SaveModalState) => React.ReactNode);
|
||||
onSave: (props: OnSaveProps & { returnToOrigin: boolean }) => void;
|
||||
}
|
||||
|
||||
|
@ -53,8 +54,11 @@ export function SavedObjectSaveModalOrigin(props: OriginSaveModalProps) {
|
|||
});
|
||||
|
||||
const getReturnToOriginSwitch = (state: SaveModalState) => {
|
||||
const sourceOptions =
|
||||
typeof props.options === 'function' ? props.options(state) : props.options;
|
||||
|
||||
if (!props.originatingApp) {
|
||||
return;
|
||||
return sourceOptions;
|
||||
}
|
||||
const origin = props.getAppNameFromId
|
||||
? props.getAppNameFromId(props.originatingApp) || props.originatingApp
|
||||
|
@ -67,6 +71,7 @@ export function SavedObjectSaveModalOrigin(props: OriginSaveModalProps) {
|
|||
const originVerb = !documentInfo.id || state.copyOnSave ? addLabel : returnLabel;
|
||||
return (
|
||||
<Fragment>
|
||||
{sourceOptions}
|
||||
<EuiFormRow>
|
||||
<EuiSwitch
|
||||
data-test-subj="returnToOriginModeSwitch"
|
||||
|
@ -89,6 +94,7 @@ export function SavedObjectSaveModalOrigin(props: OriginSaveModalProps) {
|
|||
);
|
||||
} else {
|
||||
setReturnToOriginMode(false);
|
||||
return sourceOptions;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export {
|
||||
ISavedObjectDecoratorRegistry,
|
||||
SavedObjectDecoratorRegistry,
|
||||
SavedObjectDecoratorConfig,
|
||||
} from './registry';
|
||||
export { SavedObjectDecorator, SavedObjectDecoratorFactory } from './types';
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ISavedObjectDecoratorRegistry } from './registry';
|
||||
|
||||
const createRegistryMock = () => {
|
||||
const mock: jest.Mocked<ISavedObjectDecoratorRegistry> = {
|
||||
register: jest.fn(),
|
||||
getOrderedDecorators: jest.fn(),
|
||||
};
|
||||
|
||||
mock.getOrderedDecorators.mockReturnValue([]);
|
||||
|
||||
return mock;
|
||||
};
|
||||
|
||||
export const savedObjectsDecoratorRegistryMock = {
|
||||
create: createRegistryMock,
|
||||
};
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SavedObjectDecoratorRegistry } from './registry';
|
||||
|
||||
const mockDecorator = (id: string = 'foo') => {
|
||||
return {
|
||||
getId: () => id,
|
||||
decorateConfig: () => undefined,
|
||||
decorateObject: () => undefined,
|
||||
};
|
||||
};
|
||||
|
||||
describe('SavedObjectDecoratorRegistry', () => {
|
||||
let registry: SavedObjectDecoratorRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new SavedObjectDecoratorRegistry();
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('allow to register a decorator', () => {
|
||||
expect(() => {
|
||||
registry.register({
|
||||
id: 'foo',
|
||||
priority: 9000,
|
||||
factory: () => mockDecorator(),
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('throws when trying to register the same id twice', () => {
|
||||
registry.register({
|
||||
id: 'foo',
|
||||
priority: 9000,
|
||||
factory: () => mockDecorator(),
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
registry.register({
|
||||
id: 'foo',
|
||||
priority: 42,
|
||||
factory: () => mockDecorator(),
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"A decorator is already registered for id foo"`);
|
||||
});
|
||||
|
||||
it('throws when trying to register multiple decorators with the same priority', () => {
|
||||
registry.register({
|
||||
id: 'foo',
|
||||
priority: 100,
|
||||
factory: () => mockDecorator(),
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
registry.register({
|
||||
id: 'bar',
|
||||
priority: 100,
|
||||
factory: () => mockDecorator(),
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"A decorator is already registered for priority 100"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOrderedDecorators', () => {
|
||||
it('returns the decorators in correct order', () => {
|
||||
registry.register({
|
||||
id: 'A',
|
||||
priority: 1000,
|
||||
factory: () => mockDecorator('A'),
|
||||
});
|
||||
registry.register({
|
||||
id: 'B',
|
||||
priority: 100,
|
||||
factory: () => mockDecorator('B'),
|
||||
});
|
||||
registry.register({
|
||||
id: 'C',
|
||||
priority: 2000,
|
||||
factory: () => mockDecorator('C'),
|
||||
});
|
||||
|
||||
const decorators = registry.getOrderedDecorators({} as any);
|
||||
expect(decorators.map((d) => d.getId())).toEqual(['B', 'A', 'C']);
|
||||
});
|
||||
|
||||
it('invoke the decorators factory with the provided services', () => {
|
||||
const services = Symbol('services');
|
||||
|
||||
const decorator = {
|
||||
id: 'foo',
|
||||
priority: 9000,
|
||||
factory: jest.fn(),
|
||||
};
|
||||
registry.register(decorator);
|
||||
registry.getOrderedDecorators(services as any);
|
||||
|
||||
expect(decorator.factory).toHaveBeenCalledTimes(1);
|
||||
expect(decorator.factory).toHaveBeenCalledWith(services);
|
||||
});
|
||||
|
||||
it('invoke the factory each time the method is called', () => {
|
||||
const services = Symbol('services');
|
||||
|
||||
const decorator = {
|
||||
id: 'foo',
|
||||
priority: 9000,
|
||||
factory: jest.fn(),
|
||||
};
|
||||
registry.register(decorator);
|
||||
registry.getOrderedDecorators(services as any);
|
||||
|
||||
expect(decorator.factory).toHaveBeenCalledTimes(1);
|
||||
|
||||
registry.getOrderedDecorators(services as any);
|
||||
|
||||
expect(decorator.factory).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { SavedObjectDecoratorFactory } from './types';
|
||||
import { SavedObjectKibanaServices, SavedObject } from '../../types';
|
||||
|
||||
export interface SavedObjectDecoratorConfig<T extends SavedObject = SavedObject> {
|
||||
/**
|
||||
* The id of the decorator
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Highest priority will be called **last**
|
||||
* (the decoration will be at the highest level)
|
||||
*/
|
||||
priority: number;
|
||||
/**
|
||||
* The factory to use to create the decorator
|
||||
*/
|
||||
factory: SavedObjectDecoratorFactory<T>;
|
||||
}
|
||||
|
||||
export type ISavedObjectDecoratorRegistry = PublicMethodsOf<SavedObjectDecoratorRegistry>;
|
||||
|
||||
export class SavedObjectDecoratorRegistry {
|
||||
private readonly registry = new Map<string, SavedObjectDecoratorConfig<any>>();
|
||||
|
||||
public register(config: SavedObjectDecoratorConfig<any>) {
|
||||
if (this.registry.has(config.id)) {
|
||||
throw new Error(`A decorator is already registered for id ${config.id}`);
|
||||
}
|
||||
if ([...this.registry.values()].find(({ priority }) => priority === config.priority)) {
|
||||
throw new Error(`A decorator is already registered for priority ${config.priority}`);
|
||||
}
|
||||
this.registry.set(config.id, config);
|
||||
}
|
||||
|
||||
public getOrderedDecorators(services: SavedObjectKibanaServices) {
|
||||
return [...this.registry.values()]
|
||||
.sort((a, b) => a.priority - b.priority)
|
||||
.map(({ factory }) => factory(services));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SavedObject, SavedObjectKibanaServices, SavedObjectConfig } from '../../types';
|
||||
|
||||
export interface SavedObjectDecorator<T extends SavedObject = SavedObject> {
|
||||
/**
|
||||
* Id of the decorator
|
||||
*/
|
||||
getId(): string;
|
||||
|
||||
/**
|
||||
* Decorate the saved object provided config. This can be used to enhance or alter the object's provided
|
||||
* configuration.
|
||||
*/
|
||||
decorateConfig: (config: SavedObjectConfig) => void;
|
||||
/**
|
||||
* Decorate the saved object instance. Can be used to add additional methods to it.
|
||||
*
|
||||
* @remarks This will be called before the internal constructor of the object, meaning that
|
||||
* wrapping existing methods is not possible (and is not a desired pattern).
|
||||
*/
|
||||
decorateObject: (object: T) => void;
|
||||
}
|
||||
|
||||
export type SavedObjectDecoratorFactory<T extends SavedObject = SavedObject> = (
|
||||
services: SavedObjectKibanaServices
|
||||
) => SavedObjectDecorator<T>;
|
|
@ -36,12 +36,11 @@ export async function applyESResp(
|
|||
config: SavedObjectConfig,
|
||||
dependencies: SavedObjectKibanaServices
|
||||
) {
|
||||
const mapping = expandShorthand(config.mapping);
|
||||
const esType = config.type || '';
|
||||
const mapping = expandShorthand(config.mapping ?? {});
|
||||
const savedObjectType = config.type || '';
|
||||
savedObject._source = _.cloneDeep(resp._source);
|
||||
const injectReferences = config.injectReferences;
|
||||
if (typeof resp.found === 'boolean' && !resp.found) {
|
||||
throw new SavedObjectNotFound(esType, savedObject.id || '');
|
||||
throw new SavedObjectNotFound(savedObjectType, savedObject.id || '');
|
||||
}
|
||||
|
||||
const meta = resp._source.kibanaSavedObjectMeta || {};
|
||||
|
@ -101,6 +100,7 @@ export async function applyESResp(
|
|||
}
|
||||
}
|
||||
|
||||
const injectReferences = config.injectReferences;
|
||||
if (injectReferences && resp.references && resp.references.length > 0) {
|
||||
injectReferences(savedObject, resp.references);
|
||||
}
|
||||
|
|
|
@ -30,12 +30,27 @@ import {
|
|||
} from '../../types';
|
||||
import { applyESResp } from './apply_es_resp';
|
||||
import { saveSavedObject } from './save_saved_object';
|
||||
import { SavedObjectDecorator } from '../decorators';
|
||||
|
||||
const applyDecorators = (
|
||||
object: SavedObject,
|
||||
config: SavedObjectConfig,
|
||||
decorators: SavedObjectDecorator[]
|
||||
) => {
|
||||
decorators.forEach((decorator) => {
|
||||
decorator.decorateConfig(config);
|
||||
decorator.decorateObject(object);
|
||||
});
|
||||
};
|
||||
|
||||
export function buildSavedObject(
|
||||
savedObject: SavedObject,
|
||||
config: SavedObjectConfig = {},
|
||||
services: SavedObjectKibanaServices
|
||||
config: SavedObjectConfig,
|
||||
services: SavedObjectKibanaServices,
|
||||
decorators: SavedObjectDecorator[] = []
|
||||
) {
|
||||
applyDecorators(savedObject, config, decorators);
|
||||
|
||||
const { indexPatterns, savedObjectsClient } = services;
|
||||
// type name for this object, used as the ES-type
|
||||
const esType = config.type || '';
|
||||
|
|
|
@ -23,7 +23,7 @@ import { expandShorthand } from './field_mapping';
|
|||
|
||||
export function serializeSavedObject(savedObject: SavedObject, config: SavedObjectConfig) {
|
||||
// mapping definition for the fields that this object will expose
|
||||
const mapping = expandShorthand(config.mapping);
|
||||
const mapping = expandShorthand(config.mapping ?? {});
|
||||
const attributes = {} as Record<string, any>;
|
||||
const references = [];
|
||||
|
||||
|
|
|
@ -18,7 +18,13 @@
|
|||
*/
|
||||
|
||||
export { createSavedObjectClass } from './saved_object';
|
||||
export { SavedObjectLoader } from './saved_object_loader';
|
||||
export { SavedObjectLoader, SavedObjectLoaderFindOptions } from './saved_object_loader';
|
||||
export { checkForDuplicateTitle } from './helpers/check_for_duplicate_title';
|
||||
export { saveWithConfirmation } from './helpers/save_with_confirmation';
|
||||
export { isErrorNonFatal } from './helpers/save_saved_object';
|
||||
export {
|
||||
SavedObjectDecoratorRegistry,
|
||||
SavedObjectDecoratorFactory,
|
||||
SavedObjectDecorator,
|
||||
SavedObjectDecoratorConfig,
|
||||
} from './decorators';
|
||||
|
|
|
@ -25,12 +25,14 @@ import {
|
|||
SavedObjectKibanaServices,
|
||||
SavedObjectSaveOpts,
|
||||
} from '../types';
|
||||
import { SavedObjectDecorator } from './decorators';
|
||||
|
||||
import { coreMock } from '../../../../core/public/mocks';
|
||||
import { dataPluginMock, createSearchSourceMock } from '../../../../plugins/data/public/mocks';
|
||||
import { getStubIndexPattern, StubIndexPattern } from '../../../../plugins/data/public/test_utils';
|
||||
import { SavedObjectAttributes, SimpleSavedObject } from 'kibana/public';
|
||||
import { IIndexPattern } from '../../../data/common/index_patterns';
|
||||
import { savedObjectsDecoratorRegistryMock } from './decorators/registry.mock';
|
||||
|
||||
const getConfig = (cfg: any) => cfg;
|
||||
|
||||
|
@ -39,6 +41,7 @@ describe('Saved Object', () => {
|
|||
const dataStartMock = dataPluginMock.createStartContract();
|
||||
const saveOptionsMock = {} as SavedObjectSaveOpts;
|
||||
const savedObjectsClientStub = startMock.savedObjects.client;
|
||||
let decoratorRegistry: ReturnType<typeof savedObjectsDecoratorRegistryMock.create>;
|
||||
|
||||
let SavedObjectClass: new (config: SavedObjectConfig) => SavedObject;
|
||||
|
||||
|
@ -94,26 +97,116 @@ describe('Saved Object', () => {
|
|||
* @returns {Promise<SavedObject>} A promise that resolves with an instance of
|
||||
* SavedObject
|
||||
*/
|
||||
function createInitializedSavedObject(config: SavedObjectConfig = {}) {
|
||||
function createInitializedSavedObject(config: SavedObjectConfig = { type: 'dashboard' }) {
|
||||
const savedObject = new SavedObjectClass(config);
|
||||
savedObject.title = 'my saved object';
|
||||
|
||||
return savedObject.init!();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
SavedObjectClass = createSavedObjectClass(({
|
||||
savedObjectsClient: savedObjectsClientStub,
|
||||
indexPatterns: dataStartMock.indexPatterns,
|
||||
search: {
|
||||
...dataStartMock.search,
|
||||
searchSource: {
|
||||
...dataStartMock.search.searchSource,
|
||||
create: createSearchSourceMock,
|
||||
createEmpty: createSearchSourceMock,
|
||||
const initSavedObjectClass = () => {
|
||||
SavedObjectClass = createSavedObjectClass(
|
||||
({
|
||||
savedObjectsClient: savedObjectsClientStub,
|
||||
indexPatterns: dataStartMock.indexPatterns,
|
||||
search: {
|
||||
...dataStartMock.search,
|
||||
searchSource: {
|
||||
...dataStartMock.search.searchSource,
|
||||
create: createSearchSourceMock,
|
||||
createEmpty: createSearchSourceMock,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown) as SavedObjectKibanaServices);
|
||||
} as unknown) as SavedObjectKibanaServices,
|
||||
decoratorRegistry
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
decoratorRegistry = savedObjectsDecoratorRegistryMock.create();
|
||||
initSavedObjectClass();
|
||||
});
|
||||
|
||||
describe('decorators', () => {
|
||||
it('calls the decorators during construct', () => {
|
||||
const decorA = {
|
||||
getId: () => 'A',
|
||||
decorateConfig: jest.fn(),
|
||||
decorateObject: jest.fn(),
|
||||
};
|
||||
const decorB = {
|
||||
getId: () => 'B',
|
||||
decorateConfig: jest.fn(),
|
||||
decorateObject: jest.fn(),
|
||||
};
|
||||
|
||||
decoratorRegistry.getOrderedDecorators.mockReturnValue([decorA, decorB]);
|
||||
|
||||
initSavedObjectClass();
|
||||
createInitializedSavedObject();
|
||||
|
||||
expect(decorA.decorateConfig).toHaveBeenCalledTimes(1);
|
||||
expect(decorA.decorateObject).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls the decorators in correct order', () => {
|
||||
const decorA = {
|
||||
getId: () => 'A',
|
||||
decorateConfig: jest.fn(),
|
||||
decorateObject: jest.fn(),
|
||||
};
|
||||
const decorB = {
|
||||
getId: () => 'B',
|
||||
decorateConfig: jest.fn(),
|
||||
decorateObject: jest.fn(),
|
||||
};
|
||||
|
||||
decoratorRegistry.getOrderedDecorators.mockReturnValue([decorA, decorB]);
|
||||
|
||||
initSavedObjectClass();
|
||||
createInitializedSavedObject();
|
||||
|
||||
expect(decorA.decorateConfig.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
decorB.decorateConfig.mock.invocationCallOrder[0]
|
||||
);
|
||||
expect(decorA.decorateObject.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
decorB.decorateObject.mock.invocationCallOrder[0]
|
||||
);
|
||||
});
|
||||
|
||||
it('passes the mutated config and object down the decorator chain', () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const newMappingValue = 'string';
|
||||
const newObjectMethod = jest.fn();
|
||||
|
||||
const decorA: SavedObjectDecorator = {
|
||||
getId: () => 'A',
|
||||
decorateConfig: (config) => {
|
||||
config.mapping = {
|
||||
...config.mapping,
|
||||
addedFromA: newMappingValue,
|
||||
};
|
||||
},
|
||||
decorateObject: (object) => {
|
||||
(object as any).newMethod = newObjectMethod;
|
||||
},
|
||||
};
|
||||
const decorB: SavedObjectDecorator = {
|
||||
getId: () => 'B',
|
||||
decorateConfig: (config) => {
|
||||
expect(config.mapping!.addedFromA).toBe(newMappingValue);
|
||||
},
|
||||
decorateObject: (object) => {
|
||||
expect((object as any).newMethod).toBe(newObjectMethod);
|
||||
},
|
||||
};
|
||||
|
||||
decoratorRegistry.getOrderedDecorators.mockReturnValue([decorA, decorB]);
|
||||
|
||||
initSavedObjectClass();
|
||||
createInitializedSavedObject();
|
||||
});
|
||||
});
|
||||
|
||||
describe('save', () => {
|
||||
|
@ -578,13 +671,16 @@ describe('Saved Object', () => {
|
|||
});
|
||||
|
||||
it('passes references to search source parsing function', async () => {
|
||||
SavedObjectClass = createSavedObjectClass(({
|
||||
savedObjectsClient: savedObjectsClientStub,
|
||||
indexPatterns: dataStartMock.indexPatterns,
|
||||
search: {
|
||||
...dataStartMock.search,
|
||||
},
|
||||
} as unknown) as SavedObjectKibanaServices);
|
||||
SavedObjectClass = createSavedObjectClass(
|
||||
({
|
||||
savedObjectsClient: savedObjectsClientStub,
|
||||
indexPatterns: dataStartMock.indexPatterns,
|
||||
search: {
|
||||
...dataStartMock.search,
|
||||
},
|
||||
} as unknown) as SavedObjectKibanaServices,
|
||||
decoratorRegistry
|
||||
);
|
||||
const savedObject = new SavedObjectClass({ type: 'dashboard', searchSource: true });
|
||||
return savedObject.init!().then(async () => {
|
||||
const searchSourceJSON = JSON.stringify({
|
||||
|
|
|
@ -28,9 +28,13 @@
|
|||
* service and the saved object api.
|
||||
*/
|
||||
import { SavedObject, SavedObjectConfig, SavedObjectKibanaServices } from '../types';
|
||||
import { ISavedObjectDecoratorRegistry } from './decorators';
|
||||
import { buildSavedObject } from './helpers/build_saved_object';
|
||||
|
||||
export function createSavedObjectClass(services: SavedObjectKibanaServices) {
|
||||
export function createSavedObjectClass(
|
||||
services: SavedObjectKibanaServices,
|
||||
decoratorRegistry: ISavedObjectDecoratorRegistry
|
||||
) {
|
||||
/**
|
||||
* The SavedObject class is a base class for saved objects loaded from the server and
|
||||
* provides additional functionality besides loading/saving/deleting/etc.
|
||||
|
@ -44,7 +48,7 @@ export function createSavedObjectClass(services: SavedObjectKibanaServices) {
|
|||
constructor(config: SavedObjectConfig = {}) {
|
||||
// @ts-ignore
|
||||
const self: SavedObject = this;
|
||||
buildSavedObject(self, config, services);
|
||||
buildSavedObject(self, config, services, decoratorRegistry.getOrderedDecorators(services));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,10 +16,21 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { SavedObjectsClientContract, SavedObjectsFindOptions } from 'kibana/public';
|
||||
import {
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsFindOptions,
|
||||
SavedObjectsFindOptionsReference,
|
||||
SavedObjectReference,
|
||||
} from 'kibana/public';
|
||||
import { SavedObject } from '../types';
|
||||
import { StringUtils } from './helpers/string_utils';
|
||||
|
||||
export interface SavedObjectLoaderFindOptions {
|
||||
size?: number;
|
||||
fields?: string[];
|
||||
hasReference?: SavedObjectsFindOptionsReference[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The SavedObjectLoader class provides some convenience functions
|
||||
* to load and save one kind of saved objects (specified in the constructor).
|
||||
|
@ -80,15 +91,21 @@ export class SavedObjectLoader {
|
|||
}
|
||||
|
||||
/**
|
||||
* Updates source to contain an id and url field, and returns the updated
|
||||
* Updates source to contain an id, url and references fields, and returns the updated
|
||||
* source object.
|
||||
* @param source
|
||||
* @param id
|
||||
* @param references
|
||||
* @returns {source} The modified source object, with an id and url field.
|
||||
*/
|
||||
mapHitSource(source: Record<string, unknown>, id: string) {
|
||||
mapHitSource(
|
||||
source: Record<string, unknown>,
|
||||
id: string,
|
||||
references: SavedObjectReference[] = []
|
||||
) {
|
||||
source.id = id;
|
||||
source.url = this.urlFor(id);
|
||||
source.references = references;
|
||||
return source;
|
||||
}
|
||||
|
||||
|
@ -98,8 +115,16 @@ export class SavedObjectLoader {
|
|||
* @param hit
|
||||
* @returns {hit.attributes} The modified hit.attributes object, with an id and url field.
|
||||
*/
|
||||
mapSavedObjectApiHits(hit: { attributes: Record<string, unknown>; id: string }) {
|
||||
return this.mapHitSource(hit.attributes, hit.id);
|
||||
mapSavedObjectApiHits({
|
||||
attributes,
|
||||
id,
|
||||
references = [],
|
||||
}: {
|
||||
attributes: Record<string, unknown>;
|
||||
id: string;
|
||||
references?: SavedObjectReference[];
|
||||
}) {
|
||||
return this.mapHitSource(attributes, id, references);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -111,7 +136,10 @@ export class SavedObjectLoader {
|
|||
* @param fields
|
||||
* @returns {Promise}
|
||||
*/
|
||||
findAll(search: string = '', size: number = 100, fields?: string[]) {
|
||||
private findAll(
|
||||
search: string = '',
|
||||
{ size = 100, fields, hasReference }: SavedObjectLoaderFindOptions
|
||||
) {
|
||||
return this.savedObjectsClient
|
||||
.find<Record<string, unknown>>({
|
||||
type: this.lowercaseType,
|
||||
|
@ -121,6 +149,7 @@ export class SavedObjectLoader {
|
|||
searchFields: ['title^3', 'description'],
|
||||
defaultSearchOperator: 'AND',
|
||||
fields,
|
||||
hasReference,
|
||||
} as SavedObjectsFindOptions)
|
||||
.then((resp) => {
|
||||
return {
|
||||
|
@ -130,8 +159,15 @@ export class SavedObjectLoader {
|
|||
});
|
||||
}
|
||||
|
||||
find(search: string = '', size: number = 100) {
|
||||
return this.findAll(search, size).then((resp) => {
|
||||
find(search: string = '', sizeOrOptions: number | SavedObjectLoaderFindOptions = 100) {
|
||||
const options: SavedObjectLoaderFindOptions =
|
||||
typeof sizeOrOptions === 'number'
|
||||
? {
|
||||
size: sizeOrOptions,
|
||||
}
|
||||
: sizeOrOptions;
|
||||
|
||||
return this.findAll(search, options).then((resp) => {
|
||||
return {
|
||||
total: resp.total,
|
||||
hits: resp.hits.filter((savedObject) => !savedObject.error),
|
||||
|
|
|
@ -79,22 +79,21 @@ export interface SavedObjectKibanaServices {
|
|||
overlays: OverlayStart;
|
||||
}
|
||||
|
||||
export interface SavedObjectAttributesAndRefs {
|
||||
attributes: SavedObjectAttributes;
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
|
||||
export interface SavedObjectConfig {
|
||||
// is only used by visualize
|
||||
afterESResp?: (savedObject: SavedObject) => Promise<SavedObject>;
|
||||
defaults?: any;
|
||||
extractReferences?: (opts: {
|
||||
attributes: SavedObjectAttributes;
|
||||
references: SavedObjectReference[];
|
||||
}) => {
|
||||
attributes: SavedObjectAttributes;
|
||||
references: SavedObjectReference[];
|
||||
};
|
||||
extractReferences?: (opts: SavedObjectAttributesAndRefs) => SavedObjectAttributesAndRefs;
|
||||
injectReferences?: <T extends SavedObject>(object: T, references: SavedObjectReference[]) => void;
|
||||
id?: string;
|
||||
init?: () => void;
|
||||
indexPattern?: IIndexPattern;
|
||||
injectReferences?: any;
|
||||
mapping?: any;
|
||||
mapping?: Record<string, any>;
|
||||
migrationVersion?: Record<string, any>;
|
||||
path?: string;
|
||||
searchSource?: ISearchSource | boolean;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["management", "data"],
|
||||
"optionalPlugins": ["dashboard", "visualizations", "discover", "home"],
|
||||
"optionalPlugins": ["dashboard", "visualizations", "discover", "home", "savedObjectsTaggingOss"],
|
||||
"extraPublicDirs": ["public/lib"],
|
||||
"requiredBundles": ["kibanaReact", "home"]
|
||||
}
|
||||
|
|
|
@ -17,18 +17,26 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { HttpStart } from 'src/core/public';
|
||||
import { HttpStart, SavedObjectsFindOptionsReference } from 'src/core/public';
|
||||
|
||||
export async function fetchExportByTypeAndSearch(
|
||||
http: HttpStart,
|
||||
types: string[],
|
||||
search: string | undefined,
|
||||
includeReferencesDeep: boolean = false
|
||||
): Promise<Blob> {
|
||||
export async function fetchExportByTypeAndSearch({
|
||||
http,
|
||||
search,
|
||||
types,
|
||||
references,
|
||||
includeReferencesDeep = false,
|
||||
}: {
|
||||
http: HttpStart;
|
||||
types: string[];
|
||||
search?: string;
|
||||
references?: SavedObjectsFindOptionsReference[];
|
||||
includeReferencesDeep?: boolean;
|
||||
}): Promise<Blob> {
|
||||
return http.post('/api/saved_objects/_export', {
|
||||
body: JSON.stringify({
|
||||
type: types,
|
||||
search,
|
||||
hasReference: references,
|
||||
includeReferencesDeep,
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -35,7 +35,12 @@ export async function findObjects(
|
|||
const response = await http.get<Record<string, any>>(
|
||||
'/api/kibana/management/saved_objects/_find',
|
||||
{
|
||||
query: findOptions as Record<string, any>,
|
||||
query: {
|
||||
...findOptions,
|
||||
hasReference: findOptions.hasReference
|
||||
? JSON.stringify(findOptions.hasReference)
|
||||
: undefined,
|
||||
} as Record<string, any>,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -17,15 +17,21 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { HttpStart } from 'src/core/public';
|
||||
import { HttpStart, SavedObjectsFindOptionsReference } from 'src/core/public';
|
||||
|
||||
export async function getSavedObjectCounts(
|
||||
http: HttpStart,
|
||||
typesToInclude: string[],
|
||||
searchString?: string
|
||||
): Promise<Record<string, number>> {
|
||||
export async function getSavedObjectCounts({
|
||||
http,
|
||||
searchString,
|
||||
typesToInclude,
|
||||
references,
|
||||
}: {
|
||||
http: HttpStart;
|
||||
typesToInclude: string[];
|
||||
searchString?: string;
|
||||
references?: SavedObjectsFindOptionsReference[];
|
||||
}): Promise<Record<string, number>> {
|
||||
return await http.post<Record<string, number>>(
|
||||
`/api/kibana/management/saved_objects/scroll/counts`,
|
||||
{ body: JSON.stringify({ typesToInclude, searchString }) }
|
||||
{ body: JSON.stringify({ typesToInclude, searchString, references }) }
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { taggingApiMock } from '../../../saved_objects_tagging_oss/public/mocks';
|
||||
import { getTagFindReferences } from './get_tag_references';
|
||||
|
||||
const tagNameToRef = (name: string) => ({
|
||||
type: 'tag',
|
||||
id: `id-of-${name}`,
|
||||
});
|
||||
|
||||
describe('getTagFindReferences', () => {
|
||||
let taggingApi: ReturnType<typeof taggingApiMock.create>;
|
||||
const selectedTags = ['name-1', 'name-2'];
|
||||
|
||||
beforeEach(() => {
|
||||
taggingApi = taggingApiMock.create();
|
||||
taggingApi.ui.convertNameToReference.mockImplementation(tagNameToRef);
|
||||
});
|
||||
|
||||
it('returns undefined if `taggingApi` is not provided', () => {
|
||||
expect(getTagFindReferences({ selectedTags })).toBeUndefined();
|
||||
});
|
||||
it('returns undefined if `selectedTags` is not provided', () => {
|
||||
expect(getTagFindReferences({ taggingApi })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns the references for given names', () => {
|
||||
expect(getTagFindReferences({ selectedTags, taggingApi })).toEqual(
|
||||
selectedTags.map(tagNameToRef)
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores any unknown tag name', () => {
|
||||
taggingApi.ui.convertNameToReference.mockImplementation((name) => {
|
||||
if (name === 'name-1') {
|
||||
return undefined;
|
||||
}
|
||||
return tagNameToRef(name);
|
||||
});
|
||||
|
||||
expect(getTagFindReferences({ selectedTags, taggingApi })).toEqual([tagNameToRef('name-2')]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SavedObjectsFindOptionsReference } from 'kibana/server';
|
||||
import { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public';
|
||||
|
||||
export const getTagFindReferences = ({
|
||||
selectedTags,
|
||||
taggingApi,
|
||||
}: {
|
||||
selectedTags?: string[];
|
||||
taggingApi?: SavedObjectsTaggingApi;
|
||||
}): SavedObjectsFindOptionsReference[] | undefined => {
|
||||
if (taggingApi && selectedTags) {
|
||||
const references: SavedObjectsFindOptionsReference[] = [];
|
||||
selectedTags.forEach((tagName) => {
|
||||
const ref = taggingApi.ui.convertNameToReference(tagName);
|
||||
if (ref) {
|
||||
references.push(ref);
|
||||
}
|
||||
});
|
||||
return references;
|
||||
}
|
||||
return undefined;
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue