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