mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* add saved object export details in ndjson response Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co> * update core doc Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co> * exclude export details for space copy Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co> * fixing tests Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co> * display warning instead of success if export contains missing refs Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co> * nits/typo Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co> * properly updates api integration tests Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co> * fix typings Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co> * add test on objects_table component Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co> * remove added translations from jp/cn bundles Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co> * restoring line feeds Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co> * improve doc and user alert message Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co> * restoring line feeds on server.api.md Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co> * warning test label Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>
This commit is contained in:
parent
168f4d6223
commit
05c76f293c
34 changed files with 1085 additions and 456 deletions
|
@ -23,12 +23,29 @@ experimental[] Retrieve a set of saved objects that you want to import into {kib
|
|||
`includeReferencesDeep`::
|
||||
(Optional, boolean) Includes all of the referenced objects in the exported objects.
|
||||
|
||||
`excludeExportDetails`::
|
||||
(Optional, boolean) Do not add export details entry at the end of the stream.
|
||||
|
||||
TIP: You must include `type` or `objects` in the request body.
|
||||
|
||||
[[saved-objects-api-export-request-response-body]]
|
||||
==== Response body
|
||||
|
||||
The format of the response body includes newline delimited JSON.
|
||||
The format of the response body is newline delimited JSON. Each exported object is exported as a valid JSON record and separated by the newline character '\n'.
|
||||
|
||||
When `excludeExportDetails=false` (the default) we append an export result details record at the end of the file after all the saved object records. The export result details object has the following format:
|
||||
|
||||
[source,json]
|
||||
--------------------------------------------------
|
||||
{
|
||||
"exportedCount": 27,
|
||||
"missingRefCount": 2,
|
||||
"missingReferences": [
|
||||
{ "id": "an-id", "type": "visualisation"},
|
||||
{ "id": "another-id", "type": "index-pattern"}
|
||||
]
|
||||
}
|
||||
--------------------------------------------------
|
||||
|
||||
[[export-objects-api-create-request-codes]]
|
||||
==== Response code
|
||||
|
@ -50,6 +67,18 @@ POST api/saved_objects/_export
|
|||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
Export all index pattern saved objects and exclude the export summary from the stream:
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
POST api/saved_objects/_export
|
||||
{
|
||||
"type": "index-pattern",
|
||||
"excludeExportDetails": true
|
||||
}
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
Export a specific saved object:
|
||||
|
||||
[source,js]
|
||||
|
@ -65,3 +94,20 @@ POST api/saved_objects/_export
|
|||
}
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
Export a specific saved object and it's related objects :
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
POST api/saved_objects/_export
|
||||
{
|
||||
"objects": [
|
||||
{
|
||||
"type": "dashboard",
|
||||
"id": "be3733a0-9efe-11e7-acb3-3dab96693fab"
|
||||
}
|
||||
],
|
||||
"includeReferencesDeep": true
|
||||
}
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
|
|
@ -92,6 +92,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [SavedObjectsClientWrapperOptions](./kibana-plugin-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. |
|
||||
| [SavedObjectsCreateOptions](./kibana-plugin-server.savedobjectscreateoptions.md) | |
|
||||
| [SavedObjectsExportOptions](./kibana-plugin-server.savedobjectsexportoptions.md) | Options controlling the export operation. |
|
||||
| [SavedObjectsExportResultDetails](./kibana-plugin-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry |
|
||||
| [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) | |
|
||||
| [SavedObjectsFindResponse](./kibana-plugin-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. |
|
||||
| [SavedObjectsImportConflictError](./kibana-plugin-server.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsExportOptions](./kibana-plugin-server.savedobjectsexportoptions.md) > [excludeExportDetails](./kibana-plugin-server.savedobjectsexportoptions.excludeexportdetails.md)
|
||||
|
||||
## SavedObjectsExportOptions.excludeExportDetails property
|
||||
|
||||
flag to not append [export details](./kibana-plugin-server.savedobjectsexportresultdetails.md) to the end of the export stream.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
excludeExportDetails?: boolean;
|
||||
```
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
## SavedObjectsExportOptions.includeReferencesDeep property
|
||||
|
||||
flag to also include all related saved objects in the export response.
|
||||
flag to also include all related saved objects in the export stream.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
|
|
|
@ -16,8 +16,9 @@ export interface SavedObjectsExportOptions
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [excludeExportDetails](./kibana-plugin-server.savedobjectsexportoptions.excludeexportdetails.md) | <code>boolean</code> | flag to not append [export details](./kibana-plugin-server.savedobjectsexportresultdetails.md) to the end of the export stream. |
|
||||
| [exportSizeLimit](./kibana-plugin-server.savedobjectsexportoptions.exportsizelimit.md) | <code>number</code> | the maximum number of objects to export. |
|
||||
| [includeReferencesDeep](./kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md) | <code>boolean</code> | flag to also include all related saved objects in the export response. |
|
||||
| [includeReferencesDeep](./kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md) | <code>boolean</code> | flag to also include all related saved objects in the export stream. |
|
||||
| [namespace](./kibana-plugin-server.savedobjectsexportoptions.namespace.md) | <code>string</code> | optional namespace to override the namespace used by the savedObjectsClient. |
|
||||
| [objects](./kibana-plugin-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. |
|
||||
| [savedObjectsClient](./kibana-plugin-server.savedobjectsexportoptions.savedobjectsclient.md) | <code>SavedObjectsClientContract</code> | an instance of the SavedObjectsClient. |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsExportResultDetails](./kibana-plugin-server.savedobjectsexportresultdetails.md) > [exportedCount](./kibana-plugin-server.savedobjectsexportresultdetails.exportedcount.md)
|
||||
|
||||
## SavedObjectsExportResultDetails.exportedCount property
|
||||
|
||||
number of successfully exported objects
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
exportedCount: number;
|
||||
```
|
|
@ -0,0 +1,22 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsExportResultDetails](./kibana-plugin-server.savedobjectsexportresultdetails.md)
|
||||
|
||||
## SavedObjectsExportResultDetails interface
|
||||
|
||||
Structure of the export result details entry
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SavedObjectsExportResultDetails
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [exportedCount](./kibana-plugin-server.savedobjectsexportresultdetails.exportedcount.md) | <code>number</code> | number of successfully exported objects |
|
||||
| [missingRefCount](./kibana-plugin-server.savedobjectsexportresultdetails.missingrefcount.md) | <code>number</code> | number of missing references |
|
||||
| [missingReferences](./kibana-plugin-server.savedobjectsexportresultdetails.missingreferences.md) | <code>Array<{</code><br/><code> id: string;</code><br/><code> type: string;</code><br/><code> }></code> | missing references details |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsExportResultDetails](./kibana-plugin-server.savedobjectsexportresultdetails.md) > [missingRefCount](./kibana-plugin-server.savedobjectsexportresultdetails.missingrefcount.md)
|
||||
|
||||
## SavedObjectsExportResultDetails.missingRefCount property
|
||||
|
||||
number of missing references
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
missingRefCount: number;
|
||||
```
|
|
@ -0,0 +1,16 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsExportResultDetails](./kibana-plugin-server.savedobjectsexportresultdetails.md) > [missingReferences](./kibana-plugin-server.savedobjectsexportresultdetails.missingreferences.md)
|
||||
|
||||
## SavedObjectsExportResultDetails.missingReferences property
|
||||
|
||||
missing references details
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
missingReferences: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
}>;
|
||||
```
|
|
@ -144,6 +144,7 @@ export {
|
|||
SavedObjectsCreateOptions,
|
||||
SavedObjectsErrorHelpers,
|
||||
SavedObjectsExportOptions,
|
||||
SavedObjectsExportResultDetails,
|
||||
SavedObjectsFindResponse,
|
||||
SavedObjectsImportConflictError,
|
||||
SavedObjectsImportError,
|
||||
|
|
|
@ -74,27 +74,32 @@ describe('getSortedObjectsForExport()', () => {
|
|||
const response = await readStreamToCompletion(exportStream);
|
||||
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "name",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "name",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"exportedCount": 2,
|
||||
"missingRefCount": 0,
|
||||
"missingReferences": Array [],
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(savedObjectsClient.find).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
|
@ -122,6 +127,65 @@ describe('getSortedObjectsForExport()', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
test('exclude export details if option is specified', async () => {
|
||||
savedObjectsClient.find.mockResolvedValueOnce({
|
||||
total: 2,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '2',
|
||||
type: 'search',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
per_page: 1,
|
||||
page: 0,
|
||||
});
|
||||
const exportStream = await getSortedObjectsForExport({
|
||||
savedObjectsClient,
|
||||
exportSizeLimit: 500,
|
||||
types: ['index-pattern', 'search'],
|
||||
excludeExportDetails: true,
|
||||
});
|
||||
|
||||
const response = await readStreamToCompletion(exportStream);
|
||||
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "name",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('exports selected types with search string when present', async () => {
|
||||
savedObjectsClient.find.mockResolvedValueOnce({
|
||||
total: 2,
|
||||
|
@ -158,27 +222,32 @@ describe('getSortedObjectsForExport()', () => {
|
|||
const response = await readStreamToCompletion(exportStream);
|
||||
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "name",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "name",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"exportedCount": 2,
|
||||
"missingRefCount": 0,
|
||||
"missingReferences": Array [],
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(savedObjectsClient.find).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
|
@ -242,27 +311,32 @@ describe('getSortedObjectsForExport()', () => {
|
|||
const response = await readStreamToCompletion(exportStream);
|
||||
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "name",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "name",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"exportedCount": 2,
|
||||
"missingRefCount": 0,
|
||||
"missingReferences": Array [],
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(savedObjectsClient.find).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
|
@ -365,27 +439,32 @@ describe('getSortedObjectsForExport()', () => {
|
|||
});
|
||||
const response = await readStreamToCompletion(exportStream);
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "name",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "name",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"exportedCount": 2,
|
||||
"missingRefCount": 0,
|
||||
"missingReferences": Array [],
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
|
@ -456,27 +535,32 @@ describe('getSortedObjectsForExport()', () => {
|
|||
});
|
||||
const response = await readStreamToCompletion(exportStream);
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "name",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "name",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"exportedCount": 2,
|
||||
"missingRefCount": 0,
|
||||
"missingReferences": Array [],
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
import Boom from 'boom';
|
||||
import { createListStream } from '../../../../legacy/utils/streams';
|
||||
import { SavedObjectsClientContract } from '../types';
|
||||
import { injectNestedDependencies } from './inject_nested_depdendencies';
|
||||
import { fetchNestedDependencies } from './inject_nested_depdendencies';
|
||||
import { sortObjects } from './sort_objects';
|
||||
|
||||
/**
|
||||
|
@ -43,12 +43,32 @@ export interface SavedObjectsExportOptions {
|
|||
savedObjectsClient: SavedObjectsClientContract;
|
||||
/** the maximum number of objects to export. */
|
||||
exportSizeLimit: number;
|
||||
/** flag to also include all related saved objects in the export response. */
|
||||
/** flag to also include all related saved objects in the export stream. */
|
||||
includeReferencesDeep?: boolean;
|
||||
/** flag to not append {@link SavedObjectsExportResultDetails | export details} to the end of the export stream. */
|
||||
excludeExportDetails?: boolean;
|
||||
/** optional namespace to override the namespace used by the savedObjectsClient. */
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Structure of the export result details entry
|
||||
* @public
|
||||
*/
|
||||
export interface SavedObjectsExportResultDetails {
|
||||
/** number of successfully exported objects */
|
||||
exportedCount: number;
|
||||
/** number of missing references */
|
||||
missingRefCount: number;
|
||||
/** missing references details */
|
||||
missingReferences: Array<{
|
||||
/** the missing reference id. */
|
||||
id: string;
|
||||
/** the missing reference type. */
|
||||
type: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
async function fetchObjectsToExport({
|
||||
objects,
|
||||
types,
|
||||
|
@ -106,9 +126,10 @@ export async function getSortedObjectsForExport({
|
|||
savedObjectsClient,
|
||||
exportSizeLimit,
|
||||
includeReferencesDeep = false,
|
||||
excludeExportDetails = false,
|
||||
namespace,
|
||||
}: SavedObjectsExportOptions) {
|
||||
const objectsToExport = await fetchObjectsToExport({
|
||||
const rootObjects = await fetchObjectsToExport({
|
||||
types,
|
||||
objects,
|
||||
search,
|
||||
|
@ -116,12 +137,18 @@ export async function getSortedObjectsForExport({
|
|||
exportSizeLimit,
|
||||
namespace,
|
||||
});
|
||||
|
||||
const exportedObjects = sortObjects(
|
||||
includeReferencesDeep
|
||||
? await injectNestedDependencies(objectsToExport, savedObjectsClient, namespace)
|
||||
: objectsToExport
|
||||
);
|
||||
|
||||
return createListStream(exportedObjects);
|
||||
let exportedObjects = [...rootObjects];
|
||||
let missingReferences: SavedObjectsExportResultDetails['missingReferences'] = [];
|
||||
if (includeReferencesDeep) {
|
||||
const fetchResult = await fetchNestedDependencies(rootObjects, savedObjectsClient, namespace);
|
||||
exportedObjects = fetchResult.objects;
|
||||
missingReferences = fetchResult.missingRefs;
|
||||
}
|
||||
exportedObjects = sortObjects(exportedObjects);
|
||||
const exportDetails: SavedObjectsExportResultDetails = {
|
||||
exportedCount: exportedObjects.length,
|
||||
missingRefCount: missingReferences.length,
|
||||
missingReferences,
|
||||
};
|
||||
return createListStream([...exportedObjects, ...(excludeExportDetails ? [] : [exportDetails])]);
|
||||
}
|
||||
|
|
|
@ -20,4 +20,5 @@
|
|||
export {
|
||||
getSortedObjectsForExport,
|
||||
SavedObjectsExportOptions,
|
||||
SavedObjectsExportResultDetails,
|
||||
} from './get_sorted_objects_for_export';
|
||||
|
|
|
@ -18,10 +18,7 @@
|
|||
*/
|
||||
|
||||
import { SavedObject } from '../types';
|
||||
import {
|
||||
getObjectReferencesToFetch,
|
||||
injectNestedDependencies,
|
||||
} from './inject_nested_depdendencies';
|
||||
import { getObjectReferencesToFetch, fetchNestedDependencies } from './inject_nested_depdendencies';
|
||||
|
||||
describe('getObjectReferencesToFetch()', () => {
|
||||
test('works with no saved objects', () => {
|
||||
|
@ -110,7 +107,7 @@ describe('getObjectReferencesToFetch()', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('injectNestedDependencies', () => {
|
||||
describe('fetchNestedDependencies', () => {
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
find: jest.fn(),
|
||||
|
@ -135,16 +132,19 @@ describe('injectNestedDependencies', () => {
|
|||
references: [],
|
||||
},
|
||||
];
|
||||
const result = await injectNestedDependencies(savedObjects, savedObjectsClient);
|
||||
const result = await fetchNestedDependencies(savedObjects, savedObjectsClient);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"missingRefs": Array [],
|
||||
"objects": Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
|
@ -169,28 +169,31 @@ describe('injectNestedDependencies', () => {
|
|||
],
|
||||
},
|
||||
];
|
||||
const result = await injectNestedDependencies(savedObjects, savedObjectsClient);
|
||||
const result = await fetchNestedDependencies(savedObjects, savedObjectsClient);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref_0",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"missingRefs": Array [],
|
||||
"objects": Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref_0",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
|
@ -219,28 +222,31 @@ describe('injectNestedDependencies', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
const result = await injectNestedDependencies(savedObjects, savedObjectsClient);
|
||||
const result = await fetchNestedDependencies(savedObjects, savedObjectsClient);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref_0",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"missingRefs": Array [],
|
||||
"objects": Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref_0",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
|
@ -337,69 +343,72 @@ describe('injectNestedDependencies', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
const result = await injectNestedDependencies(savedObjects, savedObjectsClient);
|
||||
const result = await fetchNestedDependencies(savedObjects, savedObjectsClient);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "5",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "4",
|
||||
"name": "panel_0",
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"id": "3",
|
||||
"name": "panel_1",
|
||||
"type": "visualization",
|
||||
},
|
||||
],
|
||||
"type": "dashboard",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "4",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "2",
|
||||
"name": "ref_0",
|
||||
"type": "search",
|
||||
},
|
||||
],
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "3",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref_0",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref_0",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"missingRefs": Array [],
|
||||
"objects": Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "5",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "4",
|
||||
"name": "panel_0",
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"id": "3",
|
||||
"name": "panel_1",
|
||||
"type": "visualization",
|
||||
},
|
||||
],
|
||||
"type": "dashboard",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "4",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "2",
|
||||
"name": "ref_0",
|
||||
"type": "search",
|
||||
},
|
||||
],
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "3",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref_0",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref_0",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
|
@ -449,10 +458,10 @@ describe('injectNestedDependencies', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
test('throws error when bulkGet returns an error', async () => {
|
||||
test('returns list of missing references', async () => {
|
||||
const savedObjects = [
|
||||
{
|
||||
id: '2',
|
||||
id: '1',
|
||||
type: 'search',
|
||||
attributes: {},
|
||||
references: [
|
||||
|
@ -461,6 +470,11 @@ describe('injectNestedDependencies', () => {
|
|||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
{
|
||||
name: 'ref_1',
|
||||
type: 'index-pattern',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
@ -474,11 +488,50 @@ describe('injectNestedDependencies', () => {
|
|||
message: 'Not found',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
await expect(
|
||||
injectNestedDependencies(savedObjects, savedObjectsClient)
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Bad Request"`);
|
||||
const result = await fetchNestedDependencies(savedObjects, savedObjectsClient);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"missingRefs": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"objects": Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref_0",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"id": "2",
|
||||
"name": "ref_1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test(`doesn't deal with circular dependencies`, async () => {
|
||||
|
@ -512,34 +565,37 @@ describe('injectNestedDependencies', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
const result = await injectNestedDependencies(savedObjects, savedObjectsClient);
|
||||
const result = await fetchNestedDependencies(savedObjects, savedObjectsClient);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref_0",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "2",
|
||||
"name": "ref_0",
|
||||
"type": "search",
|
||||
},
|
||||
],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"missingRefs": Array [],
|
||||
"objects": Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref_0",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "2",
|
||||
"name": "ref_0",
|
||||
"type": "search",
|
||||
},
|
||||
],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
|
|
|
@ -17,47 +17,43 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { SavedObject, SavedObjectsClientContract } from '../types';
|
||||
|
||||
export function getObjectReferencesToFetch(savedObjectsMap: Map<string, SavedObject>) {
|
||||
const objectsToFetch = new Map<string, { type: string; id: string }>();
|
||||
for (const savedObject of savedObjectsMap.values()) {
|
||||
for (const { type, id } of savedObject.references || []) {
|
||||
if (!savedObjectsMap.has(`${type}:${id}`)) {
|
||||
objectsToFetch.set(`${type}:${id}`, { type, id });
|
||||
for (const ref of savedObject.references || []) {
|
||||
if (!savedObjectsMap.has(objKey(ref))) {
|
||||
objectsToFetch.set(objKey(ref), { type: ref.type, id: ref.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...objectsToFetch.values()];
|
||||
}
|
||||
|
||||
export async function injectNestedDependencies(
|
||||
export async function fetchNestedDependencies(
|
||||
savedObjects: SavedObject[],
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
namespace?: string
|
||||
) {
|
||||
const savedObjectsMap = new Map<string, SavedObject>();
|
||||
for (const savedObject of savedObjects) {
|
||||
savedObjectsMap.set(`${savedObject.type}:${savedObject.id}`, savedObject);
|
||||
savedObjectsMap.set(objKey(savedObject), savedObject);
|
||||
}
|
||||
let objectsToFetch = getObjectReferencesToFetch(savedObjectsMap);
|
||||
while (objectsToFetch.length > 0) {
|
||||
const bulkGetResponse = await savedObjectsClient.bulkGet(objectsToFetch, { namespace });
|
||||
// Check for errors
|
||||
const erroredObjects = bulkGetResponse.saved_objects.filter(obj => !!obj.error);
|
||||
if (erroredObjects.length) {
|
||||
const err = Boom.badRequest();
|
||||
err.output.payload.attributes = {
|
||||
objects: erroredObjects,
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
// Push to array result
|
||||
for (const savedObject of bulkGetResponse.saved_objects) {
|
||||
savedObjectsMap.set(`${savedObject.type}:${savedObject.id}`, savedObject);
|
||||
savedObjectsMap.set(objKey(savedObject), savedObject);
|
||||
}
|
||||
objectsToFetch = getObjectReferencesToFetch(savedObjectsMap);
|
||||
}
|
||||
return [...savedObjectsMap.values()];
|
||||
const allObjects = [...savedObjectsMap.values()];
|
||||
return {
|
||||
objects: allObjects.filter(obj => !obj.error),
|
||||
missingRefs: allObjects.filter(obj => !!obj.error).map(obj => ({ type: obj.type, id: obj.id })),
|
||||
};
|
||||
}
|
||||
|
||||
const objKey = (obj: { type: string; id: string }) => `${obj.type}:${obj.id}`;
|
||||
|
|
|
@ -25,7 +25,11 @@ export { SavedObjectsManagement } from './management';
|
|||
|
||||
export * from './import';
|
||||
|
||||
export { getSortedObjectsForExport, SavedObjectsExportOptions } from './export';
|
||||
export {
|
||||
getSortedObjectsForExport,
|
||||
SavedObjectsExportOptions,
|
||||
SavedObjectsExportResultDetails,
|
||||
} from './export';
|
||||
|
||||
export { SavedObjectsSerializer, RawDoc as SavedObjectsRawDoc } from './serialization';
|
||||
|
||||
|
|
|
@ -1263,6 +1263,7 @@ export class SavedObjectsErrorHelpers {
|
|||
|
||||
// @public
|
||||
export interface SavedObjectsExportOptions {
|
||||
excludeExportDetails?: boolean;
|
||||
exportSizeLimit: number;
|
||||
includeReferencesDeep?: boolean;
|
||||
namespace?: string;
|
||||
|
@ -1275,6 +1276,16 @@ export interface SavedObjectsExportOptions {
|
|||
types?: string[];
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface SavedObjectsExportResultDetails {
|
||||
exportedCount: number;
|
||||
missingRefCount: number;
|
||||
missingReferences: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions {
|
||||
// (undocumented)
|
||||
|
|
|
@ -25,6 +25,7 @@ import { ObjectsTable, POSSIBLE_TYPES } from '../objects_table';
|
|||
import { Flyout } from '../components/flyout/';
|
||||
import { Relationships } from '../components/relationships/';
|
||||
import { findObjects } from '../../../lib';
|
||||
import { extractExportDetails } from '../../../lib/extract_export_details';
|
||||
|
||||
jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
|
||||
|
||||
|
@ -49,6 +50,10 @@ jest.mock('../../../lib/fetch_export_by_type_and_search', () => ({
|
|||
fetchExportByTypeAndSearch: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../lib/extract_export_details', () => ({
|
||||
extractExportDetails: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../lib/get_saved_object_counts', () => ({
|
||||
getSavedObjectCounts: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
|
@ -190,12 +195,14 @@ beforeEach(() => {
|
|||
|
||||
let addDangerMock;
|
||||
let addSuccessMock;
|
||||
let addWarningMock;
|
||||
|
||||
describe('ObjectsTable', () => {
|
||||
beforeEach(() => {
|
||||
defaultProps.savedObjectsClient.find.mockClear();
|
||||
extractExportDetails.mockReset();
|
||||
// mock _.debounce to fire immediately with no internal timer
|
||||
require('lodash').debounce = function (func) {
|
||||
require('lodash').debounce = func => {
|
||||
function debounced(...args) {
|
||||
return func.apply(this, args);
|
||||
}
|
||||
|
@ -203,9 +210,11 @@ describe('ObjectsTable', () => {
|
|||
};
|
||||
addDangerMock = jest.fn();
|
||||
addSuccessMock = jest.fn();
|
||||
addWarningMock = jest.fn();
|
||||
require('ui/notify').toastNotifications = {
|
||||
addDanger: addDangerMock,
|
||||
addSuccess: addSuccessMock,
|
||||
addWarning: addWarningMock,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -280,6 +289,55 @@ describe('ObjectsTable', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should display a warning is export contains missing references', async () => {
|
||||
const mockSelectedSavedObjects = [
|
||||
{ id: '1', type: 'index-pattern' },
|
||||
{ id: '3', type: 'dashboard' },
|
||||
];
|
||||
|
||||
const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({
|
||||
_id: obj.id,
|
||||
_type: obj._type,
|
||||
_source: {},
|
||||
}));
|
||||
|
||||
const mockSavedObjectsClient = {
|
||||
...defaultProps.savedObjectsClient,
|
||||
bulkGet: jest.fn().mockImplementation(() => ({
|
||||
savedObjects: mockSavedObjects,
|
||||
})),
|
||||
};
|
||||
|
||||
const { fetchExportObjects } = require('../../../lib/fetch_export_objects');
|
||||
extractExportDetails.mockImplementation(() => ({
|
||||
exportedCount: 2,
|
||||
missingRefCount: 1,
|
||||
missingReferences: [{ id: '7', type: 'visualisation' }],
|
||||
}));
|
||||
|
||||
const component = shallowWithI18nProvider(
|
||||
<ObjectsTable {...defaultProps} savedObjectsClient={mockSavedObjectsClient} />
|
||||
);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
|
||||
// Set some as selected
|
||||
component.instance().onSelectionChanged(mockSelectedSavedObjects);
|
||||
|
||||
await component.instance().onExport(true);
|
||||
|
||||
expect(fetchExportObjects).toHaveBeenCalledWith(mockSelectedSavedObjects, true);
|
||||
expect(addWarningMock).toHaveBeenCalledWith({
|
||||
title:
|
||||
'Your file is downloading in the background. ' +
|
||||
'Some related objects could not be found. ' +
|
||||
'Please see the last line in the exported file for a list of missing objects.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow the user to choose when exporting all', async () => {
|
||||
const component = shallowWithI18nProvider(<ObjectsTable {...defaultProps} />);
|
||||
|
||||
|
@ -295,7 +353,9 @@ describe('ObjectsTable', () => {
|
|||
});
|
||||
|
||||
it('should export all', async () => {
|
||||
const { fetchExportByTypeAndSearch } = require('../../../lib/fetch_export_by_type_and_search');
|
||||
const {
|
||||
fetchExportByTypeAndSearch,
|
||||
} = require('../../../lib/fetch_export_by_type_and_search');
|
||||
const { saveAs } = require('@elastic/filesaver');
|
||||
const component = shallowWithI18nProvider(<ObjectsTable {...defaultProps} />);
|
||||
|
||||
|
@ -312,20 +372,20 @@ describe('ObjectsTable', () => {
|
|||
|
||||
expect(fetchExportByTypeAndSearch).toHaveBeenCalledWith(POSSIBLE_TYPES, undefined, true);
|
||||
expect(saveAs).toHaveBeenCalledWith(blob, 'export.ndjson');
|
||||
expect(addSuccessMock).toHaveBeenCalledWith({ title: 'Your file is downloading in the background' });
|
||||
expect(addSuccessMock).toHaveBeenCalledWith({
|
||||
title: 'Your file is downloading in the background',
|
||||
});
|
||||
});
|
||||
|
||||
it('should export all, accounting for the current search criteria', async () => {
|
||||
const { fetchExportByTypeAndSearch } = require('../../../lib/fetch_export_by_type_and_search');
|
||||
const {
|
||||
fetchExportByTypeAndSearch,
|
||||
} = require('../../../lib/fetch_export_by_type_and_search');
|
||||
const { saveAs } = require('@elastic/filesaver');
|
||||
const component = shallowWithI18nProvider(
|
||||
<ObjectsTable
|
||||
{...defaultProps}
|
||||
/>
|
||||
);
|
||||
const component = shallowWithI18nProvider(<ObjectsTable {...defaultProps} />);
|
||||
|
||||
component.instance().onQueryChange({
|
||||
query: Query.parse('test')
|
||||
query: Query.parse('test'),
|
||||
});
|
||||
|
||||
// Ensure all promises resolve
|
||||
|
|
|
@ -64,6 +64,7 @@ import {
|
|||
fetchExportByTypeAndSearch,
|
||||
findObjects,
|
||||
} from '../../lib';
|
||||
import { extractExportDetails } from '../../lib/extract_export_details';
|
||||
|
||||
export const POSSIBLE_TYPES = chrome.getInjected('importAndExportableTypes');
|
||||
|
||||
|
@ -296,32 +297,31 @@ export class ObjectsTable extends Component {
|
|||
}
|
||||
|
||||
saveAs(blob, 'export.ndjson');
|
||||
toastNotifications.addSuccess({
|
||||
title: i18n.translate('kbn.management.objects.objectsTable.export.successNotification', {
|
||||
defaultMessage: 'Your file is downloading in the background',
|
||||
}),
|
||||
});
|
||||
|
||||
const exportDetails = await extractExportDetails(blob);
|
||||
this.showExportSuccessMessage(exportDetails);
|
||||
};
|
||||
|
||||
onExportAll = async () => {
|
||||
const { exportAllSelectedOptions, isIncludeReferencesDeepChecked, activeQuery } = this.state;
|
||||
const { queryText } = parseQuery(activeQuery);
|
||||
const exportTypes = Object.entries(exportAllSelectedOptions).reduce(
|
||||
(accum, [id, selected]) => {
|
||||
if (selected) {
|
||||
accum.push(id);
|
||||
}
|
||||
return accum;
|
||||
},
|
||||
[]
|
||||
);
|
||||
const exportTypes = Object.entries(exportAllSelectedOptions).reduce((accum, [id, selected]) => {
|
||||
if (selected) {
|
||||
accum.push(id);
|
||||
}
|
||||
return accum;
|
||||
}, []);
|
||||
|
||||
let blob;
|
||||
try {
|
||||
blob = await fetchExportByTypeAndSearch(exportTypes, queryText ? `${queryText}*` : undefined, isIncludeReferencesDeepChecked);
|
||||
blob = await fetchExportByTypeAndSearch(
|
||||
exportTypes,
|
||||
queryText ? `${queryText}*` : undefined,
|
||||
isIncludeReferencesDeepChecked
|
||||
);
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('kbn.management.objects.objectsTable.exportAll.dangerNotification', {
|
||||
title: i18n.translate('kbn.management.objects.objectsTable.export.dangerNotification', {
|
||||
defaultMessage: 'Unable to generate export',
|
||||
}),
|
||||
});
|
||||
|
@ -329,14 +329,34 @@ export class ObjectsTable extends Component {
|
|||
}
|
||||
|
||||
saveAs(blob, 'export.ndjson');
|
||||
toastNotifications.addSuccess({
|
||||
title: i18n.translate('kbn.management.objects.objectsTable.exportAll.successNotification', {
|
||||
defaultMessage: 'Your file is downloading in the background',
|
||||
}),
|
||||
});
|
||||
|
||||
const exportDetails = await extractExportDetails(blob);
|
||||
this.showExportSuccessMessage(exportDetails);
|
||||
this.setState({ isShowingExportAllOptionsModal: false });
|
||||
};
|
||||
|
||||
showExportSuccessMessage = exportDetails => {
|
||||
if (exportDetails && exportDetails.missingReferences.length > 0) {
|
||||
toastNotifications.addWarning({
|
||||
title: i18n.translate(
|
||||
'kbn.management.objects.objectsTable.export.successWithMissingRefsNotification',
|
||||
{
|
||||
defaultMessage:
|
||||
'Your file is downloading in the background. ' +
|
||||
'Some related objects could not be found. ' +
|
||||
'Please see the last line in the exported file for a list of missing objects.',
|
||||
}
|
||||
),
|
||||
});
|
||||
} else {
|
||||
toastNotifications.addSuccess({
|
||||
title: i18n.translate('kbn.management.objects.objectsTable.export.successNotification', {
|
||||
defaultMessage: 'Your file is downloading in the background',
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
finishImport = () => {
|
||||
this.hideImportFlyout();
|
||||
this.fetchSavedObjects();
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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 { extractExportDetails, SavedObjectsExportResultDetails } from '../extract_export_details';
|
||||
|
||||
describe('extractExportDetails', () => {
|
||||
const objLine = (id: string, type: string) => {
|
||||
return JSON.stringify({ attributes: {}, id, references: [], type }) + '\n';
|
||||
};
|
||||
const detailsLine = (
|
||||
exported: number,
|
||||
missingRefs: SavedObjectsExportResultDetails['missingReferences'] = []
|
||||
) => {
|
||||
return (
|
||||
JSON.stringify({
|
||||
exportedCount: exported,
|
||||
missingRefCount: missingRefs.length,
|
||||
missingReferences: missingRefs,
|
||||
}) + '\n'
|
||||
);
|
||||
};
|
||||
|
||||
it('should extract the export details from the export blob', async () => {
|
||||
const exportData = new Blob(
|
||||
[
|
||||
[
|
||||
objLine('1', 'index-pattern'),
|
||||
objLine('2', 'index-pattern'),
|
||||
objLine('3', 'index-pattern'),
|
||||
detailsLine(3),
|
||||
].join(''),
|
||||
],
|
||||
{ type: 'application/ndjson', endings: 'transparent' }
|
||||
);
|
||||
const result = await extractExportDetails(exportData);
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result).toEqual({
|
||||
exportedCount: 3,
|
||||
missingRefCount: 0,
|
||||
missingReferences: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly extract the missing references', async () => {
|
||||
const exportData = new Blob(
|
||||
[
|
||||
[
|
||||
objLine('1', 'index-pattern'),
|
||||
detailsLine(1, [{ id: '2', type: 'index-pattern' }, { id: '3', type: 'index-pattern' }]),
|
||||
].join(''),
|
||||
],
|
||||
{
|
||||
type: 'application/ndjson',
|
||||
endings: 'transparent',
|
||||
}
|
||||
);
|
||||
const result = await extractExportDetails(exportData);
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result).toEqual({
|
||||
exportedCount: 1,
|
||||
missingRefCount: 2,
|
||||
missingReferences: [{ id: '2', type: 'index-pattern' }, { id: '3', type: 'index-pattern' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when the export does not contain details', async () => {
|
||||
const exportData = new Blob(
|
||||
[
|
||||
[
|
||||
objLine('1', 'index-pattern'),
|
||||
objLine('2', 'index-pattern'),
|
||||
objLine('3', 'index-pattern'),
|
||||
].join(''),
|
||||
],
|
||||
{ type: 'application/ndjson', endings: 'transparent' }
|
||||
);
|
||||
const result = await extractExportDetails(exportData);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 async function extractExportDetails(
|
||||
blob: Blob
|
||||
): Promise<SavedObjectsExportResultDetails | undefined> {
|
||||
const reader = new FileReader();
|
||||
const content = await new Promise<string>((resolve, reject) => {
|
||||
reader.addEventListener('loadend', e => {
|
||||
resolve((e as any).target.result);
|
||||
});
|
||||
reader.addEventListener('error', e => {
|
||||
reject(e);
|
||||
});
|
||||
reader.readAsText(blob, 'utf-8');
|
||||
});
|
||||
const lines = content.split('\n').filter(l => l.length > 0);
|
||||
const maybeDetails = JSON.parse(lines[lines.length - 1]);
|
||||
if (isExportDetails(maybeDetails)) {
|
||||
return maybeDetails;
|
||||
}
|
||||
}
|
||||
|
||||
export interface SavedObjectsExportResultDetails {
|
||||
exportedCount: number;
|
||||
missingRefCount: number;
|
||||
missingReferences: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
function isExportDetails(object: any): object is SavedObjectsExportResultDetails {
|
||||
return 'exportedCount' in object && 'missingRefCount' in object && 'missingReferences' in object;
|
||||
}
|
|
@ -32,3 +32,4 @@ export * from './log_legacy_import';
|
|||
export * from './process_import_response';
|
||||
export * from './get_default_title';
|
||||
export * from './find_objects';
|
||||
export * from './extract_export_details';
|
||||
|
|
|
@ -77,4 +77,30 @@ describe('createSavedObjectsStreamFromNdJson', () => {
|
|||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('filters the export details entry from the stream', async () => {
|
||||
const savedObjectsStream = createSavedObjectsStreamFromNdJson(
|
||||
new Readable({
|
||||
read() {
|
||||
this.push('{"id": "foo", "type": "foo-type"}\n');
|
||||
this.push('{"id": "bar", "type": "bar-type"}\n');
|
||||
this.push('{"exportedCount": 2, "missingRefCount": 0, "missingReferences": []}\n');
|
||||
this.push(null);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const result = await readStreamToCompletion(savedObjectsStream);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: 'foo',
|
||||
type: 'foo-type',
|
||||
},
|
||||
{
|
||||
id: 'bar',
|
||||
type: 'bar-type',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { Readable } from 'stream';
|
||||
import { SavedObject } from 'src/core/server';
|
||||
import { SavedObject, SavedObjectsExportResultDetails } from 'src/core/server';
|
||||
import { createSplitStream, createMapStream, createFilterStream } from '../../../utils/streams';
|
||||
|
||||
export function createSavedObjectsStreamFromNdJson(ndJsonStream: Readable) {
|
||||
|
@ -30,5 +30,9 @@ export function createSavedObjectsStreamFromNdJson(ndJsonStream: Readable) {
|
|||
}
|
||||
})
|
||||
)
|
||||
.pipe(createFilterStream<SavedObject>(obj => !!obj));
|
||||
.pipe(
|
||||
createFilterStream<SavedObject | SavedObjectsExportResultDetails>(
|
||||
obj => !!obj && !(obj as SavedObjectsExportResultDetails).exportedCount
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -157,6 +157,7 @@ describe('POST /api/saved_objects/_export', () => {
|
|||
"calls": Array [
|
||||
Array [
|
||||
Object {
|
||||
"excludeExportDetails": false,
|
||||
"exportSizeLimit": 10000,
|
||||
"includeReferencesDeep": true,
|
||||
"objects": undefined,
|
||||
|
|
|
@ -28,7 +28,7 @@ import {
|
|||
} from '../../../utils/streams';
|
||||
// Disable lint errors for imports from src/core/server/saved_objects until SavedObjects migration is complete
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { getSortedObjectsForExport } from '../../../../core/server/saved_objects/export';
|
||||
import { getSortedObjectsForExport } from '../../../../core/server/saved_objects';
|
||||
import { Prerequisites } from './types';
|
||||
|
||||
interface ExportRequest extends Hapi.Request {
|
||||
|
@ -43,6 +43,7 @@ interface ExportRequest extends Hapi.Request {
|
|||
}>;
|
||||
search?: string;
|
||||
includeReferencesDeep: boolean;
|
||||
excludeExportDetails: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -73,6 +74,7 @@ export const createExportRoute = (
|
|||
.optional(),
|
||||
search: Joi.string().optional(),
|
||||
includeReferencesDeep: Joi.boolean().default(false),
|
||||
excludeExportDetails: Joi.boolean().default(false),
|
||||
})
|
||||
.xor('type', 'objects')
|
||||
.nand('search', 'objects')
|
||||
|
@ -87,6 +89,7 @@ export const createExportRoute = (
|
|||
objects: request.payload.objects,
|
||||
exportSizeLimit: server.config().get('savedObjects.maxImportExportSize'),
|
||||
includeReferencesDeep: request.payload.includeReferencesDeep,
|
||||
excludeExportDetails: request.payload.excludeExportDetails,
|
||||
});
|
||||
|
||||
const docsToExport: string[] = await createPromiseFromStreams([
|
||||
|
|
|
@ -37,7 +37,30 @@ export default function ({ getService }) {
|
|||
type: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
.then(resp => {
|
||||
const objects = resp.text.split('\n').map(JSON.parse);
|
||||
expect(objects).to.have.length(4);
|
||||
expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab');
|
||||
expect(objects[0]).to.have.property('type', 'index-pattern');
|
||||
expect(objects[1]).to.have.property('id', 'dd7caf20-9efd-11e7-acb3-3dab96693fab');
|
||||
expect(objects[1]).to.have.property('type', 'visualization');
|
||||
expect(objects[2]).to.have.property('id', 'be3733a0-9efe-11e7-acb3-3dab96693fab');
|
||||
expect(objects[2]).to.have.property('type', 'dashboard');
|
||||
expect(objects[3]).to.have.property('exportedCount', 3);
|
||||
expect(objects[3]).to.have.property('missingRefCount', 0);
|
||||
expect(objects[3].missingReferences).to.have.length(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should exclude the export details if asked', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.send({
|
||||
type: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
const objects = resp.text.split('\n').map(JSON.parse);
|
||||
expect(objects).to.have.length(3);
|
||||
expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab');
|
||||
|
@ -62,15 +85,18 @@ export default function ({ getService }) {
|
|||
],
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
.then(resp => {
|
||||
const objects = resp.text.split('\n').map(JSON.parse);
|
||||
expect(objects).to.have.length(3);
|
||||
expect(objects).to.have.length(4);
|
||||
expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab');
|
||||
expect(objects[0]).to.have.property('type', 'index-pattern');
|
||||
expect(objects[1]).to.have.property('id', 'dd7caf20-9efd-11e7-acb3-3dab96693fab');
|
||||
expect(objects[1]).to.have.property('type', 'visualization');
|
||||
expect(objects[2]).to.have.property('id', 'be3733a0-9efe-11e7-acb3-3dab96693fab');
|
||||
expect(objects[2]).to.have.property('type', 'dashboard');
|
||||
expect(objects[3]).to.have.property('exportedCount', 3);
|
||||
expect(objects[3]).to.have.property('missingRefCount', 0);
|
||||
expect(objects[3].missingReferences).to.have.length(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -82,15 +108,18 @@ export default function ({ getService }) {
|
|||
type: ['dashboard'],
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
.then(resp => {
|
||||
const objects = resp.text.split('\n').map(JSON.parse);
|
||||
expect(objects).to.have.length(3);
|
||||
expect(objects).to.have.length(4);
|
||||
expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab');
|
||||
expect(objects[0]).to.have.property('type', 'index-pattern');
|
||||
expect(objects[1]).to.have.property('id', 'dd7caf20-9efd-11e7-acb3-3dab96693fab');
|
||||
expect(objects[1]).to.have.property('type', 'visualization');
|
||||
expect(objects[2]).to.have.property('id', 'be3733a0-9efe-11e7-acb3-3dab96693fab');
|
||||
expect(objects[2]).to.have.property('type', 'dashboard');
|
||||
expect(objects[3]).to.have.property('exportedCount', 3);
|
||||
expect(objects[3]).to.have.property('missingRefCount', 0);
|
||||
expect(objects[3].missingReferences).to.have.length(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -100,18 +129,21 @@ export default function ({ getService }) {
|
|||
.send({
|
||||
includeReferencesDeep: true,
|
||||
type: ['dashboard'],
|
||||
search: 'Requests*'
|
||||
search: 'Requests*',
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
.then(resp => {
|
||||
const objects = resp.text.split('\n').map(JSON.parse);
|
||||
expect(objects).to.have.length(3);
|
||||
expect(objects).to.have.length(4);
|
||||
expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab');
|
||||
expect(objects[0]).to.have.property('type', 'index-pattern');
|
||||
expect(objects[1]).to.have.property('id', 'dd7caf20-9efd-11e7-acb3-3dab96693fab');
|
||||
expect(objects[1]).to.have.property('type', 'visualization');
|
||||
expect(objects[2]).to.have.property('id', 'be3733a0-9efe-11e7-acb3-3dab96693fab');
|
||||
expect(objects[2]).to.have.property('type', 'dashboard');
|
||||
expect(objects[3]).to.have.property('exportedCount', 3);
|
||||
expect(objects[3]).to.have.property('missingRefCount', 0);
|
||||
expect(objects[3].missingReferences).to.have.length(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -127,7 +159,7 @@ export default function ({ getService }) {
|
|||
],
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
|
@ -159,12 +191,13 @@ export default function ({ getService }) {
|
|||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'child "type" fails because ["type" at position 0 fails because ' +
|
||||
message:
|
||||
'child "type" fails because ["type" at position 0 fails because ' +
|
||||
'["0" must be one of [config, dashboard, index-pattern, query, search, url, visualization]]]',
|
||||
validation: {
|
||||
source: 'payload',
|
||||
keys: ['type.0'],
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -178,12 +211,12 @@ export default function ({ getService }) {
|
|||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: '"value" must be an object',
|
||||
validation: { source: 'payload', keys: [ 'value' ] },
|
||||
validation: { source: 'payload', keys: ['value'] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -193,47 +226,55 @@ export default function ({ getService }) {
|
|||
.post('/api/saved_objects/_export')
|
||||
.send({
|
||||
type: 'dashboard',
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.headers['content-disposition']).to.eql('attachment; filename="export.ndjson"');
|
||||
.then(resp => {
|
||||
expect(resp.headers['content-disposition']).to.eql(
|
||||
'attachment; filename="export.ndjson"'
|
||||
);
|
||||
expect(resp.headers['content-type']).to.eql('application/ndjson');
|
||||
const objects = resp.text.split('\n').map(JSON.parse);
|
||||
expect(objects).to.eql([{
|
||||
attributes: {
|
||||
description: '',
|
||||
hits: 0,
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
|
||||
expect(objects).to.eql([
|
||||
{
|
||||
attributes: {
|
||||
description: '',
|
||||
hits: 0,
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON:
|
||||
objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
|
||||
},
|
||||
optionsJSON: objects[0].attributes.optionsJSON,
|
||||
panelsJSON: objects[0].attributes.panelsJSON,
|
||||
refreshInterval: {
|
||||
display: 'Off',
|
||||
pause: false,
|
||||
value: 0,
|
||||
},
|
||||
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
|
||||
timeRestore: true,
|
||||
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
|
||||
title: 'Requests',
|
||||
version: 1,
|
||||
},
|
||||
optionsJSON: objects[0].attributes.optionsJSON,
|
||||
panelsJSON: objects[0].attributes.panelsJSON,
|
||||
refreshInterval: {
|
||||
display: 'Off',
|
||||
pause: false,
|
||||
value: 0,
|
||||
},
|
||||
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
|
||||
timeRestore: true,
|
||||
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
|
||||
title: 'Requests',
|
||||
version: 1,
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
migrationVersion: objects[0].migrationVersion,
|
||||
references: [
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
name: 'panel_0',
|
||||
type: 'visualization',
|
||||
},
|
||||
],
|
||||
type: 'dashboard',
|
||||
updated_at: '2017-09-21T18:57:40.826Z',
|
||||
version: objects[0].version,
|
||||
},
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
migrationVersion: objects[0].migrationVersion,
|
||||
references: [
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
name: 'panel_0',
|
||||
type: 'visualization',
|
||||
},
|
||||
],
|
||||
type: 'dashboard',
|
||||
updated_at: '2017-09-21T18:57:40.826Z',
|
||||
version: objects[0].version,
|
||||
}]);
|
||||
]);
|
||||
expect(objects[0].migrationVersion).to.be.ok();
|
||||
expect(() => JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)).not.to.throwError();
|
||||
expect(() =>
|
||||
JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)
|
||||
).not.to.throwError();
|
||||
expect(() => JSON.parse(objects[0].attributes.optionsJSON)).not.to.throwError();
|
||||
expect(() => JSON.parse(objects[0].attributes.panelsJSON)).not.to.throwError();
|
||||
});
|
||||
|
@ -244,47 +285,55 @@ export default function ({ getService }) {
|
|||
.post('/api/saved_objects/_export')
|
||||
.send({
|
||||
type: ['dashboard'],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.headers['content-disposition']).to.eql('attachment; filename="export.ndjson"');
|
||||
.then(resp => {
|
||||
expect(resp.headers['content-disposition']).to.eql(
|
||||
'attachment; filename="export.ndjson"'
|
||||
);
|
||||
expect(resp.headers['content-type']).to.eql('application/ndjson');
|
||||
const objects = resp.text.split('\n').map(JSON.parse);
|
||||
expect(objects).to.eql([{
|
||||
attributes: {
|
||||
description: '',
|
||||
hits: 0,
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
|
||||
expect(objects).to.eql([
|
||||
{
|
||||
attributes: {
|
||||
description: '',
|
||||
hits: 0,
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON:
|
||||
objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
|
||||
},
|
||||
optionsJSON: objects[0].attributes.optionsJSON,
|
||||
panelsJSON: objects[0].attributes.panelsJSON,
|
||||
refreshInterval: {
|
||||
display: 'Off',
|
||||
pause: false,
|
||||
value: 0,
|
||||
},
|
||||
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
|
||||
timeRestore: true,
|
||||
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
|
||||
title: 'Requests',
|
||||
version: 1,
|
||||
},
|
||||
optionsJSON: objects[0].attributes.optionsJSON,
|
||||
panelsJSON: objects[0].attributes.panelsJSON,
|
||||
refreshInterval: {
|
||||
display: 'Off',
|
||||
pause: false,
|
||||
value: 0,
|
||||
},
|
||||
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
|
||||
timeRestore: true,
|
||||
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
|
||||
title: 'Requests',
|
||||
version: 1,
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
migrationVersion: objects[0].migrationVersion,
|
||||
references: [
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
name: 'panel_0',
|
||||
type: 'visualization',
|
||||
},
|
||||
],
|
||||
type: 'dashboard',
|
||||
updated_at: '2017-09-21T18:57:40.826Z',
|
||||
version: objects[0].version,
|
||||
},
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
migrationVersion: objects[0].migrationVersion,
|
||||
references: [
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
name: 'panel_0',
|
||||
type: 'visualization',
|
||||
},
|
||||
],
|
||||
type: 'dashboard',
|
||||
updated_at: '2017-09-21T18:57:40.826Z',
|
||||
version: objects[0].version,
|
||||
}]);
|
||||
]);
|
||||
expect(objects[0].migrationVersion).to.be.ok();
|
||||
expect(() => JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)).not.to.throwError();
|
||||
expect(() =>
|
||||
JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)
|
||||
).not.to.throwError();
|
||||
expect(() => JSON.parse(objects[0].attributes.optionsJSON)).not.to.throwError();
|
||||
expect(() => JSON.parse(objects[0].attributes.panelsJSON)).not.to.throwError();
|
||||
});
|
||||
|
@ -300,47 +349,55 @@ export default function ({ getService }) {
|
|||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
},
|
||||
],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.headers['content-disposition']).to.eql('attachment; filename="export.ndjson"');
|
||||
.then(resp => {
|
||||
expect(resp.headers['content-disposition']).to.eql(
|
||||
'attachment; filename="export.ndjson"'
|
||||
);
|
||||
expect(resp.headers['content-type']).to.eql('application/ndjson');
|
||||
const objects = resp.text.split('\n').map(JSON.parse);
|
||||
expect(objects).to.eql([{
|
||||
attributes: {
|
||||
description: '',
|
||||
hits: 0,
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
|
||||
expect(objects).to.eql([
|
||||
{
|
||||
attributes: {
|
||||
description: '',
|
||||
hits: 0,
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON:
|
||||
objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
|
||||
},
|
||||
optionsJSON: objects[0].attributes.optionsJSON,
|
||||
panelsJSON: objects[0].attributes.panelsJSON,
|
||||
refreshInterval: {
|
||||
display: 'Off',
|
||||
pause: false,
|
||||
value: 0,
|
||||
},
|
||||
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
|
||||
timeRestore: true,
|
||||
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
|
||||
title: 'Requests',
|
||||
version: 1,
|
||||
},
|
||||
optionsJSON: objects[0].attributes.optionsJSON,
|
||||
panelsJSON: objects[0].attributes.panelsJSON,
|
||||
refreshInterval: {
|
||||
display: 'Off',
|
||||
pause: false,
|
||||
value: 0,
|
||||
},
|
||||
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
|
||||
timeRestore: true,
|
||||
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
|
||||
title: 'Requests',
|
||||
version: 1,
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
migrationVersion: objects[0].migrationVersion,
|
||||
references: [
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
name: 'panel_0',
|
||||
type: 'visualization',
|
||||
},
|
||||
],
|
||||
type: 'dashboard',
|
||||
updated_at: '2017-09-21T18:57:40.826Z',
|
||||
version: objects[0].version,
|
||||
},
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
migrationVersion: objects[0].migrationVersion,
|
||||
references: [
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
name: 'panel_0',
|
||||
type: 'visualization',
|
||||
},
|
||||
],
|
||||
type: 'dashboard',
|
||||
updated_at: '2017-09-21T18:57:40.826Z',
|
||||
version: objects[0].version,
|
||||
}]);
|
||||
]);
|
||||
expect(objects[0].migrationVersion).to.be.ok();
|
||||
expect(() => JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)).not.to.throwError();
|
||||
expect(() =>
|
||||
JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)
|
||||
).not.to.throwError();
|
||||
expect(() => JSON.parse(objects[0].attributes.optionsJSON)).not.to.throwError();
|
||||
expect(() => JSON.parse(objects[0].attributes.panelsJSON)).not.to.throwError();
|
||||
});
|
||||
|
@ -357,14 +414,15 @@ export default function ({ getService }) {
|
|||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
},
|
||||
],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: '"value" contains a conflict between exclusive peers [type, objects]',
|
||||
validation: { source: 'payload', keys: [ 'value' ] },
|
||||
validation: { source: 'payload', keys: ['value'] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -382,14 +440,12 @@ export default function ({ getService }) {
|
|||
},
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
.then(resp => {
|
||||
customVisId = resp.body.id;
|
||||
});
|
||||
});
|
||||
after(async () => {
|
||||
await supertest
|
||||
.delete(`/api/saved_objects/visualization/${customVisId}`)
|
||||
.expect(200);
|
||||
await supertest.delete(`/api/saved_objects/visualization/${customVisId}`).expect(200);
|
||||
await esArchiver.unload('saved_objects/10k');
|
||||
});
|
||||
|
||||
|
@ -398,13 +454,14 @@ export default function ({ getService }) {
|
|||
.post('/api/saved_objects/_export')
|
||||
.send({
|
||||
type: ['dashboard', 'visualization', 'search', 'index-pattern'],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: `Can't export more than 10000 objects`
|
||||
message: `Can't export more than 10000 objects`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -412,22 +469,24 @@ export default function ({ getService }) {
|
|||
});
|
||||
|
||||
describe('without kibana index', () => {
|
||||
before(async () => (
|
||||
// just in case the kibana server has recreated it
|
||||
await es.indices.delete({
|
||||
index: '.kibana',
|
||||
ignore: [404],
|
||||
})
|
||||
));
|
||||
before(
|
||||
async () =>
|
||||
// just in case the kibana server has recreated it
|
||||
await es.indices.delete({
|
||||
index: '.kibana',
|
||||
ignore: [404],
|
||||
})
|
||||
);
|
||||
|
||||
it('should return empty response', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.send({
|
||||
type: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
.then(resp => {
|
||||
expect(resp.text).to.eql('');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -138,6 +138,7 @@ describe('copySavedObjectsToSpaces', () => {
|
|||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"excludeExportDetails": true,
|
||||
"exportSizeLimit": 1000,
|
||||
"includeReferencesDeep": true,
|
||||
"namespace": "sourceSpace",
|
||||
|
|
|
@ -36,6 +36,7 @@ export function copySavedObjectsToSpacesFactory(
|
|||
const objectStream = await importExport.getSortedObjectsForExport({
|
||||
namespace: spaceIdToNamespace(sourceSpaceId),
|
||||
includeReferencesDeep: options.includeReferences,
|
||||
excludeExportDetails: true,
|
||||
objects: options.objects,
|
||||
savedObjectsClient,
|
||||
types: eligibleTypes,
|
||||
|
|
|
@ -158,6 +158,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
|
|||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"excludeExportDetails": true,
|
||||
"exportSizeLimit": 1000,
|
||||
"includeReferencesDeep": true,
|
||||
"namespace": "sourceSpace",
|
||||
|
|
|
@ -31,6 +31,7 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory(
|
|||
const objectStream = await importExport.getSortedObjectsForExport({
|
||||
namespace: spaceIdToNamespace(sourceSpaceId),
|
||||
includeReferencesDeep: options.includeReferences,
|
||||
excludeExportDetails: true,
|
||||
objects: options.objects,
|
||||
savedObjectsClient,
|
||||
types: eligibleTypes,
|
||||
|
|
|
@ -1884,8 +1884,6 @@
|
|||
"kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModalTitle": "保存されたオブジェクトの削除",
|
||||
"kbn.management.objects.objectsTable.export.dangerNotification": "エクスポートを生成できません",
|
||||
"kbn.management.objects.objectsTable.export.successNotification": "ファイルはバックグラウンドでダウンロード中です",
|
||||
"kbn.management.objects.objectsTable.exportAll.dangerNotification": "エクスポートを生成できません",
|
||||
"kbn.management.objects.objectsTable.exportAll.successNotification": "ファイルはバックグラウンドでダウンロード中です",
|
||||
"kbn.management.objects.objectsTable.exportObjectsConfirmModal.cancelButtonLabel": "キャンセル",
|
||||
"kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel": "すべてエクスポート:",
|
||||
"kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportOptionsLabel": "オプション",
|
||||
|
|
|
@ -1885,8 +1885,6 @@
|
|||
"kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModalTitle": "删除已保存对象",
|
||||
"kbn.management.objects.objectsTable.export.dangerNotification": "无法生成报告",
|
||||
"kbn.management.objects.objectsTable.export.successNotification": "您的文件正在后台下载",
|
||||
"kbn.management.objects.objectsTable.exportAll.dangerNotification": "无法生成报告",
|
||||
"kbn.management.objects.objectsTable.exportAll.successNotification": "您的文件正在后台下载",
|
||||
"kbn.management.objects.objectsTable.exportObjectsConfirmModal.cancelButtonLabel": "取消",
|
||||
"kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel": "全部导出",
|
||||
"kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportOptionsLabel": "选项",
|
||||
|
|
|
@ -104,6 +104,7 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
|
|||
.post(`${getUrlPrefix(spaceId)}/api/saved_objects/_export`)
|
||||
.send({
|
||||
type: 'visualization',
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.auth(user.username, user.password)
|
||||
.expect(tests.spaceAwareType.statusCode)
|
||||
|
@ -120,6 +121,7 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
|
|||
id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`,
|
||||
},
|
||||
],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.auth(user.username, user.password)
|
||||
.expect(tests.spaceAwareType.statusCode)
|
||||
|
@ -137,6 +139,7 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
|
|||
id: `hiddentype_1`,
|
||||
},
|
||||
],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.auth(user.username, user.password)
|
||||
.expect(tests.hiddenType.statusCode)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue