Ignore missing references on saved object exports (#47685) (#48388)

* add saved object export details in ndjson response

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* update core doc

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* exclude export details for space copy

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* fixing tests

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* display warning instead of success if export contains missing refs

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* nits/typo

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* properly updates api integration tests

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* fix typings

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* add test on objects_table component

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* remove added translations from jp/cn bundles

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* restoring line feeds

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* improve doc and user alert message

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* restoring line feeds on server.api.md

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>

* warning test label

Signed-off-by: pgayvallet <pierre.gayvallet@elastic.co>
This commit is contained in:
Rudolf Meijering 2019-10-16 15:39:17 +02:00 committed by GitHub
parent 168f4d6223
commit 05c76f293c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1085 additions and 456 deletions

View file

@ -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

View file

@ -92,6 +92,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [SavedObjectsClientWrapperOptions](./kibana-plugin-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. |
| [SavedObjectsCreateOptions](./kibana-plugin-server.savedobjectscreateoptions.md) | |
| [SavedObjectsExportOptions](./kibana-plugin-server.savedobjectsexportoptions.md) | Options controlling the export operation. |
| [SavedObjectsExportResultDetails](./kibana-plugin-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry |
| [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) | |
| [SavedObjectsFindResponse](./kibana-plugin-server.savedobjectsfindresponse.md) | Return type of the Saved Objects <code>find()</code> method.<!-- -->\*Note\*: this type is different between the Public and Server Saved Objects clients. |
| [SavedObjectsImportConflictError](./kibana-plugin-server.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [SavedObjectsExportOptions](./kibana-plugin-server.savedobjectsexportoptions.md) &gt; [excludeExportDetails](./kibana-plugin-server.savedobjectsexportoptions.excludeexportdetails.md)
## SavedObjectsExportOptions.excludeExportDetails property
flag to not append [export details](./kibana-plugin-server.savedobjectsexportresultdetails.md) to the end of the export stream.
<b>Signature:</b>
```typescript
excludeExportDetails?: boolean;
```

View file

@ -4,7 +4,7 @@
## SavedObjectsExportOptions.includeReferencesDeep property
flag to also include all related saved objects in the export response.
flag to also include all related saved objects in the export stream.
<b>Signature:</b>

View file

@ -16,8 +16,9 @@ export interface SavedObjectsExportOptions
| Property | Type | Description |
| --- | --- | --- |
| [excludeExportDetails](./kibana-plugin-server.savedobjectsexportoptions.excludeexportdetails.md) | <code>boolean</code> | flag to not append [export details](./kibana-plugin-server.savedobjectsexportresultdetails.md) to the end of the export stream. |
| [exportSizeLimit](./kibana-plugin-server.savedobjectsexportoptions.exportsizelimit.md) | <code>number</code> | the maximum number of objects to export. |
| [includeReferencesDeep](./kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md) | <code>boolean</code> | flag to also include all related saved objects in the export response. |
| [includeReferencesDeep](./kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md) | <code>boolean</code> | flag to also include all related saved objects in the export stream. |
| [namespace](./kibana-plugin-server.savedobjectsexportoptions.namespace.md) | <code>string</code> | optional namespace to override the namespace used by the savedObjectsClient. |
| [objects](./kibana-plugin-server.savedobjectsexportoptions.objects.md) | <code>Array&lt;{</code><br/><code> id: string;</code><br/><code> type: string;</code><br/><code> }&gt;</code> | optional array of objects to export. |
| [savedObjectsClient](./kibana-plugin-server.savedobjectsexportoptions.savedobjectsclient.md) | <code>SavedObjectsClientContract</code> | an instance of the SavedObjectsClient. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [SavedObjectsExportResultDetails](./kibana-plugin-server.savedobjectsexportresultdetails.md) &gt; [exportedCount](./kibana-plugin-server.savedobjectsexportresultdetails.exportedcount.md)
## SavedObjectsExportResultDetails.exportedCount property
number of successfully exported objects
<b>Signature:</b>
```typescript
exportedCount: number;
```

View file

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [SavedObjectsExportResultDetails](./kibana-plugin-server.savedobjectsexportresultdetails.md)
## SavedObjectsExportResultDetails interface
Structure of the export result details entry
<b>Signature:</b>
```typescript
export interface SavedObjectsExportResultDetails
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [exportedCount](./kibana-plugin-server.savedobjectsexportresultdetails.exportedcount.md) | <code>number</code> | number of successfully exported objects |
| [missingRefCount](./kibana-plugin-server.savedobjectsexportresultdetails.missingrefcount.md) | <code>number</code> | number of missing references |
| [missingReferences](./kibana-plugin-server.savedobjectsexportresultdetails.missingreferences.md) | <code>Array&lt;{</code><br/><code> id: string;</code><br/><code> type: string;</code><br/><code> }&gt;</code> | missing references details |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [SavedObjectsExportResultDetails](./kibana-plugin-server.savedobjectsexportresultdetails.md) &gt; [missingRefCount](./kibana-plugin-server.savedobjectsexportresultdetails.missingrefcount.md)
## SavedObjectsExportResultDetails.missingRefCount property
number of missing references
<b>Signature:</b>
```typescript
missingRefCount: number;
```

View file

@ -0,0 +1,16 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [SavedObjectsExportResultDetails](./kibana-plugin-server.savedobjectsexportresultdetails.md) &gt; [missingReferences](./kibana-plugin-server.savedobjectsexportresultdetails.missingreferences.md)
## SavedObjectsExportResultDetails.missingReferences property
missing references details
<b>Signature:</b>
```typescript
missingReferences: Array<{
id: string;
type: string;
}>;
```

View file

@ -144,6 +144,7 @@ export {
SavedObjectsCreateOptions,
SavedObjectsErrorHelpers,
SavedObjectsExportOptions,
SavedObjectsExportResultDetails,
SavedObjectsFindResponse,
SavedObjectsImportConflictError,
SavedObjectsImportError,

View file

@ -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 [

View file

@ -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])]);
}

View file

@ -20,4 +20,5 @@
export {
getSortedObjectsForExport,
SavedObjectsExportOptions,
SavedObjectsExportResultDetails,
} from './get_sorted_objects_for_export';

View file

@ -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] {

View file

@ -17,47 +17,43 @@
* under the License.
*/
import Boom from 'boom';
import { SavedObject, SavedObjectsClientContract } from '../types';
export function getObjectReferencesToFetch(savedObjectsMap: Map<string, SavedObject>) {
const objectsToFetch = new Map<string, { type: string; id: string }>();
for (const savedObject of savedObjectsMap.values()) {
for (const { type, id } of savedObject.references || []) {
if (!savedObjectsMap.has(`${type}:${id}`)) {
objectsToFetch.set(`${type}:${id}`, { type, id });
for (const ref of savedObject.references || []) {
if (!savedObjectsMap.has(objKey(ref))) {
objectsToFetch.set(objKey(ref), { type: ref.type, id: ref.id });
}
}
}
return [...objectsToFetch.values()];
}
export async function injectNestedDependencies(
export async function fetchNestedDependencies(
savedObjects: SavedObject[],
savedObjectsClient: SavedObjectsClientContract,
namespace?: string
) {
const savedObjectsMap = new Map<string, SavedObject>();
for (const savedObject of savedObjects) {
savedObjectsMap.set(`${savedObject.type}:${savedObject.id}`, savedObject);
savedObjectsMap.set(objKey(savedObject), savedObject);
}
let objectsToFetch = getObjectReferencesToFetch(savedObjectsMap);
while (objectsToFetch.length > 0) {
const bulkGetResponse = await savedObjectsClient.bulkGet(objectsToFetch, { namespace });
// Check for errors
const erroredObjects = bulkGetResponse.saved_objects.filter(obj => !!obj.error);
if (erroredObjects.length) {
const err = Boom.badRequest();
err.output.payload.attributes = {
objects: erroredObjects,
};
throw err;
}
// Push to array result
for (const savedObject of bulkGetResponse.saved_objects) {
savedObjectsMap.set(`${savedObject.type}:${savedObject.id}`, savedObject);
savedObjectsMap.set(objKey(savedObject), savedObject);
}
objectsToFetch = getObjectReferencesToFetch(savedObjectsMap);
}
return [...savedObjectsMap.values()];
const allObjects = [...savedObjectsMap.values()];
return {
objects: allObjects.filter(obj => !obj.error),
missingRefs: allObjects.filter(obj => !!obj.error).map(obj => ({ type: obj.type, id: obj.id })),
};
}
const objKey = (obj: { type: string; id: string }) => `${obj.type}:${obj.id}`;

View file

@ -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';

View file

@ -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)

View file

@ -25,6 +25,7 @@ import { ObjectsTable, POSSIBLE_TYPES } from '../objects_table';
import { Flyout } from '../components/flyout/';
import { Relationships } from '../components/relationships/';
import { findObjects } from '../../../lib';
import { extractExportDetails } from '../../../lib/extract_export_details';
jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
@ -49,6 +50,10 @@ jest.mock('../../../lib/fetch_export_by_type_and_search', () => ({
fetchExportByTypeAndSearch: jest.fn(),
}));
jest.mock('../../../lib/extract_export_details', () => ({
extractExportDetails: jest.fn(),
}));
jest.mock('../../../lib/get_saved_object_counts', () => ({
getSavedObjectCounts: jest.fn().mockImplementation(() => {
return {
@ -190,12 +195,14 @@ beforeEach(() => {
let addDangerMock;
let addSuccessMock;
let addWarningMock;
describe('ObjectsTable', () => {
beforeEach(() => {
defaultProps.savedObjectsClient.find.mockClear();
extractExportDetails.mockReset();
// mock _.debounce to fire immediately with no internal timer
require('lodash').debounce = function (func) {
require('lodash').debounce = func => {
function debounced(...args) {
return func.apply(this, args);
}
@ -203,9 +210,11 @@ describe('ObjectsTable', () => {
};
addDangerMock = jest.fn();
addSuccessMock = jest.fn();
addWarningMock = jest.fn();
require('ui/notify').toastNotifications = {
addDanger: addDangerMock,
addSuccess: addSuccessMock,
addWarning: addWarningMock,
};
});
@ -280,6 +289,55 @@ describe('ObjectsTable', () => {
});
});
it('should display a warning is export contains missing references', async () => {
const mockSelectedSavedObjects = [
{ id: '1', type: 'index-pattern' },
{ id: '3', type: 'dashboard' },
];
const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({
_id: obj.id,
_type: obj._type,
_source: {},
}));
const mockSavedObjectsClient = {
...defaultProps.savedObjectsClient,
bulkGet: jest.fn().mockImplementation(() => ({
savedObjects: mockSavedObjects,
})),
};
const { fetchExportObjects } = require('../../../lib/fetch_export_objects');
extractExportDetails.mockImplementation(() => ({
exportedCount: 2,
missingRefCount: 1,
missingReferences: [{ id: '7', type: 'visualisation' }],
}));
const component = shallowWithI18nProvider(
<ObjectsTable {...defaultProps} savedObjectsClient={mockSavedObjectsClient} />
);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
// Set some as selected
component.instance().onSelectionChanged(mockSelectedSavedObjects);
await component.instance().onExport(true);
expect(fetchExportObjects).toHaveBeenCalledWith(mockSelectedSavedObjects, true);
expect(addWarningMock).toHaveBeenCalledWith({
title:
'Your file is downloading in the background. ' +
'Some related objects could not be found. ' +
'Please see the last line in the exported file for a list of missing objects.',
});
});
it('should allow the user to choose when exporting all', async () => {
const component = shallowWithI18nProvider(<ObjectsTable {...defaultProps} />);
@ -295,7 +353,9 @@ describe('ObjectsTable', () => {
});
it('should export all', async () => {
const { fetchExportByTypeAndSearch } = require('../../../lib/fetch_export_by_type_and_search');
const {
fetchExportByTypeAndSearch,
} = require('../../../lib/fetch_export_by_type_and_search');
const { saveAs } = require('@elastic/filesaver');
const component = shallowWithI18nProvider(<ObjectsTable {...defaultProps} />);
@ -312,20 +372,20 @@ describe('ObjectsTable', () => {
expect(fetchExportByTypeAndSearch).toHaveBeenCalledWith(POSSIBLE_TYPES, undefined, true);
expect(saveAs).toHaveBeenCalledWith(blob, 'export.ndjson');
expect(addSuccessMock).toHaveBeenCalledWith({ title: 'Your file is downloading in the background' });
expect(addSuccessMock).toHaveBeenCalledWith({
title: 'Your file is downloading in the background',
});
});
it('should export all, accounting for the current search criteria', async () => {
const { fetchExportByTypeAndSearch } = require('../../../lib/fetch_export_by_type_and_search');
const {
fetchExportByTypeAndSearch,
} = require('../../../lib/fetch_export_by_type_and_search');
const { saveAs } = require('@elastic/filesaver');
const component = shallowWithI18nProvider(
<ObjectsTable
{...defaultProps}
/>
);
const component = shallowWithI18nProvider(<ObjectsTable {...defaultProps} />);
component.instance().onQueryChange({
query: Query.parse('test')
query: Query.parse('test'),
});
// Ensure all promises resolve

View file

@ -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();

View file

@ -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();
});
});

View file

@ -0,0 +1,51 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export async function extractExportDetails(
blob: Blob
): Promise<SavedObjectsExportResultDetails | undefined> {
const reader = new FileReader();
const content = await new Promise<string>((resolve, reject) => {
reader.addEventListener('loadend', e => {
resolve((e as any).target.result);
});
reader.addEventListener('error', e => {
reject(e);
});
reader.readAsText(blob, 'utf-8');
});
const lines = content.split('\n').filter(l => l.length > 0);
const maybeDetails = JSON.parse(lines[lines.length - 1]);
if (isExportDetails(maybeDetails)) {
return maybeDetails;
}
}
export interface SavedObjectsExportResultDetails {
exportedCount: number;
missingRefCount: number;
missingReferences: Array<{
id: string;
type: string;
}>;
}
function isExportDetails(object: any): object is SavedObjectsExportResultDetails {
return 'exportedCount' in object && 'missingRefCount' in object && 'missingReferences' in object;
}

View file

@ -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';

View file

@ -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',
},
]);
});
});

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { Readable } from 'stream';
import { SavedObject } from 'src/core/server';
import { SavedObject, SavedObjectsExportResultDetails } from 'src/core/server';
import { createSplitStream, createMapStream, createFilterStream } from '../../../utils/streams';
export function createSavedObjectsStreamFromNdJson(ndJsonStream: Readable) {
@ -30,5 +30,9 @@ export function createSavedObjectsStreamFromNdJson(ndJsonStream: Readable) {
}
})
)
.pipe(createFilterStream<SavedObject>(obj => !!obj));
.pipe(
createFilterStream<SavedObject | SavedObjectsExportResultDetails>(
obj => !!obj && !(obj as SavedObjectsExportResultDetails).exportedCount
)
);
}

View file

@ -157,6 +157,7 @@ describe('POST /api/saved_objects/_export', () => {
"calls": Array [
Array [
Object {
"excludeExportDetails": false,
"exportSizeLimit": 10000,
"includeReferencesDeep": true,
"objects": undefined,

View file

@ -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([

View file

@ -37,7 +37,30 @@ export default function ({ getService }) {
type: ['index-pattern', 'search', 'visualization', 'dashboard'],
})
.expect(200)
.then((resp) => {
.then(resp => {
const objects = resp.text.split('\n').map(JSON.parse);
expect(objects).to.have.length(4);
expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab');
expect(objects[0]).to.have.property('type', 'index-pattern');
expect(objects[1]).to.have.property('id', 'dd7caf20-9efd-11e7-acb3-3dab96693fab');
expect(objects[1]).to.have.property('type', 'visualization');
expect(objects[2]).to.have.property('id', 'be3733a0-9efe-11e7-acb3-3dab96693fab');
expect(objects[2]).to.have.property('type', 'dashboard');
expect(objects[3]).to.have.property('exportedCount', 3);
expect(objects[3]).to.have.property('missingRefCount', 0);
expect(objects[3].missingReferences).to.have.length(0);
});
});
it('should exclude the export details if asked', async () => {
await supertest
.post('/api/saved_objects/_export')
.send({
type: ['index-pattern', 'search', 'visualization', 'dashboard'],
excludeExportDetails: true,
})
.expect(200)
.then(resp => {
const objects = resp.text.split('\n').map(JSON.parse);
expect(objects).to.have.length(3);
expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab');
@ -62,15 +85,18 @@ export default function ({ getService }) {
],
})
.expect(200)
.then((resp) => {
.then(resp => {
const objects = resp.text.split('\n').map(JSON.parse);
expect(objects).to.have.length(3);
expect(objects).to.have.length(4);
expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab');
expect(objects[0]).to.have.property('type', 'index-pattern');
expect(objects[1]).to.have.property('id', 'dd7caf20-9efd-11e7-acb3-3dab96693fab');
expect(objects[1]).to.have.property('type', 'visualization');
expect(objects[2]).to.have.property('id', 'be3733a0-9efe-11e7-acb3-3dab96693fab');
expect(objects[2]).to.have.property('type', 'dashboard');
expect(objects[3]).to.have.property('exportedCount', 3);
expect(objects[3]).to.have.property('missingRefCount', 0);
expect(objects[3].missingReferences).to.have.length(0);
});
});
@ -82,15 +108,18 @@ export default function ({ getService }) {
type: ['dashboard'],
})
.expect(200)
.then((resp) => {
.then(resp => {
const objects = resp.text.split('\n').map(JSON.parse);
expect(objects).to.have.length(3);
expect(objects).to.have.length(4);
expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab');
expect(objects[0]).to.have.property('type', 'index-pattern');
expect(objects[1]).to.have.property('id', 'dd7caf20-9efd-11e7-acb3-3dab96693fab');
expect(objects[1]).to.have.property('type', 'visualization');
expect(objects[2]).to.have.property('id', 'be3733a0-9efe-11e7-acb3-3dab96693fab');
expect(objects[2]).to.have.property('type', 'dashboard');
expect(objects[3]).to.have.property('exportedCount', 3);
expect(objects[3]).to.have.property('missingRefCount', 0);
expect(objects[3].missingReferences).to.have.length(0);
});
});
@ -100,18 +129,21 @@ export default function ({ getService }) {
.send({
includeReferencesDeep: true,
type: ['dashboard'],
search: 'Requests*'
search: 'Requests*',
})
.expect(200)
.then((resp) => {
.then(resp => {
const objects = resp.text.split('\n').map(JSON.parse);
expect(objects).to.have.length(3);
expect(objects).to.have.length(4);
expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab');
expect(objects[0]).to.have.property('type', 'index-pattern');
expect(objects[1]).to.have.property('id', 'dd7caf20-9efd-11e7-acb3-3dab96693fab');
expect(objects[1]).to.have.property('type', 'visualization');
expect(objects[2]).to.have.property('id', 'be3733a0-9efe-11e7-acb3-3dab96693fab');
expect(objects[2]).to.have.property('type', 'dashboard');
expect(objects[3]).to.have.property('exportedCount', 3);
expect(objects[3]).to.have.property('missingRefCount', 0);
expect(objects[3].missingReferences).to.have.length(0);
});
});
@ -127,7 +159,7 @@ export default function ({ getService }) {
],
})
.expect(400)
.then((resp) => {
.then(resp => {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
@ -159,12 +191,13 @@ export default function ({ getService }) {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message: 'child "type" fails because ["type" at position 0 fails because ' +
message:
'child "type" fails because ["type" at position 0 fails because ' +
'["0" must be one of [config, dashboard, index-pattern, query, search, url, visualization]]]',
validation: {
source: 'payload',
keys: ['type.0'],
}
},
});
});
});
@ -178,12 +211,12 @@ export default function ({ getService }) {
await supertest
.post('/api/saved_objects/_export')
.expect(400)
.then((resp) => {
.then(resp => {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message: '"value" must be an object',
validation: { source: 'payload', keys: [ 'value' ] },
validation: { source: 'payload', keys: ['value'] },
});
});
});
@ -193,47 +226,55 @@ export default function ({ getService }) {
.post('/api/saved_objects/_export')
.send({
type: 'dashboard',
excludeExportDetails: true,
})
.expect(200)
.then((resp) => {
expect(resp.headers['content-disposition']).to.eql('attachment; filename="export.ndjson"');
.then(resp => {
expect(resp.headers['content-disposition']).to.eql(
'attachment; filename="export.ndjson"'
);
expect(resp.headers['content-type']).to.eql('application/ndjson');
const objects = resp.text.split('\n').map(JSON.parse);
expect(objects).to.eql([{
attributes: {
description: '',
hits: 0,
kibanaSavedObjectMeta: {
searchSourceJSON: objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
expect(objects).to.eql([
{
attributes: {
description: '',
hits: 0,
kibanaSavedObjectMeta: {
searchSourceJSON:
objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
},
optionsJSON: objects[0].attributes.optionsJSON,
panelsJSON: objects[0].attributes.panelsJSON,
refreshInterval: {
display: 'Off',
pause: false,
value: 0,
},
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
timeRestore: true,
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
title: 'Requests',
version: 1,
},
optionsJSON: objects[0].attributes.optionsJSON,
panelsJSON: objects[0].attributes.panelsJSON,
refreshInterval: {
display: 'Off',
pause: false,
value: 0,
},
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
timeRestore: true,
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
title: 'Requests',
version: 1,
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
migrationVersion: objects[0].migrationVersion,
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
name: 'panel_0',
type: 'visualization',
},
],
type: 'dashboard',
updated_at: '2017-09-21T18:57:40.826Z',
version: objects[0].version,
},
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
migrationVersion: objects[0].migrationVersion,
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
name: 'panel_0',
type: 'visualization',
},
],
type: 'dashboard',
updated_at: '2017-09-21T18:57:40.826Z',
version: objects[0].version,
}]);
]);
expect(objects[0].migrationVersion).to.be.ok();
expect(() => JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)).not.to.throwError();
expect(() =>
JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)
).not.to.throwError();
expect(() => JSON.parse(objects[0].attributes.optionsJSON)).not.to.throwError();
expect(() => JSON.parse(objects[0].attributes.panelsJSON)).not.to.throwError();
});
@ -244,47 +285,55 @@ export default function ({ getService }) {
.post('/api/saved_objects/_export')
.send({
type: ['dashboard'],
excludeExportDetails: true,
})
.expect(200)
.then((resp) => {
expect(resp.headers['content-disposition']).to.eql('attachment; filename="export.ndjson"');
.then(resp => {
expect(resp.headers['content-disposition']).to.eql(
'attachment; filename="export.ndjson"'
);
expect(resp.headers['content-type']).to.eql('application/ndjson');
const objects = resp.text.split('\n').map(JSON.parse);
expect(objects).to.eql([{
attributes: {
description: '',
hits: 0,
kibanaSavedObjectMeta: {
searchSourceJSON: objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
expect(objects).to.eql([
{
attributes: {
description: '',
hits: 0,
kibanaSavedObjectMeta: {
searchSourceJSON:
objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
},
optionsJSON: objects[0].attributes.optionsJSON,
panelsJSON: objects[0].attributes.panelsJSON,
refreshInterval: {
display: 'Off',
pause: false,
value: 0,
},
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
timeRestore: true,
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
title: 'Requests',
version: 1,
},
optionsJSON: objects[0].attributes.optionsJSON,
panelsJSON: objects[0].attributes.panelsJSON,
refreshInterval: {
display: 'Off',
pause: false,
value: 0,
},
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
timeRestore: true,
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
title: 'Requests',
version: 1,
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
migrationVersion: objects[0].migrationVersion,
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
name: 'panel_0',
type: 'visualization',
},
],
type: 'dashboard',
updated_at: '2017-09-21T18:57:40.826Z',
version: objects[0].version,
},
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
migrationVersion: objects[0].migrationVersion,
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
name: 'panel_0',
type: 'visualization',
},
],
type: 'dashboard',
updated_at: '2017-09-21T18:57:40.826Z',
version: objects[0].version,
}]);
]);
expect(objects[0].migrationVersion).to.be.ok();
expect(() => JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)).not.to.throwError();
expect(() =>
JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)
).not.to.throwError();
expect(() => JSON.parse(objects[0].attributes.optionsJSON)).not.to.throwError();
expect(() => JSON.parse(objects[0].attributes.panelsJSON)).not.to.throwError();
});
@ -300,47 +349,55 @@ export default function ({ getService }) {
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
},
],
excludeExportDetails: true,
})
.expect(200)
.then((resp) => {
expect(resp.headers['content-disposition']).to.eql('attachment; filename="export.ndjson"');
.then(resp => {
expect(resp.headers['content-disposition']).to.eql(
'attachment; filename="export.ndjson"'
);
expect(resp.headers['content-type']).to.eql('application/ndjson');
const objects = resp.text.split('\n').map(JSON.parse);
expect(objects).to.eql([{
attributes: {
description: '',
hits: 0,
kibanaSavedObjectMeta: {
searchSourceJSON: objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
expect(objects).to.eql([
{
attributes: {
description: '',
hits: 0,
kibanaSavedObjectMeta: {
searchSourceJSON:
objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
},
optionsJSON: objects[0].attributes.optionsJSON,
panelsJSON: objects[0].attributes.panelsJSON,
refreshInterval: {
display: 'Off',
pause: false,
value: 0,
},
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
timeRestore: true,
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
title: 'Requests',
version: 1,
},
optionsJSON: objects[0].attributes.optionsJSON,
panelsJSON: objects[0].attributes.panelsJSON,
refreshInterval: {
display: 'Off',
pause: false,
value: 0,
},
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
timeRestore: true,
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
title: 'Requests',
version: 1,
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
migrationVersion: objects[0].migrationVersion,
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
name: 'panel_0',
type: 'visualization',
},
],
type: 'dashboard',
updated_at: '2017-09-21T18:57:40.826Z',
version: objects[0].version,
},
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
migrationVersion: objects[0].migrationVersion,
references: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
name: 'panel_0',
type: 'visualization',
},
],
type: 'dashboard',
updated_at: '2017-09-21T18:57:40.826Z',
version: objects[0].version,
}]);
]);
expect(objects[0].migrationVersion).to.be.ok();
expect(() => JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)).not.to.throwError();
expect(() =>
JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)
).not.to.throwError();
expect(() => JSON.parse(objects[0].attributes.optionsJSON)).not.to.throwError();
expect(() => JSON.parse(objects[0].attributes.panelsJSON)).not.to.throwError();
});
@ -357,14 +414,15 @@ export default function ({ getService }) {
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
},
],
excludeExportDetails: true,
})
.expect(400)
.then((resp) => {
.then(resp => {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message: '"value" contains a conflict between exclusive peers [type, objects]',
validation: { source: 'payload', keys: [ 'value' ] },
validation: { source: 'payload', keys: ['value'] },
});
});
});
@ -382,14 +440,12 @@ export default function ({ getService }) {
},
})
.expect(200)
.then((resp) => {
.then(resp => {
customVisId = resp.body.id;
});
});
after(async () => {
await supertest
.delete(`/api/saved_objects/visualization/${customVisId}`)
.expect(200);
await supertest.delete(`/api/saved_objects/visualization/${customVisId}`).expect(200);
await esArchiver.unload('saved_objects/10k');
});
@ -398,13 +454,14 @@ export default function ({ getService }) {
.post('/api/saved_objects/_export')
.send({
type: ['dashboard', 'visualization', 'search', 'index-pattern'],
excludeExportDetails: true,
})
.expect(400)
.then((resp) => {
.then(resp => {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message: `Can't export more than 10000 objects`
message: `Can't export more than 10000 objects`,
});
});
});
@ -412,22 +469,24 @@ export default function ({ getService }) {
});
describe('without kibana index', () => {
before(async () => (
// just in case the kibana server has recreated it
await es.indices.delete({
index: '.kibana',
ignore: [404],
})
));
before(
async () =>
// just in case the kibana server has recreated it
await es.indices.delete({
index: '.kibana',
ignore: [404],
})
);
it('should return empty response', async () => {
await supertest
.post('/api/saved_objects/_export')
.send({
type: ['index-pattern', 'search', 'visualization', 'dashboard'],
excludeExportDetails: true,
})
.expect(200)
.then((resp) => {
.then(resp => {
expect(resp.text).to.eql('');
});
});

View file

@ -138,6 +138,7 @@ describe('copySavedObjectsToSpaces', () => {
Array [
Array [
Object {
"excludeExportDetails": true,
"exportSizeLimit": 1000,
"includeReferencesDeep": true,
"namespace": "sourceSpace",

View file

@ -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,

View file

@ -158,6 +158,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
Array [
Array [
Object {
"excludeExportDetails": true,
"exportSizeLimit": 1000,
"includeReferencesDeep": true,
"namespace": "sourceSpace",

View file

@ -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,

View file

@ -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": "オプション",

View file

@ -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": "选项",

View file

@ -104,6 +104,7 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
.post(`${getUrlPrefix(spaceId)}/api/saved_objects/_export`)
.send({
type: 'visualization',
excludeExportDetails: true,
})
.auth(user.username, user.password)
.expect(tests.spaceAwareType.statusCode)
@ -120,6 +121,7 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`,
},
],
excludeExportDetails: true,
})
.auth(user.username, user.password)
.expect(tests.spaceAwareType.statusCode)
@ -137,6 +139,7 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
id: `hiddentype_1`,
},
],
excludeExportDetails: true,
})
.auth(user.username, user.password)
.expect(tests.hiddenType.statusCode)