diff --git a/docs/api/saved-objects/export.asciidoc b/docs/api/saved-objects/export.asciidoc
index d06756b109a90..e6c0adf2128ba 100644
--- a/docs/api/saved-objects/export.asciidoc
+++ b/docs/api/saved-objects/export.asciidoc
@@ -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
diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md
index c085d4cdf0d42..b2d5491d01a4b 100644
--- a/docs/development/core/server/kibana-plugin-server.md
+++ b/docs/development/core/server/kibana-plugin-server.md
@@ -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 find() 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. |
diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.excludeexportdetails.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.excludeexportdetails.md
new file mode 100644
index 0000000000000..bffc809689834
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.excludeexportdetails.md
@@ -0,0 +1,13 @@
+
+
+[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.
+
+Signature:
+
+```typescript
+excludeExportDetails?: boolean;
+```
diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md
index d721fc260eaf8..0cd7688e04184 100644
--- a/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md
+++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md
@@ -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.
Signature:
diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.md
index 0f1bd94d01552..d312d7d4b3499 100644
--- a/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.md
+++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.md
@@ -16,8 +16,9 @@ export interface SavedObjectsExportOptions
| Property | Type | Description |
| --- | --- | --- |
+| [excludeExportDetails](./kibana-plugin-server.savedobjectsexportoptions.excludeexportdetails.md) | boolean | 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) | number | the maximum number of objects to export. |
-| [includeReferencesDeep](./kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md) | boolean | flag to also include all related saved objects in the export response. |
+| [includeReferencesDeep](./kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md) | boolean | flag to also include all related saved objects in the export stream. |
| [namespace](./kibana-plugin-server.savedobjectsexportoptions.namespace.md) | string | optional namespace to override the namespace used by the savedObjectsClient. |
| [objects](./kibana-plugin-server.savedobjectsexportoptions.objects.md) | Array<{
id: string;
type: string;
}> | optional array of objects to export. |
| [savedObjectsClient](./kibana-plugin-server.savedobjectsexportoptions.savedobjectsclient.md) | SavedObjectsClientContract | an instance of the SavedObjectsClient. |
diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.exportedcount.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.exportedcount.md
new file mode 100644
index 0000000000000..c2e588dd3c121
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.exportedcount.md
@@ -0,0 +1,13 @@
+
+
+[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
+
+Signature:
+
+```typescript
+exportedCount: number;
+```
diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.md
new file mode 100644
index 0000000000000..fb3af350d21ea
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.md
@@ -0,0 +1,22 @@
+
+
+[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
+
+Signature:
+
+```typescript
+export interface SavedObjectsExportResultDetails
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [exportedCount](./kibana-plugin-server.savedobjectsexportresultdetails.exportedcount.md) | number | number of successfully exported objects |
+| [missingRefCount](./kibana-plugin-server.savedobjectsexportresultdetails.missingrefcount.md) | number | number of missing references |
+| [missingReferences](./kibana-plugin-server.savedobjectsexportresultdetails.missingreferences.md) | Array<{
id: string;
type: string;
}> | missing references details |
+
diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.missingrefcount.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.missingrefcount.md
new file mode 100644
index 0000000000000..5b51199ea4780
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.missingrefcount.md
@@ -0,0 +1,13 @@
+
+
+[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
+
+Signature:
+
+```typescript
+missingRefCount: number;
+```
diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.missingreferences.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.missingreferences.md
new file mode 100644
index 0000000000000..1602bfb6e6cb6
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.missingreferences.md
@@ -0,0 +1,16 @@
+
+
+[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
+
+Signature:
+
+```typescript
+missingReferences: Array<{
+ id: string;
+ type: string;
+ }>;
+```
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index d92c92841bb48..fe4d0a05c4fb9 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -144,6 +144,7 @@ export {
SavedObjectsCreateOptions,
SavedObjectsErrorHelpers,
SavedObjectsExportOptions,
+ SavedObjectsExportResultDetails,
SavedObjectsFindResponse,
SavedObjectsImportConflictError,
SavedObjectsImportError,
diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts
index df3bbe7c455e4..1a2a843ebb2b8 100644
--- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts
+++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts
@@ -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 [
diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts
index eca8fc0405300..e1a705a36db75 100644
--- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts
+++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts
@@ -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])]);
}
diff --git a/src/core/server/saved_objects/export/index.ts b/src/core/server/saved_objects/export/index.ts
index d994df2af627c..7533b8e500039 100644
--- a/src/core/server/saved_objects/export/index.ts
+++ b/src/core/server/saved_objects/export/index.ts
@@ -20,4 +20,5 @@
export {
getSortedObjectsForExport,
SavedObjectsExportOptions,
+ SavedObjectsExportResultDetails,
} from './get_sorted_objects_for_export';
diff --git a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts b/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts
index 4613553fbd301..89d555e06a634 100644
--- a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts
+++ b/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts
@@ -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] {
diff --git a/src/core/server/saved_objects/export/inject_nested_depdendencies.ts b/src/core/server/saved_objects/export/inject_nested_depdendencies.ts
index 279b06f955571..d00650926e57a 100644
--- a/src/core/server/saved_objects/export/inject_nested_depdendencies.ts
+++ b/src/core/server/saved_objects/export/inject_nested_depdendencies.ts
@@ -17,47 +17,43 @@
* under the License.
*/
-import Boom from 'boom';
import { SavedObject, SavedObjectsClientContract } from '../types';
export function getObjectReferencesToFetch(savedObjectsMap: Map) {
const objectsToFetch = new Map();
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();
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}`;
diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts
index 674f8df33ee37..76c62e0841bff 100644
--- a/src/core/server/saved_objects/index.ts
+++ b/src/core/server/saved_objects/index.ts
@@ -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';
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 46e5d2b6ab6c6..604a0916c028f 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -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)
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js
index 7cf3f935ec4b5..39a9f7cd98a57 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js
@@ -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(
+
+ );
+
+ // 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();
@@ -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();
@@ -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(
-
- );
+ const component = shallowWithI18nProvider();
component.instance().onQueryChange({
- query: Query.parse('test')
+ query: Query.parse('test'),
});
// Ensure all promises resolve
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js
index 52871c360309c..188762f165b24 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js
@@ -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();
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/extract_export_details.test.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/extract_export_details.test.ts
new file mode 100644
index 0000000000000..a6ed2e36839f4
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/extract_export_details.test.ts
@@ -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();
+ });
+});
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/extract_export_details.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/extract_export_details.ts
new file mode 100644
index 0000000000000..fdd72aece06bc
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/extract_export_details.ts
@@ -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 {
+ const reader = new FileReader();
+ const content = await new Promise((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;
+}
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.js
index 245812867f1de..b6c8d25568446 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.js
@@ -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';
diff --git a/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.test.ts b/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.test.ts
index 2eab73137fff0..342063fefaec6 100644
--- a/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.test.ts
+++ b/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.test.ts
@@ -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',
+ },
+ ]);
+ });
});
diff --git a/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.ts b/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.ts
index 10047284f5c96..b96514054db56 100644
--- a/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.ts
+++ b/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.ts
@@ -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(obj => !!obj));
+ .pipe(
+ createFilterStream(
+ obj => !!obj && !(obj as SavedObjectsExportResultDetails).exportedCount
+ )
+ );
}
diff --git a/src/legacy/server/saved_objects/routes/export.test.ts b/src/legacy/server/saved_objects/routes/export.test.ts
index 491e3a9067611..1b7e0dfa65db5 100644
--- a/src/legacy/server/saved_objects/routes/export.test.ts
+++ b/src/legacy/server/saved_objects/routes/export.test.ts
@@ -157,6 +157,7 @@ describe('POST /api/saved_objects/_export', () => {
"calls": Array [
Array [
Object {
+ "excludeExportDetails": false,
"exportSizeLimit": 10000,
"includeReferencesDeep": true,
"objects": undefined,
diff --git a/src/legacy/server/saved_objects/routes/export.ts b/src/legacy/server/saved_objects/routes/export.ts
index fc120030a873c..ce4aed4b78c2a 100644
--- a/src/legacy/server/saved_objects/routes/export.ts
+++ b/src/legacy/server/saved_objects/routes/export.ts
@@ -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([
diff --git a/test/api_integration/apis/saved_objects/export.js b/test/api_integration/apis/saved_objects/export.js
index e39749aa48159..9ab7a09309952 100644
--- a/test/api_integration/apis/saved_objects/export.js
+++ b/test/api_integration/apis/saved_objects/export.js
@@ -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,
- },
- optionsJSON: objects[0].attributes.optionsJSON,
- panelsJSON: objects[0].attributes.panelsJSON,
- refreshInterval: {
- display: 'Off',
- pause: false,
- value: 0,
+ 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,
},
- 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,
- },
- optionsJSON: objects[0].attributes.optionsJSON,
- panelsJSON: objects[0].attributes.panelsJSON,
- refreshInterval: {
- display: 'Off',
- pause: false,
- value: 0,
+ 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,
},
- 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,
- },
- optionsJSON: objects[0].attributes.optionsJSON,
- panelsJSON: objects[0].attributes.panelsJSON,
- refreshInterval: {
- display: 'Off',
- pause: false,
- value: 0,
+ 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,
},
- 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('');
});
});
diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts
index 10e427a29e442..f01a28847bbdf 100644
--- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts
+++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts
@@ -138,6 +138,7 @@ describe('copySavedObjectsToSpaces', () => {
Array [
Array [
Object {
+ "excludeExportDetails": true,
"exportSizeLimit": 1000,
"includeReferencesDeep": true,
"namespace": "sourceSpace",
diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts
index 608d57d873687..76c3037e672ad 100644
--- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts
+++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts
@@ -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,
diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts
index fbafb18699081..97b7480ea4af8 100644
--- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts
+++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts
@@ -158,6 +158,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
Array [
Array [
Object {
+ "excludeExportDetails": true,
"exportSizeLimit": 1000,
"includeReferencesDeep": true,
"namespace": "sourceSpace",
diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts
index d7c602c28b253..22ceeb9dd4dfa 100644
--- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts
+++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts
@@ -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,
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 90613ba4378a9..60a55ca192c78 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -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": "オプション",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 22baff00f8cbc..76d731f685e90 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -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": "选项",
diff --git a/x-pack/test/saved_object_api_integration/common/suites/export.ts b/x-pack/test/saved_object_api_integration/common/suites/export.ts
index d7d1a99e63e02..114a1fe53ccd6 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/export.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/export.ts
@@ -104,6 +104,7 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest