Convert saved objects UI to use new import / export API (#33513)

* Initial work converting UI to use new server side APIs

* Remove missed file

* Fix jest tests

* Code cleanup pt1

* Fix file casing

* Fix jest tests

* Modify UI to support including nested references

* Fix button layout

* Connect includeReferencesDeep, remove references_missing_references logic

* Fix broken tests

* Cleanup

* Display success notifications and auto close modals on export

* More code cleanup

* Log to server when user imports using legacy .json file

* Final cleanup

* Update test snapshots to match updated text

* Apply PR feedback pt1

* Remove isLoading and wasImportSuccessful state variables, use single status variable instead

* Move business logic out of flyout component

* Apply PR feedback

* Update wordings
This commit is contained in:
Mike Côté 2019-04-09 12:02:54 -04:00 committed by GitHub
parent 5e32992a54
commit 962722ae8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 2411 additions and 628 deletions

View file

@ -77,76 +77,129 @@ exports[`ObjectsTable delete should show a confirm modal 1`] = `
`;
exports[`ObjectsTable export should allow the user to choose when exporting all 1`] = `
<EuiConfirmModal
buttonColor="primary"
cancelButtonText={
<FormattedMessage
defaultMessage="Cancel"
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.cancelButtonLabel"
values={Object {}}
/>
}
confirmButtonText={
<FormattedMessage
defaultMessage="Export All"
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel"
values={Object {}}
/>
}
defaultFocusedButton="confirm"
onCancel={[Function]}
onConfirm={[Function]}
title={
<FormattedMessage
defaultMessage="Export {filteredItemCount, plural, one{# object} other {# objects}}"
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalTitle"
values={
Object {
"filteredItemCount": 4,
}
}
/>
}
<EuiModal
maxWidth={true}
onClose={[Function]}
>
<p>
<FormattedMessage
defaultMessage="Select which types to export. The number in parentheses indicates how many of this type are available to export."
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalDescription"
values={Object {}}
/>
</p>
<EuiCheckboxGroup
idToSelectedMap={
Object {
"dashboard": true,
"index-pattern": true,
"search": true,
"visualization": true,
}
}
onChange={[Function]}
options={
Array [
Object {
"id": "index-pattern",
"label": "index-pattern (0)",
},
Object {
"id": "visualization",
"label": "visualization (0)",
},
Object {
"id": "dashboard",
"label": "dashboard (0)",
},
Object {
"id": "search",
"label": "search (0)",
},
]
}
/>
</EuiConfirmModal>
<EuiModalHeader>
<EuiModalHeaderTitle>
<FormattedMessage
defaultMessage="Export {filteredItemCount, plural, one{# object} other {# objects}}"
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalTitle"
values={
Object {
"filteredItemCount": 4,
}
}
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiText
grow={true}
size="m"
>
<p>
<FormattedMessage
defaultMessage="Select which types to export."
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalDescription"
values={Object {}}
/>
</p>
<EuiCheckboxGroup
idToSelectedMap={
Object {
"dashboard": true,
"index-pattern": true,
"search": true,
"visualization": true,
}
}
onChange={[Function]}
options={
Array [
Object {
"id": "index-pattern",
"label": "index-pattern (0)",
},
Object {
"id": "visualization",
"label": "visualization (0)",
},
Object {
"id": "dashboard",
"label": "dashboard (0)",
},
Object {
"id": "search",
"label": "search (0)",
},
]
}
/>
</EuiText>
</EuiModalBody>
<EuiModalFooter>
<EuiFlexGroup
justifyContent="spaceBetween"
>
<EuiFlexItem>
<EuiSwitch
checked={true}
label={
<FormattedMessage
defaultMessage="Include related objects"
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel"
values={Object {}}
/>
}
name="includeReferencesDeep"
onChange={[Function]}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiFlexGroup>
<EuiFlexItem
grow={false}
>
<EuiButtonEmpty
color="primary"
iconSide="left"
onClick={[Function]}
type="button"
>
<FormattedMessage
defaultMessage="Cancel"
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.cancelButtonLabel"
values={Object {}}
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiButton
color="primary"
fill={true}
iconSide="left"
onClick={[Function]}
size="m"
type="button"
>
<FormattedMessage
defaultMessage="Export all"
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel"
values={Object {}}
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalFooter>
</EuiModal>
`;
exports[`ObjectsTable import should show the flyout 1`] = `

View file

@ -24,6 +24,8 @@ import { ObjectsTable, INCLUDED_TYPES } from '../objects_table';
import { Flyout } from '../components/flyout/';
import { Relationships } from '../components/relationships/';
jest.mock('ui/kfetch', () => jest.fn());
jest.mock('../components/header', () => ({
Header: () => 'Header',
}));
@ -45,12 +47,12 @@ jest.mock('ui/chrome', () => ({
addBasePath: () => ''
}));
jest.mock('../../../lib/retrieve_and_export_docs', () => ({
retrieveAndExportDocs: jest.fn(),
jest.mock('../../../lib/fetch_export_objects', () => ({
fetchExportObjects: jest.fn(),
}));
jest.mock('../../../lib/scan_all_types', () => ({
scanAllTypes: jest.fn(),
jest.mock('../../../lib/fetch_export_by_type', () => ({
fetchExportByType: jest.fn(),
}));
jest.mock('../../../lib/get_saved_object_counts', () => ({
@ -64,8 +66,8 @@ jest.mock('../../../lib/get_saved_object_counts', () => ({
})
}));
jest.mock('../../../lib/save_to_file', () => ({
saveToFile: jest.fn(),
jest.mock('@elastic/filesaver', () => ({
saveAs: jest.fn(),
}));
jest.mock('../../../lib/get_relationships', () => ({
@ -147,6 +149,7 @@ const defaultProps = {
};
let addDangerMock;
let addSuccessMock;
describe('ObjectsTable', () => {
beforeEach(() => {
@ -159,8 +162,10 @@ describe('ObjectsTable', () => {
return debounced;
};
addDangerMock = jest.fn();
addSuccessMock = jest.fn();
require('ui/notify').toastNotifications = {
addDanger: addDangerMock,
addSuccess: addSuccessMock,
};
});
@ -222,7 +227,7 @@ describe('ObjectsTable', () => {
}))
};
const { retrieveAndExportDocs } = require('../../../lib/retrieve_and_export_docs');
const { fetchExportObjects } = require('../../../lib/fetch_export_objects');
const component = shallowWithIntl(
<ObjectsTable.WrappedComponent
@ -239,10 +244,10 @@ describe('ObjectsTable', () => {
// Set some as selected
component.instance().onSelectionChanged(mockSelectedSavedObjects);
await component.instance().onExport();
await component.instance().onExport(true);
expect(mockSavedObjectsClient.bulkGet).toHaveBeenCalledWith(mockSelectedSavedObjects);
expect(retrieveAndExportDocs).toHaveBeenCalledWith(mockSavedObjects, mockSavedObjectsClient);
expect(fetchExportObjects).toHaveBeenCalledWith(mockSelectedSavedObjects, true);
expect(addSuccessMock).toHaveBeenCalledWith({ title: 'Your file is downloading in the background' });
});
it('should allow the user to choose when exporting all', async () => {
@ -260,12 +265,12 @@ describe('ObjectsTable', () => {
component.find('Header').prop('onExportAll')();
component.update();
expect(component.find('EuiConfirmModal')).toMatchSnapshot();
expect(component.find('EuiModal')).toMatchSnapshot();
});
it('should export all', async () => {
const { scanAllTypes } = require('../../../lib/scan_all_types');
const { saveToFile } = require('../../../lib/save_to_file');
const { fetchExportByType } = require('../../../lib/fetch_export_by_type');
const { saveAs } = require('@elastic/filesaver');
const component = shallowWithIntl(
<ObjectsTable.WrappedComponent
{...defaultProps}
@ -278,12 +283,14 @@ describe('ObjectsTable', () => {
component.update();
// Set up mocks
scanAllTypes.mockImplementation(() => allSavedObjects);
const blob = new Blob([JSON.stringify(allSavedObjects)], { type: 'application/ndjson' });
fetchExportByType.mockImplementation(() => blob);
await component.instance().onExportAll();
expect(scanAllTypes).toHaveBeenCalledWith(defaultProps.$http, INCLUDED_TYPES);
expect(saveToFile).toHaveBeenCalledWith(JSON.stringify(allSavedObjects, null, 2));
expect(fetchExportByType).toHaveBeenCalledWith(INCLUDED_TYPES, true);
expect(saveAs).toHaveBeenCalledWith(blob, 'export.ndjson');
expect(addSuccessMock).toHaveBeenCalledWith({ title: 'Your file is downloading in the background' });
});
});

View file

@ -22,43 +22,342 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = `
/>
</h2>
</EuiTitle>
<EuiSpacer
size="s"
<span>
<EuiSpacer
size="s"
/>
<EuiCallOut
color="warning"
iconType="help"
size="m"
title={
<FormattedMessage
defaultMessage="Index Pattern Conflicts"
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsTitle"
values={Object {}}
/>
}
>
<p>
<FormattedMessage
defaultMessage="The following saved objects use index patterns that do not exist. Please select the index patterns you'd like re-associated with them. You can {indexPatternLink} if necessary."
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsDescription"
values={
Object {
"indexPatternLink": <EuiLink
color="primary"
href=""
type="button"
>
<FormattedMessage
defaultMessage="create a new index pattern"
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsCalloutLinkText"
values={Object {}}
/>
</EuiLink>,
}
}
/>
</p>
</EuiCallOut>
</span>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiInMemoryTable
columns={
Array [
Object {
"description": "ID of the index pattern",
"field": "existingIndexPatternId",
"name": "ID",
"sortable": true,
},
Object {
"description": "How many affected objects",
"field": "list",
"name": "Count",
"render": [Function],
},
Object {
"description": "Sample of affected objects",
"field": "list",
"name": "Sample of affected objects",
"render": [Function],
},
Object {
"field": "existingIndexPatternId",
"name": "New index pattern",
"render": [Function],
},
]
}
executeQueryOptions={Object {}}
items={
Array [
Object {
"existingIndexPatternId": "MyIndexPattern*",
"list": Array [
Object {
"id": "1",
"title": "My Visualization",
"type": "visualization",
},
],
"newIndexPatternId": undefined,
},
]
}
pagination={
Object {
"pageSizeOptions": Array [
5,
10,
25,
],
}
}
responsive={true}
sorting={false}
/>
<EuiCallOut
color="warning"
iconType="help"
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiButtonEmpty
color="primary"
iconSide="left"
onClick={[MockFunction]}
size="s"
type="button"
>
<FormattedMessage
defaultMessage="Cancel"
id="kbn.management.objects.objectsTable.flyout.import.cancelButtonLabel"
values={Object {}}
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiButton
color="primary"
data-test-subj="importSavedObjectsConfirmBtn"
fill={true}
iconSide="left"
isLoading={false}
onClick={[Function]}
size="s"
type="button"
>
<FormattedMessage
defaultMessage="Confirm all changes"
id="kbn.management.objects.objectsTable.flyout.importSuccessful.confirmAllChangesButtonLabel"
values={Object {}}
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
`;
exports[`Flyout conflicts should allow conflict resolution 2`] = `
[MockFunction] {
"calls": Array [
Array [
Object {
"getConflictResolutions": [Function],
"state": Object {
"conflictedIndexPatterns": undefined,
"conflictedSavedObjectsLinkedToSavedSearches": undefined,
"conflictedSearchDocs": undefined,
"conflictingRecord": undefined,
"error": undefined,
"failedImports": Array [
Object {
"error": Object {
"references": Array [
Object {
"id": "MyIndexPattern*",
"type": "index-pattern",
},
],
"type": "missing_references",
},
"obj": Object {
"id": "1",
"title": "My Visualization",
"type": "visualization",
},
},
],
"file": Object {
"name": "foo.ndjson",
"path": "/home/foo.ndjson",
},
"importCount": 0,
"indexPatterns": Array [
Object {
"id": "1",
},
Object {
"id": "2",
},
],
"isLegacyFile": false,
"isOverwriteAllChecked": true,
"loadingMessage": undefined,
"status": "loading",
"unmatchedReferences": Array [
Object {
"existingIndexPatternId": "MyIndexPattern*",
"list": Array [
Object {
"id": "1",
"title": "My Visualization",
"type": "visualization",
},
],
"newIndexPatternId": "2",
},
],
},
},
],
],
"results": Array [
Object {
"type": "return",
"value": Object {
"failedImports": Array [],
"importCount": 1,
"status": "success",
},
},
],
}
`;
exports[`Flyout conflicts should handle errors 1`] = `
<EuiCallOut
color="warning"
iconType="help"
size="m"
title={
<FormattedMessage
defaultMessage="Import failed"
id="kbn.management.objects.objectsTable.flyout.importFailedTitle"
values={Object {}}
/>
}
>
<p>
<FormattedMessage
defaultMessage="Failed to import {failedImportCount} of {totalImportCount} objects. Import failed"
id="kbn.management.objects.objectsTable.flyout.importFailedDescription"
values={
Object {
"failedImportCount": 1,
"totalImportCount": 1,
}
}
/>
</p>
<p />
</EuiCallOut>
`;
exports[`Flyout legacy conflicts should allow conflict resolution 1`] = `
<EuiFlyout
closeButtonAriaLabel="Closes this dialog"
hideCloseButton={false}
maxWidth={false}
onClose={[MockFunction]}
ownFocus={false}
size="s"
>
<EuiFlyoutHeader>
<EuiTitle
size="m"
title={
textTransform="none"
>
<h2>
<FormattedMessage
defaultMessage="Index Pattern Conflicts"
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsTitle"
defaultMessage="Import saved objects"
id="kbn.management.objects.objectsTable.flyout.importSavedObjectTitle"
values={Object {}}
/>
}
>
<p>
<FormattedMessage
defaultMessage="The following saved objects use index patterns that do not exist. Please select the index patterns you'd like re-associated with them. You can {indexPatternLink} if necessary."
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsDescription"
values={
Object {
"indexPatternLink": <EuiLink
color="primary"
href=""
type="button"
>
<FormattedMessage
defaultMessage="create a new index pattern"
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsCalloutLinkText"
values={Object {}}
/>
</EuiLink>,
</h2>
</EuiTitle>
<span>
<EuiSpacer
size="s"
/>
<EuiCallOut
color="warning"
iconType="help"
size="m"
title={
<FormattedMessage
defaultMessage="Support for JSON files is going away"
id="kbn.management.objects.objectsTable.flyout.legacyFileUsedTitle"
values={Object {}}
/>
}
>
<p>
<FormattedMessage
defaultMessage="Use our updated export to generate NDJSON files, and you'll be all set."
id="kbn.management.objects.objectsTable.flyout.legacyFileUsedBody"
values={Object {}}
/>
</p>
</EuiCallOut>
</span>
<span>
<EuiSpacer
size="s"
/>
<EuiCallOut
color="warning"
iconType="help"
size="m"
title={
<FormattedMessage
defaultMessage="Index Pattern Conflicts"
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsTitle"
values={Object {}}
/>
}
>
<p>
<FormattedMessage
defaultMessage="The following saved objects use index patterns that do not exist. Please select the index patterns you'd like re-associated with them. You can {indexPatternLink} if necessary."
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsDescription"
values={
Object {
"indexPatternLink": <EuiLink
color="primary"
href=""
type="button"
>
<FormattedMessage
defaultMessage="create a new index pattern"
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsCalloutLinkText"
values={Object {}}
/>
</EuiLink>,
}
}
}
/>
</p>
</EuiCallOut>
/>
</p>
</EuiCallOut>
</span>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiInMemoryTable
@ -97,7 +396,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = `
"list": Array [
Object {
"id": "MyIndexPattern*",
"name": "MyIndexPattern*",
"title": "MyIndexPattern*",
"type": "index-pattern",
},
],
@ -164,7 +463,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = `
</EuiFlyout>
`;
exports[`Flyout conflicts should handle errors 1`] = `
exports[`Flyout legacy conflicts should handle errors 1`] = `
<EuiCallOut
color="danger"
iconType="cross"
@ -214,7 +513,7 @@ exports[`Flyout should render import step 1`] = `
hasEmptyLabelSpace={false}
label={
<FormattedMessage
defaultMessage="Please select a JSON file to import"
defaultMessage="Please select a file to import"
id="kbn.management.objects.objectsTable.flyout.selectFileToImportFormRowLabel"
values={Object {}}
/>

View file

@ -22,6 +22,8 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { Flyout } from '../flyout';
jest.mock('ui/kfetch', () => jest.fn());
jest.mock('ui/errors', () => ({
SavedObjectNotFound: class SavedObjectNotFound extends Error {
constructor(options) {
@ -35,12 +37,20 @@ jest.mock('ui/errors', () => ({
},
}));
jest.mock('../../../../../lib/import_file', () => ({
importFile: jest.fn(),
}));
jest.mock('../../../../../lib/resolve_import_errors', () => ({
resolveImportErrors: jest.fn(),
}));
jest.mock('ui/chrome', () => ({
addBasePath: () => {},
}));
jest.mock('../../../../../lib/import_file', () => ({
importFile: jest.fn(),
jest.mock('../../../../../lib/import_legacy_file', () => ({
importLegacyFile: jest.fn(),
}));
jest.mock('../../../../../lib/resolve_saved_objects', () => ({
@ -57,13 +67,19 @@ const defaultProps = {
done: jest.fn(),
services: [],
newIndexPatternUrl: '',
getConflictResolutions: jest.fn(),
indexPatterns: {
getFields: jest.fn().mockImplementation(() => [{ id: '1' }, { id: '2' }]),
},
};
const mockFile = {
path: '/home/foo.txt',
name: 'foo.ndjson',
path: '/home/foo.ndjson',
};
const legacyMockFile = {
name: 'foo.json',
path: '/home/foo.json',
};
describe('Flyout', () => {
@ -105,7 +121,7 @@ describe('Flyout', () => {
});
it('should handle invalid files', async () => {
const { importFile } = require('../../../../../lib/import_file');
const { importLegacyFile } = require('../../../../../lib/import_legacy_file');
const component = shallowWithIntl(<Flyout.WrappedComponent {...defaultProps} />);
// Ensure all promises resolve
@ -113,18 +129,18 @@ describe('Flyout', () => {
// Ensure the state changes are reflected
component.update();
importFile.mockImplementation(() => {
importLegacyFile.mockImplementation(() => {
throw new Error('foobar');
});
await component.instance().import();
await component.instance().legacyImport();
expect(component.state('error')).toBe('The file could not be processed.');
importFile.mockImplementation(() => ({
importLegacyFile.mockImplementation(() => ({
invalid: true,
}));
await component.instance().import();
await component.instance().legacyImport();
expect(component.state('error')).toBe(
'Saved objects file format is invalid and cannot be imported.'
);
@ -132,6 +148,157 @@ describe('Flyout', () => {
describe('conflicts', () => {
const { importFile } = require('../../../../../lib/import_file');
const { resolveImportErrors } = require('../../../../../lib/resolve_import_errors');
beforeEach(() => {
importFile.mockImplementation(() => ({
success: false,
successCount: 0,
errors: [
{
id: '1',
type: 'visualization',
title: 'My Visualization',
error: {
type: 'missing_references',
references: [
{
id: 'MyIndexPattern*',
type: 'index-pattern',
},
],
}
},
],
}));
resolveImportErrors.mockImplementation(() => ({
status: 'success',
importCount: 1,
failedImports: [],
}));
});
it('should figure out unmatchedReferences', async () => {
const component = shallowWithIntl(<Flyout.WrappedComponent {...defaultProps} />);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
component.setState({ file: mockFile, isLegacyFile: false });
await component.instance().import();
expect(importFile).toHaveBeenCalledWith(mockFile, true);
expect(component.state()).toMatchObject({
conflictedIndexPatterns: undefined,
conflictedSavedObjectsLinkedToSavedSearches: undefined,
conflictedSearchDocs: undefined,
importCount: 0,
status: 'idle',
error: undefined,
unmatchedReferences: [
{
existingIndexPatternId: 'MyIndexPattern*',
newIndexPatternId: undefined,
list: [
{
id: '1',
type: 'visualization',
title: 'My Visualization',
},
],
},
],
});
});
it('should allow conflict resolution', async () => {
const component = shallowWithIntl(<Flyout.WrappedComponent {...defaultProps} />);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
component.setState({ file: mockFile, isLegacyFile: false });
await component.instance().import();
// Ensure it looks right
component.update();
expect(component).toMatchSnapshot();
// Ensure we can change the resolution
component
.instance()
.onIndexChanged('MyIndexPattern*', { target: { value: '2' } });
expect(component.state('unmatchedReferences')[0].newIndexPatternId).toBe('2');
// Let's resolve now
await component
.find('EuiButton[data-test-subj="importSavedObjectsConfirmBtn"]')
.simulate('click');
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
expect(resolveImportErrors).toMatchSnapshot();
});
it('should handle errors', async () => {
const component = shallowWithIntl(<Flyout.WrappedComponent {...defaultProps} />);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
resolveImportErrors.mockImplementation(() => ({
status: 'success',
importCount: 0,
failedImports: [
{
obj: {
type: 'visualization',
id: '1',
},
error: {
type: 'unknown',
},
},
],
}));
component.setState({ file: mockFile, isLegacyFile: false });
// Go through the import flow
await component.instance().import();
component.update();
// Set a resolution
component
.instance()
.onIndexChanged('MyIndexPattern*', { target: { value: '2' } });
await component
.find('EuiButton[data-test-subj="importSavedObjectsConfirmBtn"]')
.simulate('click');
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
expect(component.state('failedImports')).toEqual([
{
error: {
type: 'unknown',
},
obj: {
id: '1',
type: 'visualization',
},
},
]);
expect(component.find('EuiFlyoutBody EuiCallOut')).toMatchSnapshot();
});
});
describe('legacy conflicts', () => {
const { importLegacyFile } = require('../../../../../lib/import_legacy_file');
const {
resolveSavedObjects,
resolveSavedSearches,
@ -175,7 +342,7 @@ describe('Flyout', () => {
const mockConflictedSearchDocs = [3];
beforeEach(() => {
importFile.mockImplementation(() => mockData);
importLegacyFile.mockImplementation(() => mockData);
resolveSavedObjects.mockImplementation(() => ({
conflictedIndexPatterns: mockConflictedIndexPatterns,
conflictedSavedObjectsLinkedToSavedSearches: mockConflictedSavedObjectsLinkedToSavedSearches,
@ -184,7 +351,7 @@ describe('Flyout', () => {
}));
});
it('should figure out conflicts', async () => {
it('should figure out unmatchedReferences', async () => {
const component = shallowWithIntl(<Flyout.WrappedComponent {...defaultProps} />);
// Ensure all promises resolve
@ -192,10 +359,10 @@ describe('Flyout', () => {
// Ensure the state changes are reflected
component.update();
component.setState({ file: mockFile });
await component.instance().import();
component.setState({ file: legacyMockFile, isLegacyFile: true });
await component.instance().legacyImport();
expect(importFile).toHaveBeenCalledWith(mockFile);
expect(importLegacyFile).toHaveBeenCalledWith(legacyMockFile);
// Remove the last element from data since it should be filtered out
expect(resolveSavedObjects).toHaveBeenCalledWith(
mockData.slice(0, 2).map((doc) => ({ ...doc, _migrationVersion: {} })),
@ -209,16 +376,16 @@ describe('Flyout', () => {
conflictedSavedObjectsLinkedToSavedSearches: mockConflictedSavedObjectsLinkedToSavedSearches,
conflictedSearchDocs: mockConflictedSearchDocs,
importCount: 2,
isLoading: false,
wasImportSuccessful: false,
conflicts: [
status: 'idle',
error: undefined,
unmatchedReferences: [
{
existingIndexPatternId: 'MyIndexPattern*',
newIndexPatternId: undefined,
list: [
{
id: 'MyIndexPattern*',
name: 'MyIndexPattern*',
title: 'MyIndexPattern*',
type: 'index-pattern',
},
],
@ -235,8 +402,8 @@ describe('Flyout', () => {
// Ensure the state changes are reflected
component.update();
component.setState({ file: mockFile });
await component.instance().import();
component.setState({ file: legacyMockFile, isLegacyFile: true });
await component.instance().legacyImport();
// Ensure it looks right
component.update();
@ -246,7 +413,7 @@ describe('Flyout', () => {
component
.instance()
.onIndexChanged('MyIndexPattern*', { target: { value: '2' } });
expect(component.state('conflicts')[0].newIndexPatternId).toBe('2');
expect(component.state('unmatchedReferences')[0].newIndexPatternId).toBe('2');
// Let's resolve now
await component
@ -283,10 +450,10 @@ describe('Flyout', () => {
throw new Error('foobar');
});
component.setState({ file: mockFile });
component.setState({ file: legacyMockFile, isLegacyFile: true });
// Go through the import flow
await component.instance().import();
await component.instance().legacyImport();
component.update();
// Set a resolution
component

View file

@ -41,8 +41,17 @@ import {
EuiCallOut,
EuiSpacer,
EuiLink,
EuiConfirmModal,
EuiOverlayMask,
EUI_MODAL_CONFIRM_BUTTON,
} from '@elastic/eui';
import { importFile } from '../../../../lib/import_file';
import {
importFile,
importLegacyFile,
resolveImportErrors,
logLegacyImport,
processImportResponse,
} from '../../../../lib';
import {
resolveSavedObjects,
resolveSavedSearches,
@ -68,15 +77,16 @@ class FlyoutUI extends Component {
conflictedIndexPatterns: undefined,
conflictedSavedObjectsLinkedToSavedSearches: undefined,
conflictedSearchDocs: undefined,
conflicts: undefined,
unmatchedReferences: undefined,
conflictingRecord: undefined,
error: undefined,
file: undefined,
importCount: 0,
indexPatterns: undefined,
isOverwriteAllChecked: true,
isLoading: false,
loadingMessage: undefined,
wasImportSuccessful: false,
isLegacyFile: false,
status: 'idle',
};
}
@ -99,22 +109,29 @@ class FlyoutUI extends Component {
};
setImportFile = ([file]) => {
this.setState({ file });
this.setState({
file,
isLegacyFile: /\.json$/i.test(file.name) || file.type === 'application/json',
});
};
/**
* Import
*
* Does the initial import of a file, resolveImportErrors then handles errors and retries
*/
import = async () => {
const { services, indexPatterns, intl } = this.props;
const { intl } = this.props;
const { file, isOverwriteAllChecked } = this.state;
this.setState({ status: 'loading', error: undefined });
this.setState({ isLoading: true, error: undefined });
let contents;
// Import the file
let response;
try {
contents = await importFile(file);
response = await importFile(file, isOverwriteAllChecked);
} catch (e) {
this.setState({
isLoading: false,
status: 'error',
error: intl.formatMessage({
id: 'kbn.management.objects.objectsTable.flyout.importFileErrorMessage',
defaultMessage: 'The file could not be processed.',
@ -123,9 +140,99 @@ class FlyoutUI extends Component {
return;
}
this.setState(processImportResponse(response), () => {
// Resolve import errors right away if there's no index patterns to match
// This will ask about overwriting each object, etc
if (this.state.unmatchedReferences.length === 0) {
this.resolveImportErrors();
}
});
}
/**
* Get Conflict Resolutions
*
* Function iterates through the objects, displays a modal for each asking the user if they wish to overwrite it or not.
*
* @param {array} objects List of objects to request the user if they wish to overwrite it
* @return {Promise<array>} An object with the key being "type:id" and value the resolution chosen by the user
*/
getConflictResolutions = async (objects) => {
const resolutions = {};
for (const { type, id, title } of objects) {
const overwrite = await new Promise((resolve) => {
this.setState({
conflictingRecord: {
id,
type,
title,
done: resolve,
},
});
});
resolutions[`${type}:${id}`] = overwrite;
this.setState({ conflictingRecord: undefined });
}
return resolutions;
}
/**
* Resolve Import Errors
*
* Function goes through the failedImports and tries to resolve the issues.
*/
resolveImportErrors = async () => {
const { intl } = this.props;
this.setState({
error: undefined,
status: 'loading',
loadingMessage: undefined,
});
try {
const updatedState = await resolveImportErrors({
state: this.state,
getConflictResolutions: this.getConflictResolutions,
});
this.setState(updatedState);
} catch (e) {
this.setState({
status: 'error',
error: intl.formatMessage({
id: 'kbn.management.objects.objectsTable.flyout.resolveImportErrorsFileErrorMessage',
defaultMessage: 'The file could not be processed.',
}),
});
}
}
legacyImport = async () => {
const { services, indexPatterns, intl } = this.props;
const { file, isOverwriteAllChecked } = this.state;
this.setState({ status: 'loading', error: undefined });
// Log warning on server, don't wait for response
logLegacyImport();
let contents;
try {
contents = await importLegacyFile(file);
} catch (e) {
this.setState({
status: 'error',
error: intl.formatMessage({
id: 'kbn.management.objects.objectsTable.flyout.importLegacyFileErrorMessage',
defaultMessage: 'The file could not be processed.',
}),
});
return;
}
if (!Array.isArray(contents)) {
this.setState({
isLoading: false,
status: 'error',
error: intl.formatMessage({
id: 'kbn.management.objects.objectsTable.flyout.invalidFormatOfImportedFileErrorMessage',
defaultMessage: 'Saved objects file format is invalid and cannot be imported.',
@ -162,7 +269,7 @@ class FlyoutUI extends Component {
const byId = groupBy(conflictedIndexPatterns, ({ obj }) =>
obj.searchSource.getOwnField('index')
);
const conflicts = Object.entries(byId).reduce(
const unmatchedReferences = Object.entries(byId).reduce(
(accum, [existingIndexPatternId, list]) => {
accum.push({
existingIndexPatternId,
@ -170,7 +277,7 @@ class FlyoutUI extends Component {
list: list.map(({ doc }) => ({
id: existingIndexPatternId,
type: doc._type,
name: doc._source.title,
title: doc._source.title,
})),
});
return accum;
@ -183,19 +290,18 @@ class FlyoutUI extends Component {
conflictedSavedObjectsLinkedToSavedSearches,
conflictedSearchDocs,
failedImports,
conflicts,
unmatchedReferences,
importCount: importedObjectCount,
isLoading: false,
wasImportSuccessful: conflicts.length === 0,
status: unmatchedReferences.length === 0 ? 'success' : 'idle',
});
};
get hasConflicts() {
return this.state.conflicts && this.state.conflicts.length > 0;
get hasUnmatchedReferences() {
return this.state.unmatchedReferences && this.state.unmatchedReferences.length > 0;
}
get resolutions() {
return this.state.conflicts.reduce(
return this.state.unmatchedReferences.reduce(
(accum, { existingIndexPatternId, newIndexPatternId }) => {
if (newIndexPatternId) {
accum.push({
@ -209,7 +315,7 @@ class FlyoutUI extends Component {
);
}
confirmImport = async () => {
confirmLegacyImport = async () => {
const {
conflictedIndexPatterns,
isOverwriteAllChecked,
@ -222,20 +328,20 @@ class FlyoutUI extends Component {
this.setState({
error: undefined,
isLoading: true,
status: 'loading',
loadingMessage: undefined,
});
let importCount = this.state.importCount;
if (this.hasConflicts) {
if (this.hasUnmatchedReferences) {
try {
const resolutions = this.resolutions;
// Do not Promise.all these calls as the order matters
this.setState({
loadingMessage: intl.formatMessage({
id: 'kbn.management.objects.objectsTable.flyout.confirmImport.resolvingConflictsLoadingMessage',
id: 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.resolvingConflictsLoadingMessage',
defaultMessage: 'Resolving conflicts…',
}),
});
@ -248,7 +354,7 @@ class FlyoutUI extends Component {
}
this.setState({
loadingMessage: intl.formatMessage({
id: 'kbn.management.objects.objectsTable.flyout.confirmImport.savingConflictsLoadingMessage',
id: 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage',
defaultMessage: 'Saving conflicts…',
}),
});
@ -258,7 +364,7 @@ class FlyoutUI extends Component {
);
this.setState({
loadingMessage: intl.formatMessage({
id: 'kbn.management.objects.objectsTable.flyout.confirmImport.savedSearchAreLinkedProperlyLoadingMessage',
id: 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage',
defaultMessage: 'Ensure saved searches are linked properly…',
}),
});
@ -270,7 +376,7 @@ class FlyoutUI extends Component {
);
this.setState({
loadingMessage: intl.formatMessage({
id: 'kbn.management.objects.objectsTable.flyout.confirmImport.retryingFailedObjectsLoadingMessage',
id: 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage',
defaultMessage: 'Retrying failed objects…',
}),
});
@ -281,20 +387,20 @@ class FlyoutUI extends Component {
} catch (e) {
this.setState({
error: e.message,
isLoading: false,
status: 'error',
loadingMessage: undefined,
});
return;
}
}
this.setState({ isLoading: false, wasImportSuccessful: true, importCount });
this.setState({ status: 'success', importCount });
};
onIndexChanged = (id, e) => {
const value = e.target.value;
this.setState(state => {
const conflictIndex = state.conflicts.findIndex(
const conflictIndex = state.unmatchedReferences.findIndex(
conflict => conflict.existingIndexPatternId === id
);
if (conflictIndex === -1) {
@ -302,23 +408,23 @@ class FlyoutUI extends Component {
}
return {
conflicts: [
...state.conflicts.slice(0, conflictIndex),
unmatchedReferences: [
...state.unmatchedReferences.slice(0, conflictIndex),
{
...state.conflicts[conflictIndex],
...state.unmatchedReferences[conflictIndex],
newIndexPatternId: value,
},
...state.conflicts.slice(conflictIndex + 1),
...state.unmatchedReferences.slice(conflictIndex + 1),
],
};
});
};
renderConflicts() {
const { conflicts } = this.state;
renderUnmatchedReferences() {
const { unmatchedReferences } = this.state;
const { intl } = this.props;
if (!conflicts) {
if (!unmatchedReferences) {
return null;
}
@ -362,7 +468,7 @@ class FlyoutUI extends Component {
render: list => {
return (
<ul style={{ listStyle: 'none' }}>
{take(list, 3).map((obj, key) => <li key={key}>{obj.name}</li>)}
{take(list, 3).map((obj, key) => <li key={key}>{obj.title}</li>)}
</ul>
);
},
@ -402,7 +508,7 @@ class FlyoutUI extends Component {
return (
<EuiInMemoryTable
items={conflicts}
items={unmatchedReferences}
columns={columns}
pagination={pagination}
/>
@ -410,9 +516,9 @@ class FlyoutUI extends Component {
}
renderError() {
const { error } = this.state;
const { error, status } = this.state;
if (!error) {
if (status !== 'error') {
return null;
}
@ -433,16 +539,17 @@ class FlyoutUI extends Component {
}
renderBody() {
const { intl } = this.props;
const {
isLoading,
status,
loadingMessage,
isOverwriteAllChecked,
wasImportSuccessful,
importCount,
failedImports = [],
isLegacyFile,
} = this.state;
if (isLoading) {
if (status === 'loading') {
return (
<EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}>
@ -456,7 +563,8 @@ class FlyoutUI extends Component {
);
}
if (failedImports.length && !this.hasConflicts) {
// Kept backwards compatible logic
if (failedImports.length && (!this.hasUnmatchedReferences || (isLegacyFile === false && status === 'success'))) {
return (
<EuiCallOut
title={(
@ -468,18 +576,36 @@ class FlyoutUI extends Component {
<p>
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.importFailedDescription"
defaultMessage="Failed to import {failedImportCount} of {totalImportCount} objects.Import failed"
defaultMessage="Failed to import {failedImportCount} of {totalImportCount} objects. Import failed"
values={{ failedImportCount: failedImports.length, totalImportCount: importCount + failedImports.length, }}
/>
</p>
<p>
{failedImports.map(({ error }) => getField(error, 'body.message', error.message || '')).join(' ')}
{failedImports.map(({ error, obj }) => {
if (error.type === 'missing_references') {
return error.references.map((reference) => {
return intl.formatMessage(
{
id: 'kbn.management.objects.objectsTable.flyout.importFailedMissingReference',
defaultMessage: '{type} [id={id}] could not locate {refType} [id={refId}]',
},
{
id: obj.id,
type: obj.type,
refId: reference.id,
refType: reference.type,
}
);
});
}
return getField(error, 'body.message', error.message || '');
}).join(' ')}
</p>
</EuiCallOut>
);
}
if (wasImportSuccessful) {
if (status === 'success') {
if (importCount === 0) {
return (
<EuiCallOut
@ -518,8 +644,8 @@ class FlyoutUI extends Component {
);
}
if (this.hasConflicts) {
return this.renderConflicts();
if (this.hasUnmatchedReferences) {
return this.renderUnmatchedReferences();
}
return (
@ -528,7 +654,7 @@ class FlyoutUI extends Component {
label={(
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.selectFileToImportFormRowLabel"
defaultMessage="Please select a JSON file to import"
defaultMessage="Please select a file to import"
/>
)}
>
@ -561,12 +687,12 @@ class FlyoutUI extends Component {
}
renderFooter() {
const { isLoading, wasImportSuccessful } = this.state;
const { status } = this.state;
const { done, close } = this.props;
let confirmButton;
if (wasImportSuccessful) {
if (status === 'success') {
confirmButton = (
<EuiButton
onClick={done}
@ -580,13 +706,13 @@ class FlyoutUI extends Component {
/>
</EuiButton>
);
} else if (this.hasConflicts) {
} else if (this.hasUnmatchedReferences) {
confirmButton = (
<EuiButton
onClick={this.confirmImport}
onClick={this.state.isLegacyFile ? this.confirmLegacyImport : this.resolveImportErrors}
size="s"
fill
isLoading={isLoading}
isLoading={status === 'loading'}
data-test-subj="importSavedObjectsConfirmBtn"
>
<FormattedMessage
@ -598,10 +724,10 @@ class FlyoutUI extends Component {
} else {
confirmButton = (
<EuiButton
onClick={this.import}
onClick={this.state.isLegacyFile ? this.legacyImport : this.import}
size="s"
fill
isLoading={isLoading}
isLoading={status === 'loading'}
data-test-subj="importSavedObjectsImportBtn"
>
<FormattedMessage
@ -629,16 +755,38 @@ class FlyoutUI extends Component {
renderSubheader() {
if (
!this.hasConflicts ||
this.state.isLoading ||
this.state.wasImportSuccessful
this.state.status === 'loading' ||
this.state.status === 'success'
) {
return null;
}
return (
<Fragment>
<EuiSpacer size="s" />
let legacyFileWarning;
if (this.state.isLegacyFile) {
legacyFileWarning = (
<EuiCallOut
title={(
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.legacyFileUsedTitle"
defaultMessage="Support for JSON files is going away"
/>
)}
color="warning"
iconType="help"
>
<p>
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.legacyFileUsedBody"
defaultMessage="Use our updated export to generate NDJSON files, and you'll be all set."
/>
</p>
</EuiCallOut>
);
}
let indexPatternConflictsWarning;
if (this.hasUnmatchedReferences) {
indexPatternConflictsWarning = (
<EuiCallOut
title={(
<FormattedMessage
@ -667,13 +815,80 @@ class FlyoutUI extends Component {
}}
/>
</p>
</EuiCallOut>
</EuiCallOut>);
}
if (!legacyFileWarning && !indexPatternConflictsWarning) {
return null;
}
return (
<Fragment>
{legacyFileWarning &&
<span>
<EuiSpacer size="s" />
{legacyFileWarning}
</span>
}
{indexPatternConflictsWarning &&
<span>
<EuiSpacer size="s" />
{indexPatternConflictsWarning}
</span>
}
</Fragment>
);
}
overwriteConfirmed() {
this.state.conflictingRecord.done(true);
}
overwriteSkipped() {
this.state.conflictingRecord.done(false);
}
render() {
const { close } = this.props;
const { close, intl } = this.props;
let confirmOverwriteModal;
if (this.state.conflictingRecord) {
confirmOverwriteModal = (
<EuiOverlayMask>
<EuiConfirmModal
title={intl.formatMessage(
{
id: 'kbn.management.objects.objectsTable.flyout.confirmOverwriteTitle',
defaultMessage: 'Overwrite {type}?'
},
{ type: this.state.conflictingRecord.type }
)}
cancelButtonText={intl.formatMessage(
{
id: 'kbn.management.objects.objectsTable.flyout.confirmOverwriteCancelButtonText',
defaultMessage: 'Cancel',
},
)}
confirmButtonText={intl.formatMessage(
{
id: 'kbn.management.objects.objectsTable.flyout.confirmOverwriteOverwriteButtonText',
defaultMessage: 'Overwrite',
},
)}
onCancel={this.overwriteSkipped.bind(this)}
onConfirm={this.overwriteConfirmed.bind(this)}
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
>
<p>
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.confirmOverwriteBody"
defaultMessage="Are you sure you want to overwrite {title}?"
values={{ title: this.state.conflictingRecord.title }}
/>
</p>
</EuiConfirmModal>
</EuiOverlayMask>);
}
return (
<EuiFlyout onClose={close} size="s">
@ -695,6 +910,7 @@ class FlyoutUI extends Component {
</EuiFlyoutBody>
<EuiFlyoutFooter>{this.renderFooter()}</EuiFlyoutFooter>
{confirmOverwriteModal}
</EuiFlyout>
);
}

View file

@ -20,6 +20,8 @@
import React from 'react';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
jest.mock('ui/kfetch', () => jest.fn());
jest.mock('ui/errors', () => ({
SavedObjectNotFound: class SavedObjectNotFound extends Error {
constructor(options) {
@ -37,6 +39,14 @@ jest.mock('ui/chrome', () => ({
addBasePath: () => ''
}));
jest.mock('../../../../../lib/fetch_export_by_type', () => ({
fetchExportByType: jest.fn(),
}));
jest.mock('../../../../../lib/fetch_export_objects', () => ({
fetchExportObjects: jest.fn(),
}));
import { Relationships } from '../relationships';
describe('Relationships', () => {

View file

@ -42,22 +42,81 @@ exports[`Table should render normally 1`] = `
values={Object {}}
/>
</EuiButton>,
<EuiButton
color="primary"
fill={false}
iconSide="left"
iconType="exportAction"
isDisabled={false}
onClick={[Function]}
size="m"
type="button"
<EuiPopover
anchorPosition="downCenter"
button={
<EuiButton
color="primary"
fill={false}
iconSide="right"
iconType="arrowDown"
isDisabled={false}
onClick={[Function]}
size="m"
type="button"
>
<FormattedMessage
defaultMessage="Export"
id="kbn.management.objects.objectsTable.table.exportPopoverButtonLabel"
values={Object {}}
/>
</EuiButton>
}
closePopover={[Function]}
hasArrow={true}
isOpen={false}
ownFocus={false}
panelPaddingSize="m"
>
<FormattedMessage
defaultMessage="Export"
id="kbn.management.objects.objectsTable.table.exportButtonLabel"
values={Object {}}
/>
</EuiButton>,
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label={
<FormattedMessage
defaultMessage="Options"
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportOptionsLabel"
values={Object {}}
/>
}
labelType="label"
>
<EuiSwitch
checked={true}
label={
<FormattedMessage
defaultMessage="Include related objects"
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel"
values={Object {}}
/>
}
name="includeReferencesDeep"
onChange={[Function]}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiButton
color="primary"
fill={true}
iconSide="left"
iconType="exportAction"
onClick={[Function]}
size="m"
type="button"
>
<FormattedMessage
defaultMessage="Export"
id="kbn.management.objects.objectsTable.table.exportButtonLabel"
values={Object {}}
/>
</EuiButton>
</EuiFormRow>
</EuiPopover>,
]
}
/>

View file

@ -22,6 +22,8 @@ import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers';
import { findTestSubject } from '@elastic/eui/lib/test';
import { keyCodes } from '@elastic/eui/lib/services';
jest.mock('ui/kfetch', () => jest.fn());
jest.mock('ui/errors', () => ({
SavedObjectNotFound: class SavedObjectNotFound extends Error {
constructor(options) {

View file

@ -28,7 +28,10 @@ import {
EuiLink,
EuiSpacer,
EuiToolTip,
EuiFormErrorText
EuiFormErrorText,
EuiPopover,
EuiSwitch,
EuiFormRow
} from '@elastic/eui';
import { getSavedObjectLabel, getSavedObjectIcon } from '../../../../lib';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
@ -65,6 +68,8 @@ class TableUI extends PureComponent {
state = {
isSearchTextValid: true,
parseErrorMessage: null,
isExportPopoverOpen: false,
isIncludeReferencesDeepChecked: true,
}
onChange = ({ query, error }) => {
@ -83,6 +88,29 @@ class TableUI extends PureComponent {
this.props.onQueryChange({ query });
}
closeExportPopover = () => {
this.setState({ isExportPopoverOpen: false });
}
toggleExportPopoverVisibility = () => {
this.setState(state => ({
isExportPopoverOpen: !state.isExportPopoverOpen
}));
}
toggleIsIncludeReferencesDeepChecked = () => {
this.setState(state => ({
isIncludeReferencesDeepChecked: !state.isIncludeReferencesDeepChecked,
}));
}
onExportClick = () => {
const { onExport } = this.props;
const { isIncludeReferencesDeepChecked } = this.state;
onExport(isIncludeReferencesDeepChecked);
this.setState({ isExportPopoverOpen: false });
}
render() {
const {
pageIndex,
@ -94,7 +122,6 @@ class TableUI extends PureComponent {
filterOptions,
selectionConfig: selection,
onDelete,
onExport,
selectedSavedObjects,
onTableChange,
goInApp,
@ -216,6 +243,20 @@ class TableUI extends PureComponent {
);
}
const button = (
<EuiButton
iconType="arrowDown"
iconSide="right"
onClick={this.toggleExportPopoverVisibility}
isDisabled={selectedSavedObjects.length === 0}
>
<FormattedMessage
id="kbn.management.objects.objectsTable.table.exportPopoverButtonLabel"
defaultMessage="Export"
/>
</EuiButton>
);
return (
<Fragment>
<EuiSearchBar
@ -235,17 +276,46 @@ class TableUI extends PureComponent {
defaultMessage="Delete"
/>
</EuiButton>,
<EuiButton
key="exportSO"
iconType="exportAction"
onClick={onExport}
isDisabled={selectedSavedObjects.length === 0}
<EuiPopover
key="exportSOOptions"
button={button}
isOpen={this.state.isExportPopoverOpen}
closePopover={this.closeExportPopover}
>
<FormattedMessage
id="kbn.management.objects.objectsTable.table.exportButtonLabel"
defaultMessage="Export"
/>
</EuiButton>,
<EuiFormRow
label={(
<FormattedMessage
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportOptionsLabel"
defaultMessage="Options"
/>
)}
>
<EuiSwitch
name="includeReferencesDeep"
label={(
<FormattedMessage
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel"
defaultMessage="Include related objects"
/>
)}
checked={this.state.isIncludeReferencesDeepChecked}
onChange={this.toggleIsIncludeReferencesDeepChecked}
/>
</EuiFormRow>
<EuiFormRow>
<EuiButton
key="exportSO"
iconType="exportAction"
onClick={this.onExportClick}
fill
>
<FormattedMessage
id="kbn.management.objects.objectsTable.table.exportButtonLabel"
defaultMessage="Export"
/>
</EuiButton>
</EuiFormRow>
</EuiPopover>,
]}
/>
{queryParseError}

View file

@ -17,9 +17,10 @@
* under the License.
*/
import { saveAs } from '@elastic/filesaver';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { debounce, flattenDeep } from 'lodash';
import { debounce } from 'lodash';
import { Header } from './components/header';
import { Flyout } from './components/flyout';
import { Relationships } from './components/relationships';
@ -38,16 +39,26 @@ import {
EuiCheckboxGroup,
EuiToolTip,
EuiPageContent,
EuiSwitch,
EuiModal,
EuiModalHeader,
EuiModalBody,
EuiModalFooter,
EuiButtonEmpty,
EuiButton,
EuiModalHeaderTitle,
EuiText,
EuiFlexGroup,
EuiFlexItem
} from '@elastic/eui';
import {
retrieveAndExportDocs,
scanAllTypes,
saveToFile,
parseQuery,
getSavedObjectIcon,
getSavedObjectCounts,
getRelationships,
getSavedObjectLabel,
fetchExportObjects,
fetchExportByType,
} from '../../lib';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
@ -97,6 +108,7 @@ class ObjectsTableUI extends Component {
isDeleting: false,
exportAllOptions: [],
exportAllSelectedOptions: {},
isIncludeReferencesDeepChecked: true,
};
}
@ -278,17 +290,23 @@ class ObjectsTableUI extends Component {
});
};
onExport = async () => {
const { savedObjectsClient } = this.props;
onExport = async (includeReferencesDeep) => {
const { intl } = this.props;
const { selectedSavedObjects } = this.state;
const objects = await savedObjectsClient.bulkGet(selectedSavedObjects);
await retrieveAndExportDocs(objects.savedObjects, savedObjectsClient);
const objectsToExport = selectedSavedObjects.map(obj => ({ id: obj.id, type: obj.type }));
const blob = await fetchExportObjects(objectsToExport, includeReferencesDeep);
saveAs(blob, 'export.ndjson');
toastNotifications.addSuccess({
title: intl.formatMessage({
id: 'kbn.management.objects.objectsTable.export.successNotification',
defaultMessage: 'Your file is downloading in the background',
}),
});
};
onExportAll = async () => {
const { $http } = this.props;
const { exportAllSelectedOptions } = this.state;
const { intl } = this.props;
const { exportAllSelectedOptions, isIncludeReferencesDeepChecked } = this.state;
const exportTypes = Object.entries(exportAllSelectedOptions).reduce(
(accum, [id, selected]) => {
if (selected) {
@ -298,8 +316,15 @@ class ObjectsTableUI extends Component {
},
[]
);
const results = await scanAllTypes($http, exportTypes);
saveToFile(JSON.stringify(flattenDeep(results), null, 2));
const blob = await fetchExportByType(exportTypes, isIncludeReferencesDeepChecked);
saveAs(blob, 'export.ndjson');
toastNotifications.addSuccess({
title: intl.formatMessage({
id: 'kbn.management.objects.objectsTable.exportAll.successNotification',
defaultMessage: 'Your file is downloading in the background',
}),
});
this.setState({ isShowingExportAllOptionsModal: false });
};
finishImport = () => {
@ -512,12 +537,23 @@ class ObjectsTableUI extends Component {
);
}
changeIncludeReferencesDeep = () => {
this.setState(state => ({
isIncludeReferencesDeepChecked: !state.isIncludeReferencesDeepChecked,
}));
}
closeExportAllModal = () => {
this.setState({ isShowingExportAllOptionsModal: false });
}
renderExportAllOptionsModal() {
const {
isShowingExportAllOptionsModal,
filteredItemCount,
exportAllOptions,
exportAllSelectedOptions,
isIncludeReferencesDeepChecked,
} = this.state;
if (!isShowingExportAllOptionsModal) {
@ -526,53 +562,84 @@ class ObjectsTableUI extends Component {
return (
<EuiOverlayMask>
<EuiConfirmModal
title={(<FormattedMessage
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalTitle"
defaultMessage="Export {filteredItemCount, plural, one{# object} other {# objects}}"
values={{
filteredItemCount
}}
/>)}
onCancel={() =>
this.setState({ isShowingExportAllOptionsModal: false })
}
onConfirm={this.onExportAll}
cancelButtonText={(
<FormattedMessage id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.cancelButtonLabel" defaultMessage="Cancel"/>
)}
confirmButtonText={(
<FormattedMessage
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel"
defaultMessage="Export All"
/>
)}
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
<EuiModal
onClose={this.closeExportAllModal}
>
<p>
<FormattedMessage
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalDescription"
defaultMessage="Select which types to export. The number in parentheses indicates
how many of this type are available to export."
/>
</p>
<EuiCheckboxGroup
options={exportAllOptions}
idToSelectedMap={exportAllSelectedOptions}
onChange={optionId => {
const newExportAllSelectedOptions = {
...exportAllSelectedOptions,
...{
[optionId]: !exportAllSelectedOptions[optionId],
},
};
<EuiModalHeader>
<EuiModalHeaderTitle>
<FormattedMessage
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalTitle"
defaultMessage="Export {filteredItemCount, plural, one{# object} other {# objects}}"
values={{
filteredItemCount
}}
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiText>
<p>
<FormattedMessage
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalDescription"
defaultMessage="Select which types to export."
/>
</p>
<EuiCheckboxGroup
options={exportAllOptions}
idToSelectedMap={exportAllSelectedOptions}
onChange={optionId => {
const newExportAllSelectedOptions = {
...exportAllSelectedOptions,
...{
[optionId]: !exportAllSelectedOptions[optionId],
},
};
this.setState({
exportAllSelectedOptions: newExportAllSelectedOptions,
});
}}
/>
</EuiConfirmModal>
this.setState({
exportAllSelectedOptions: newExportAllSelectedOptions,
});
}}
/>
</EuiText>
</EuiModalBody>
<EuiModalFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<EuiSwitch
name="includeReferencesDeep"
label={(
<FormattedMessage
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel"
defaultMessage="Include related objects"
/>
)}
checked={isIncludeReferencesDeepChecked}
onChange={this.changeIncludeReferencesDeep}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={this.closeExportAllModal}>
<FormattedMessage
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill onClick={this.onExportAll}>
<FormattedMessage
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel"
defaultMessage="Export all"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalFooter>
</EuiModal>
</EuiOverlayMask>
);
}

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { importFile } from '../import_file';
import { importLegacyFile } from '../import_legacy_file';
describe('importFile', () => {
it('should import a file', async () => {
@ -33,7 +33,7 @@ describe('importFile', () => {
const file = 'foo';
const imported = await importFile(file, FileReader);
const imported = await importLegacyFile(file, FileReader);
expect(imported).toEqual({ text: file });
});
@ -51,7 +51,7 @@ describe('importFile', () => {
const file = 'foo';
try {
await importFile(file, FileReader);
await importLegacyFile(file, FileReader);
} catch (e) {
// There isn't a great way to handle throwing exceptions
// with async/await but this seems to work :shrug:

View file

@ -0,0 +1,146 @@
/*
* 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 { processImportResponse } from '../process_import_response';
describe('processImportResponse()', () => {
test('works when no errors exist in the response', () => {
const response = {
success: true,
successCount: 0,
};
const result = processImportResponse(response);
expect(result.status).toBe('success');
expect(result.importCount).toBe(0);
});
test('conflict errors get added to failedImports', () => {
const response = {
success: false,
successCount: 0,
errors: [
{
obj: {
type: 'a',
id: '1',
},
error: {
type: 'conflict',
},
},
],
};
const result = processImportResponse(response);
expect(result.failedImports).toMatchInlineSnapshot(`
Array [
Object {
"error": Object {
"type": "conflict",
},
"obj": Object {
"obj": Object {
"id": "1",
"type": "a",
},
},
},
]
`);
});
test('unknown errors get added to failedImports', () => {
const response = {
success: false,
successCount: 0,
errors: [
{
obj: {
type: 'a',
id: '1',
},
error: {
type: 'unknown',
},
},
],
};
const result = processImportResponse(response);
expect(result.failedImports).toMatchInlineSnapshot(`
Array [
Object {
"error": Object {
"type": "unknown",
},
"obj": Object {
"obj": Object {
"id": "1",
"type": "a",
},
},
},
]
`);
});
test('missing references get added to failedImports', () => {
const response = {
success: false,
successCount: 0,
errors: [
{
obj: {
type: 'a',
id: '1',
},
error: {
type: 'missing_references',
references: [
{
type: 'index-pattern',
id: '2',
},
],
},
},
],
};
const result = processImportResponse(response);
expect(result.failedImports).toMatchInlineSnapshot(`
Array [
Object {
"error": Object {
"references": Array [
Object {
"id": "2",
"type": "index-pattern",
},
],
"type": "missing_references",
},
"obj": Object {
"obj": Object {
"id": "1",
"type": "a",
},
},
},
]
`);
});
});

View file

@ -0,0 +1,379 @@
/*
* 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 { resolveImportErrors } from '../resolve_import_errors';
jest.mock('ui/kfetch', () => ({
kfetch: jest.fn(),
}));
function getFormData(form) {
const formData = {};
for (const [key, val] of form.entries()) {
if (key === 'retries') {
formData[key] = JSON.parse(val);
continue;
}
formData[key] = val;
}
return formData;
}
describe('resolveImportErrors', () => {
const getConflictResolutions = jest.fn();
beforeEach(() => {
jest.resetAllMocks();
});
test('works with empty import failures', async () => {
const result = await resolveImportErrors({
getConflictResolutions,
state: {
importCount: 0,
},
});
expect(result).toMatchInlineSnapshot(`
Object {
"failedImports": Array [],
"importCount": 0,
"status": "success",
}
`);
});
test(`doesn't retry if only unknown failures are passed in`, async () => {
const result = await resolveImportErrors({
getConflictResolutions,
state: {
importCount: 0,
failedImports: [
{
obj: {
type: 'a',
id: '1',
},
error: {
type: 'unknown',
},
},
],
},
});
expect(result).toMatchInlineSnapshot(`
Object {
"failedImports": Array [
Object {
"error": Object {
"type": "unknown",
},
"obj": Object {
"id": "1",
"type": "a",
},
},
],
"importCount": 0,
"status": "success",
}
`);
});
test('resolves conflicts', async () => {
const { kfetch } = require('ui/kfetch');
kfetch.mockResolvedValueOnce({
success: true,
successCount: 1,
});
getConflictResolutions.mockReturnValueOnce({
'a:1': true,
'a:2': false,
});
const result = await resolveImportErrors({
getConflictResolutions,
state: {
importCount: 0,
failedImports: [
{
obj: {
type: 'a',
id: '1',
},
error: {
type: 'conflict',
},
},
{
obj: {
type: 'a',
id: '2',
},
error: {
type: 'conflict',
},
},
],
},
});
expect(result).toMatchInlineSnapshot(`
Object {
"failedImports": Array [],
"importCount": 1,
"status": "success",
}
`);
const formData = getFormData(kfetch.mock.calls[0][0].body);
expect(formData).toMatchInlineSnapshot(`
Object {
"file": "undefined",
"retries": Array [
Object {
"id": "1",
"overwrite": true,
"replaceReferences": Array [],
"type": "a",
},
],
}
`);
});
test('resolves missing references', async () => {
const { kfetch } = require('ui/kfetch');
kfetch.mockResolvedValueOnce({
success: true,
successCount: 2,
});
getConflictResolutions.mockResolvedValueOnce({});
const result = await resolveImportErrors({
getConflictResolutions,
state: {
importCount: 0,
unmatchedReferences: [
{
existingIndexPatternId: '2',
newIndexPatternId: '3',
},
],
failedImports: [
{
obj: {
type: 'a',
id: '1',
},
error: {
type: 'missing_references',
references: [
{
type: 'index-pattern',
id: '2',
},
],
blocking: [
{
type: 'a',
id: '2',
},
],
},
},
],
},
});
expect(result).toMatchInlineSnapshot(`
Object {
"failedImports": Array [],
"importCount": 2,
"status": "success",
}
`);
const formData = getFormData(kfetch.mock.calls[0][0].body);
expect(formData).toMatchInlineSnapshot(`
Object {
"file": "undefined",
"retries": Array [
Object {
"id": "1",
"overwrite": false,
"replaceReferences": Array [
Object {
"from": "2",
"to": "3",
"type": "index-pattern",
},
],
"type": "a",
},
Object {
"id": "2",
"type": "a",
},
],
}
`);
});
test(`doesn't resolve missing references if newIndexPatternId isn't defined`, async () => {
getConflictResolutions.mockResolvedValueOnce({});
const result = await resolveImportErrors({
getConflictResolutions,
state: {
importCount: 0,
unmatchedReferences: [
{
existingIndexPatternId: '2',
newIndexPatternId: undefined,
},
],
failedImports: [
{
obj: {
type: 'a',
id: '1',
},
error: {
type: 'missing_references',
references: [
{
type: 'index-pattern',
id: '2',
},
],
blocking: [
{
type: 'a',
id: '2',
},
],
},
},
],
},
});
expect(result).toMatchInlineSnapshot(`
Object {
"failedImports": Array [],
"importCount": 0,
"status": "success",
}
`);
});
test('handles missing references then conflicts on the same errored objects', async () => {
const { kfetch } = require('ui/kfetch');
kfetch.mockResolvedValueOnce({
success: false,
successCount: 0,
errors: [
{
type: 'a',
id: '1',
error: {
type: 'conflict',
},
},
],
});
kfetch.mockResolvedValueOnce({
success: true,
successCount: 1,
});
getConflictResolutions.mockResolvedValueOnce({});
getConflictResolutions.mockResolvedValueOnce({
'a:1': true,
});
const result = await resolveImportErrors({
getConflictResolutions,
state: {
importCount: 0,
unmatchedReferences: [
{
existingIndexPatternId: '2',
newIndexPatternId: '3',
},
],
failedImports: [
{
obj: {
type: 'a',
id: '1',
},
error: {
type: 'missing_references',
references: [
{
type: 'index-pattern',
id: '2',
},
],
blocking: [],
},
},
],
},
});
expect(result).toMatchInlineSnapshot(`
Object {
"failedImports": Array [],
"importCount": 1,
"status": "success",
}
`);
const formData1 = getFormData(kfetch.mock.calls[0][0].body);
expect(formData1).toMatchInlineSnapshot(`
Object {
"file": "undefined",
"retries": Array [
Object {
"id": "1",
"overwrite": false,
"replaceReferences": Array [
Object {
"from": "2",
"to": "3",
"type": "index-pattern",
},
],
"type": "a",
},
],
}
`);
const formData2 = getFormData(kfetch.mock.calls[1][0].body);
expect(formData2).toMatchInlineSnapshot(`
Object {
"file": "undefined",
"retries": Array [
Object {
"id": "1",
"overwrite": true,
"replaceReferences": Array [
Object {
"from": "2",
"to": "3",
"type": "index-pattern",
},
],
"type": "a",
},
],
}
`);
});
});

View file

@ -1,108 +0,0 @@
/*
* 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 { retrieveAndExportDocs } from '../retrieve_and_export_docs';
jest.mock('../save_to_file', () => ({
saveToFile: jest.fn(),
}));
jest.mock('ui/errors', () => ({
SavedObjectNotFound: class SavedObjectNotFound extends Error {
constructor(options) {
super();
for (const option in options) {
if (options.hasOwnProperty(option)) {
this[option] = options[option];
}
}
}
},
}));
jest.mock('ui/chrome', () => ({
addBasePath: () => {},
}));
describe('retrieveAndExportDocs', () => {
let saveToFile;
beforeEach(() => {
saveToFile = require('../save_to_file').saveToFile;
saveToFile.mockClear();
});
it('should fetch all', async () => {
const savedObjectsClient = {
bulkGet: jest.fn().mockImplementation(() => ({
savedObjects: [],
})),
};
const objs = [1, 2, 3];
await retrieveAndExportDocs(objs, savedObjectsClient);
expect(savedObjectsClient.bulkGet.mock.calls.length).toBe(1);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith(objs);
});
it('should use the saveToFile utility', async () => {
const savedObjectsClient = {
bulkGet: jest.fn().mockImplementation(() => ({
savedObjects: [
{
id: 1,
type: 'index-pattern',
attributes: {
title: 'foobar',
},
},
{
id: 2,
type: 'search',
attributes: {
title: 'just the foo',
},
},
],
})),
};
const objs = [1, 2, 3];
await retrieveAndExportDocs(objs, savedObjectsClient);
expect(saveToFile.mock.calls.length).toBe(1);
expect(saveToFile).toHaveBeenCalledWith(
JSON.stringify(
[
{
_id: 1,
_type: 'index-pattern',
_source: { title: 'foobar' },
},
{
_id: 2,
_type: 'search',
_source: { title: 'just the foo' },
},
],
null,
2
)
);
});
});

View file

@ -1,36 +0,0 @@
/*
* 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 { scanAllTypes } from '../scan_all_types';
jest.mock('ui/chrome', () => ({
addBasePath: () => 'apiUrl',
}));
describe('scanAllTypes', () => {
it('should call the api', async () => {
const $http = {
post: jest.fn().mockImplementation(() => ([]))
};
const typesToInclude = ['index-pattern', 'dashboard'];
await scanAllTypes($http, typesToInclude);
expect($http.post).toBeCalledWith('apiUrl/export', { typesToInclude });
});
});

View file

@ -17,22 +17,15 @@
* under the License.
*/
import { saveToFile } from '../save_to_file';
import { kfetch } from 'ui/kfetch';
jest.mock('@elastic/filesaver', () => ({
saveAs: jest.fn(),
}));
describe('saveToFile', () => {
let saveAs;
beforeEach(() => {
saveAs = require('@elastic/filesaver').saveAs;
saveAs.mockClear();
export async function fetchExportByType(types, includeReferencesDeep = false) {
return await kfetch({
method: 'POST',
pathname: '/api/saved_objects/_export',
body: JSON.stringify({
type: types,
includeReferencesDeep,
}),
});
it('should use the file saver utility', async () => {
saveToFile(JSON.stringify({ foo: 1 }));
expect(saveAs.mock.calls.length).toBe(1);
});
});
}

View file

@ -17,10 +17,15 @@
* under the License.
*/
import chrome from 'ui/chrome';
import { kfetch } from 'ui/kfetch';
const apiBase = chrome.addBasePath('/api/kibana/management/saved_objects/scroll');
export async function scanAllTypes($http, typesToInclude) {
const results = await $http.post(`${apiBase}/export`, { typesToInclude });
return results.data;
export async function fetchExportObjects(objects, includeReferencesDeep = false) {
return await kfetch({
method: 'POST',
pathname: '/api/saved_objects/_export',
body: JSON.stringify({
objects,
includeReferencesDeep,
}),
});
}

View file

@ -17,16 +17,21 @@
* under the License.
*/
export async function importFile(file, FileReader = window.FileReader) {
return new Promise((resolve, reject) => {
const fr = new FileReader();
fr.onload = ({ target: { result } }) => {
try {
resolve(JSON.parse(result));
} catch (e) {
reject(e);
}
};
fr.readAsText(file);
import { kfetch } from 'ui/kfetch';
export async function importFile(file, overwriteAll = false) {
const formData = new FormData();
formData.append('file', file);
return await kfetch({
method: 'POST',
pathname: '/api/saved_objects/_import',
body: formData,
headers: {
// Important to be undefined, it forces proper headers to be set for FormData
'Content-Type': undefined,
},
query: {
overwrite: overwriteAll
},
});
}

View file

@ -17,19 +17,16 @@
* under the License.
*/
import { saveToFile } from './';
export async function retrieveAndExportDocs(objs, savedObjectsClient) {
const response = await savedObjectsClient.bulkGet(objs);
const objects = response.savedObjects.map(obj => {
return {
_id: obj.id,
_type: obj.type,
_source: obj.attributes,
_migrationVersion: obj.migrationVersion,
_references: obj.references,
export async function importLegacyFile(file, FileReader = window.FileReader) {
return new Promise((resolve, reject) => {
const fr = new FileReader();
fr.onload = ({ target: { result } }) => {
try {
resolve(JSON.parse(result));
} catch (e) {
reject(e);
}
};
fr.readAsText(file);
});
saveToFile(JSON.stringify(objects, null, 2));
}

View file

@ -17,14 +17,17 @@
* under the License.
*/
export * from './fetch_export_by_type';
export * from './fetch_export_objects';
export * from './get_in_app_url';
export * from './get_relationships';
export * from './get_saved_object_counts';
export * from './get_saved_object_icon';
export * from './get_saved_object_label';
export * from './import_file';
export * from './import_legacy_file';
export * from './parse_query';
export * from './resolve_import_errors';
export * from './resolve_saved_objects';
export * from './retrieve_and_export_docs';
export * from './save_to_file';
export * from './scan_all_types';
export * from './log_legacy_import';
export * from './process_import_response';

View file

@ -17,9 +17,11 @@
* under the License.
*/
import { saveAs } from '@elastic/filesaver';
import { kfetch } from 'ui/kfetch';
export function saveToFile(resultsJson) {
const blob = new Blob([resultsJson], { type: 'application/json' });
saveAs(blob, 'export.json');
export async function logLegacyImport() {
return await kfetch({
method: 'POST',
pathname: '/api/saved_objects/_log_legacy_import',
});
}

View file

@ -0,0 +1,54 @@
/*
* 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 function processImportResponse(response) {
// Go through the failures and split between unmatchedReferences and failedImports
const failedImports = [];
const unmatchedReferences = new Map();
for (const { error, ...obj } of response.errors || []) {
failedImports.push({ obj, error });
if (error.type !== 'missing_references') {
continue;
}
// Currently only supports resolving references on index patterns
const indexPatternRefs = error.references.filter(ref => ref.type === 'index-pattern');
for (const missingReference of indexPatternRefs) {
const conflict = unmatchedReferences.get(`${missingReference.type}:${missingReference.id}`) || {
existingIndexPatternId: missingReference.id,
list: [],
newIndexPatternId: undefined,
};
conflict.list.push(obj);
unmatchedReferences.set(`${missingReference.type}:${missingReference.id}`, conflict);
}
}
return {
failedImports,
unmatchedReferences: Array.from(unmatchedReferences.values()),
// Import won't be successful in the scenario unmatched references exist, import API returned errors of type unknown or import API
// returned errors of type missing_references.
status: unmatchedReferences.size === 0 && !failedImports.some(issue => issue.error.type === 'conflict')
? 'success'
: 'idle',
importCount: response.successCount,
conflictedSavedObjectsLinkedToSavedSearches: undefined,
conflictedSearchDocs: undefined,
};
}

View file

@ -0,0 +1,151 @@
/*
* 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 { kfetch } from 'ui/kfetch';
async function callResolveImportErrorsApi(file, retries) {
const formData = new FormData();
formData.append('file', file);
formData.append('retries', JSON.stringify(retries));
return await kfetch({
method: 'POST',
pathname: '/api/saved_objects/_resolve_import_errors',
headers: {
// Important to be undefined, it forces proper headers to be set for FormData
'Content-Type': undefined,
},
body: formData,
});
}
function mapImportFailureToRetryObject({ failure, overwriteDecisionCache, replaceReferencesCache, state }) {
const { isOverwriteAllChecked, unmatchedReferences } = state;
const isOverwriteGranted = isOverwriteAllChecked || overwriteDecisionCache.get(`${failure.obj.type}:${failure.obj.id}`) === true;
// Conflicts wihtout overwrite granted are skipped
if (!isOverwriteGranted && failure.error.type === 'conflict') {
return;
}
// Replace references if user chose a new reference
if (failure.error.type === 'missing_references') {
const objReplaceReferences = replaceReferencesCache.get(`${failure.obj.type}:${failure.obj.id}`) || [];
const indexPatternRefs = failure.error.references.filter(obj => obj.type === 'index-pattern');
for (const reference of indexPatternRefs) {
for (const unmatchedReference of unmatchedReferences) {
const hasNewValue = !!unmatchedReference.newIndexPatternId;
const matchesIndexPatternId = unmatchedReference.existingIndexPatternId === reference.id;
if (!hasNewValue || !matchesIndexPatternId) {
continue;
}
objReplaceReferences.push({
type: 'index-pattern',
from: unmatchedReference.existingIndexPatternId,
to: unmatchedReference.newIndexPatternId,
});
}
}
replaceReferencesCache.set(`${failure.obj.type}:${failure.obj.id}`, objReplaceReferences);
// Skip if nothing to replace, the UI option selected would be --Skip Import--
if (objReplaceReferences.length === 0) {
return;
}
}
return {
id: failure.obj.id,
type: failure.obj.type,
overwrite: isOverwriteAllChecked || overwriteDecisionCache.get(`${failure.obj.type}:${failure.obj.id}`) === true,
replaceReferences: replaceReferencesCache.get(`${failure.obj.type}:${failure.obj.id}`) || [],
};
}
export async function resolveImportErrors({ getConflictResolutions, state }) {
const overwriteDecisionCache = new Map();
const replaceReferencesCache = new Map();
let { importCount: successImportCount, failedImports: importFailures = [] } = state;
const { file, isOverwriteAllChecked } = state;
const doesntHaveOverwriteDecision = ({ obj }) => {
return !overwriteDecisionCache.has(`${obj.type}:${obj.id}`);
};
const getOverwriteDecision = ({ obj }) => {
return overwriteDecisionCache.get(`${obj.type}:${obj.id}`);
};
const callMapImportFailure = (failure) => {
return mapImportFailureToRetryObject({ failure, overwriteDecisionCache, replaceReferencesCache, state });
};
const isNotSkipped = (failure) => {
return (failure.error.type !== 'conflict' && failure.error.type !== 'missing_references') ||
getOverwriteDecision(failure);
};
// Loop until all issues are resolved
while (importFailures.some(failure => ['conflict', 'missing_references'].includes(failure.error.type))) {
// Ask for overwrites
if (!isOverwriteAllChecked) {
const result = await getConflictResolutions(
importFailures
.filter(({ error }) => error.type === 'conflict')
.filter(doesntHaveOverwriteDecision)
.map(({ obj }) => obj)
);
for (const key of Object.keys(result)) {
overwriteDecisionCache.set(key, result[key]);
}
}
// Build retries array
const retries = importFailures
.map(callMapImportFailure)
.filter(obj => !!obj);
for (const { error, obj } of importFailures) {
if (error.type !== 'missing_references') {
continue;
}
if (!retries.some(retryObj => retryObj.type === obj.type && retryObj.id === obj.id)) {
continue;
}
for (const { type, id } of error.blocking || []) {
retries.push({ type, id });
}
}
// Scenario where everything is skipped and nothing to retry
if (retries.length === 0) {
// Cancelled overwrites aren't failures anymore
importFailures = importFailures.filter(isNotSkipped);
break;
}
// Call API
const response = await callResolveImportErrorsApi(file, retries);
successImportCount += response.successCount;
importFailures = [];
for (const { error, ...obj } of response.errors || []) {
importFailures.push({ error, obj });
}
}
return {
status: 'success',
importCount: successImportCount,
failedImports: importFailures,
};
}

View file

@ -24,6 +24,7 @@ export { createDeleteRoute } from './delete';
export { createFindRoute } from './find';
export { createGetRoute } from './get';
export { createImportRoute } from './import';
export { createLogLegacyImportRoute } from './log_legacy_import';
export { createResolveImportErrorsRoute } from './resolve_import_errors';
export { createUpdateRoute } from './update';
export { createExportRoute } from './export';

View file

@ -0,0 +1,34 @@
/*
* 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 Hapi from 'hapi';
export const createLogLegacyImportRoute = () => ({
path: '/api/saved_objects/_log_legacy_import',
method: 'POST',
options: {
handler(request: Hapi.Request) {
request.server.log(
['warning'],
'Importing saved objects from a .json file has been deprecated'
);
return { success: true };
},
},
});

View file

@ -38,6 +38,7 @@ import {
createExportRoute,
createImportRoute,
createResolveImportErrorsRoute,
createLogLegacyImportRoute,
} from './routes';
export function savedObjectsMixin(kbnServer, server) {
@ -71,6 +72,7 @@ export function savedObjectsMixin(kbnServer, server) {
server.route(createExportRoute(prereqs, server));
server.route(createImportRoute(prereqs, server));
server.route(createResolveImportErrorsRoute(prereqs, server));
server.route(createLogLegacyImportRoute());
const schema = new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas);
const serializer = new SavedObjectsSerializer(schema);

View file

@ -110,9 +110,9 @@ describe('Saved Objects Mixin', () => {
});
describe('Routes', () => {
it('should create 10 routes', () => {
it('should create 11 routes', () => {
savedObjectsMixin(mockKbnServer, mockServer);
expect(mockServer.route).toHaveBeenCalledTimes(10);
expect(mockServer.route).toHaveBeenCalledTimes(11);
});
it('should add POST /api/saved_objects/_bulk_create', () => {
savedObjectsMixin(mockKbnServer, mockServer);
@ -177,6 +177,12 @@ describe('Saved Objects Mixin', () => {
})
);
});
it('should add POST /api/saved_objects/_log_legacy_import', () => {
savedObjectsMixin(mockKbnServer, mockServer);
expect(mockServer.route).toHaveBeenCalledWith(
expect.objectContaining({ path: '/api/saved_objects/_log_legacy_import', method: 'POST' })
);
});
});
describe('Saved object service', () => {

View file

@ -61,11 +61,14 @@ export async function kfetch(
});
return window.fetch(fullUrl, restOptions).then(async res => {
const body = await getBodyAsJson(res);
if (res.ok) {
return body;
if (!res.ok) {
throw new KFetchError(res, await getBodyAsJson(res));
}
throw new KFetchError(res, body);
const contentType = res.headers.get('content-type');
if (contentType && contentType.split(';')[0] === 'application/ndjson') {
return await getBodyAsBlob(res);
}
return await getBodyAsJson(res);
});
}
);
@ -96,13 +99,25 @@ async function getBodyAsJson(res: Response) {
}
}
async function getBodyAsBlob(res: Response) {
try {
return await res.blob();
} catch (e) {
return null;
}
}
export function withDefaultOptions(options?: KFetchOptions): KFetchOptions {
return merge(
{
method: 'GET',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
...(options && options.headers && options.headers.hasOwnProperty('Content-Type')
? {}
: {
'Content-Type': 'application/json',
}),
'kbn-version': metadata.version,
},
},

View file

@ -27,157 +27,331 @@ export default function ({ getService, getPageObjects }) {
const testSubjects = getService('testSubjects');
describe('import objects', function describeIndexTests() {
beforeEach(async function () {
// delete .kibana index and then wait for Kibana to re-create it
await kibanaServer.uiSettings.replace({});
await PageObjects.settings.navigateTo();
await esArchiver.load('management');
describe('.ndjson file', () => {
beforeEach(async function () {
// delete .kibana index and then wait for Kibana to re-create it
await kibanaServer.uiSettings.replace({});
await PageObjects.settings.navigateTo();
await esArchiver.load('management');
});
afterEach(async function () {
await esArchiver.unload('management');
});
it('should import saved objects', async function () {
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects.ndjson'));
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickImportDone();
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('Log Agents');
expect(isSavedObjectImported).to.be(true);
});
it('should provide dialog to allow the importing of saved objects with index pattern conflicts', async function () {
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_conflicts.ndjson'));
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.associateIndexPattern('d1e4c910-a2e6-11e7-bb30-233be9be6a15', 'logstash-*');
await PageObjects.settings.clickConfirmChanges();
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickImportDone();
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('saved object with index pattern conflict');
expect(isSavedObjectImported).to.be(true);
});
it('should allow the user to override duplicate saved objects', async function () {
await PageObjects.settings.clickKibanaSavedObjects();
// This data has already been loaded by the "visualize" esArchive. We'll load it again
// so that we can override the existing visualization.
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.ndjson'), false);
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
await PageObjects.settings.clickConfirmChanges();
// Override the visualization.
await PageObjects.common.clickConfirmOnModal();
const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess');
expect(isSuccessful).to.be(true);
});
it('should allow the user to cancel overriding duplicate saved objects', async function () {
await PageObjects.settings.clickKibanaSavedObjects();
// This data has already been loaded by the "visualize" esArchive. We'll load it again
// so that we can be prompted to override the existing visualization.
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.ndjson'), false);
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
await PageObjects.settings.clickConfirmChanges();
// *Don't* override the visualization.
await PageObjects.common.clickCancelOnModal();
const isSuccessful = await testSubjects.exists('importSavedObjectsSuccessNoneImported');
expect(isSuccessful).to.be(true);
});
it('should import saved objects linked to saved searches', async function () {
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_saved_search.ndjson'));
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickImportDone();
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.ndjson'));
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickImportDone();
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('saved object connected to saved search');
expect(isSavedObjectImported).to.be(true);
});
it('should not import saved objects linked to saved searches when saved search does not exist', async function () {
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.ndjson'));
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickImportDone();
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('saved object connected to saved search');
expect(isSavedObjectImported).to.be(false);
});
it('should not import saved objects linked to saved searches when saved search index pattern does not exist', async function () {
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaIndexPatterns();
await PageObjects.settings.clickIndexPatternLogstash();
await PageObjects.settings.removeIndexPattern();
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_saved_search.ndjson'));
// Wait for all the saves to happen
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickConfirmChanges();
await PageObjects.settings.clickImportDone();
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('saved object connected to saved search');
expect(isSavedObjectImported).to.be(false);
});
it('should import saved objects with index patterns when index patterns already exists', async () => {
// First, import the objects
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_index_patterns.ndjson'));
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickImportDone();
// Wait for all the saves to happen
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('saved object imported with index pattern');
expect(isSavedObjectImported).to.be(true);
});
it('should import saved objects with index patterns when index patterns does not exists', async () => {
// First, we need to delete the index pattern
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaIndexPatterns();
await PageObjects.settings.clickIndexPatternLogstash();
await PageObjects.settings.removeIndexPattern();
// Then, import the objects
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_index_patterns.ndjson'));
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickImportDone();
// Wait for all the saves to happen
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('saved object imported with index pattern');
expect(isSavedObjectImported).to.be(true);
});
});
afterEach(async function () {
await esArchiver.unload('management');
});
describe('.json file', () => {
beforeEach(async function () {
// delete .kibana index and then wait for Kibana to re-create it
await kibanaServer.uiSettings.replace({});
await PageObjects.settings.navigateTo();
await esArchiver.load('management');
});
it('should import saved objects', async function () {
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects.json'));
await PageObjects.settings.clickImportDone();
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('Log Agents');
expect(isSavedObjectImported).to.be(true);
});
afterEach(async function () {
await esArchiver.unload('management');
});
it('should provide dialog to allow the importing of saved objects with index pattern conflicts', async function () {
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects-conflicts.json'));
await PageObjects.settings.associateIndexPattern('d1e4c910-a2e6-11e7-bb30-233be9be6a15', 'logstash-*');
await PageObjects.settings.clickConfirmChanges();
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickImportDone();
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('saved object with index pattern conflict');
expect(isSavedObjectImported).to.be(true);
});
it('should import saved objects', async function () {
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects.json'));
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickImportDone();
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('Log Agents');
expect(isSavedObjectImported).to.be(true);
});
it('should allow the user to override duplicate saved objects', async function () {
await PageObjects.settings.clickKibanaSavedObjects();
it('should provide dialog to allow the importing of saved objects with index pattern conflicts', async function () {
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects-conflicts.json'));
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.associateIndexPattern('d1e4c910-a2e6-11e7-bb30-233be9be6a15', 'logstash-*');
await PageObjects.settings.clickConfirmChanges();
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickImportDone();
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('saved object with index pattern conflict');
expect(isSavedObjectImported).to.be(true);
});
// This data has already been loaded by the "visualize" esArchive. We'll load it again
// so that we can override the existing visualization.
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.json'), false);
it('should allow the user to override duplicate saved objects', async function () {
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
await PageObjects.settings.clickConfirmChanges();
// This data has already been loaded by the "visualize" esArchive. We'll load it again
// so that we can override the existing visualization.
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.json'), false);
// Override the visualization.
await PageObjects.common.clickConfirmOnModal();
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
await PageObjects.settings.clickConfirmChanges();
const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess');
expect(isSuccessful).to.be(true);
});
// Override the visualization.
await PageObjects.common.clickConfirmOnModal();
it('should allow the user to cancel overriding duplicate saved objects', async function () {
await PageObjects.settings.clickKibanaSavedObjects();
const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess');
expect(isSuccessful).to.be(true);
});
// This data has already been loaded by the "visualize" esArchive. We'll load it again
// so that we can be prompted to override the existing visualization.
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.json'), false);
it('should allow the user to cancel overriding duplicate saved objects', async function () {
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
await PageObjects.settings.clickConfirmChanges();
// This data has already been loaded by the "visualize" esArchive. We'll load it again
// so that we can be prompted to override the existing visualization.
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.json'), false);
// *Don't* override the visualization.
await PageObjects.common.clickCancelOnModal();
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
await PageObjects.settings.clickConfirmChanges();
const isSuccessful = await testSubjects.exists('importSavedObjectsSuccessNoneImported');
expect(isSuccessful).to.be(true);
});
// *Don't* override the visualization.
await PageObjects.common.clickCancelOnModal();
it('should import saved objects linked to saved searches', async function () {
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_saved_search.json'));
await PageObjects.settings.clickImportDone();
const isSuccessful = await testSubjects.exists('importSavedObjectsSuccessNoneImported');
expect(isSuccessful).to.be(true);
});
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'));
await PageObjects.settings.clickImportDone();
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
it('should import saved objects linked to saved searches', async function () {
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_saved_search.json'));
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickImportDone();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('saved object connected to saved search');
expect(isSavedObjectImported).to.be(true);
});
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'));
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickImportDone();
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
it('should not import saved objects linked to saved searches when saved search does not exist', async function () {
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'));
await PageObjects.settings.clickImportDone();
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('saved object connected to saved search');
expect(isSavedObjectImported).to.be(true);
});
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('saved object connected to saved search');
expect(isSavedObjectImported).to.be(false);
});
it('should not import saved objects linked to saved searches when saved search does not exist', async function () {
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'));
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickImportDone();
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
it('should not import saved objects linked to saved searches when saved search index pattern does not exist', async function () {
// First, import the saved search
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_saved_search.json'));
await PageObjects.settings.clickImportDone();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('saved object connected to saved search');
expect(isSavedObjectImported).to.be(false);
});
// Second, we need to delete the index pattern
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaIndexPatterns();
await PageObjects.settings.clickIndexPatternLogstash();
await PageObjects.settings.removeIndexPattern();
it('should not import saved objects linked to saved searches when saved search index pattern does not exist', async function () {
// First, import the saved search
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_saved_search.json'));
// Wait for all the saves to happen
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickImportDone();
// Last, import a saved object connected to the saved search
// This should NOT show the conflicts
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'));
await PageObjects.settings.clickImportDone();
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
// Second, we need to delete the index pattern
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaIndexPatterns();
await PageObjects.settings.clickIndexPatternLogstash();
await PageObjects.settings.removeIndexPattern();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('saved object connected to saved search');
expect(isSavedObjectImported).to.be(false);
});
// Last, import a saved object connected to the saved search
// This should NOT show the conflicts
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'));
// Wait for all the saves to happen
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickImportDone();
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
it('should import saved objects with index patterns when index patterns already exists', async () => {
// First, import the objects
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json'));
await PageObjects.settings.clickImportDone();
// Wait for all the saves to happen
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('saved object connected to saved search');
expect(isSavedObjectImported).to.be(false);
});
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('saved object imported with index pattern');
expect(isSavedObjectImported).to.be(true);
});
it('should import saved objects with index patterns when index patterns already exists', async () => {
// First, import the objects
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json'));
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickImportDone();
// Wait for all the saves to happen
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
it('should import saved objects with index patterns when index patterns does not exists', async () => {
// First, we need to delete the index pattern
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaIndexPatterns();
await PageObjects.settings.clickIndexPatternLogstash();
await PageObjects.settings.removeIndexPattern();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('saved object imported with index pattern');
expect(isSavedObjectImported).to.be(true);
});
// Then, import the objects
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json'));
await PageObjects.settings.clickImportDone();
// Wait for all the saves to happen
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
it('should import saved objects with index patterns when index patterns does not exists', async () => {
// First, we need to delete the index pattern
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaIndexPatterns();
await PageObjects.settings.clickIndexPatternLogstash();
await PageObjects.settings.removeIndexPattern();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('saved object imported with index pattern');
expect(isSavedObjectImported).to.be(true);
// Then, import the objects
await PageObjects.settings.clickKibanaSavedObjects();
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json'));
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickImportDone();
// Wait for all the saves to happen
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
const objects = await PageObjects.settings.getSavedObjectsInTable();
const isSavedObjectImported = objects.includes('saved object imported with index pattern');
expect(isSavedObjectImported).to.be(true);
});
});
});
}

View file

@ -0,0 +1 @@
{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Log Agents","uiStateJSON":"{}","visState":"{\"title\":\"Log Agents\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{\"text\":\"agent.raw: Descending\"}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"agent.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}"},"id":"082f1d60-a2e7-11e7-bb30-233be9be6a15","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"f1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","version":1}

View file

@ -0,0 +1 @@
{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"saved object with index pattern conflict","uiStateJSON":"{}","visState":"{\"title\":\"Log Agents\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{\"text\":\"agent.raw: Descending\"}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"agent.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}"},"id":"saved_object_with_index_pattern_conflict","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"d1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","version":1}

View file

@ -0,0 +1 @@
{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"},"savedSearchRefName":"search_0","title":"saved object connected to saved search","uiStateJSON":"{}","visState":"{\"title\":\"PHP Viz\",\"type\":\"horizontal_bar\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":200},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":75,\"filter\":true,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"normal\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"},"id":"saved_object_connected_to_saved_search","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"c45e6c50-ba72-11e7-a8f9-ad70f02e633d","name":"search_0","type":"search"}],"type":"visualization","version":1}

View file

@ -0,0 +1 @@
{"attributes":{"description":"AreaChart","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Shared-Item Visualization AreaChart","uiStateJSON":"{}","visState":"{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}"},"id":"Shared-Item-Visualization-AreaChart","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"logstash-*","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","version":1}

View file

@ -0,0 +1 @@
{"attributes":{"columns":["_source"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"php\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":["@timestamp","desc"],"title":"PHP saved search"},"id":"c45e6c50-ba72-11e7-a8f9-ad70f02e633d","migrationVersion":{"search":"7.0.0"},"references":[{"id":"f1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"search","version":1}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
{"attributes":{"columns":["_source"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"php\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":["@timestamp","desc"],"title":"PHP saved search"},"id":"c45e6c50-ba72-11e7-a8f9-ad70f02e633d","migrationVersion":{"search":"7.0.0"},"references":[{"id":"f1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"search","version":1}
{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"},"savedSearchRefName":"search_0","title":"saved object connected to saved search","uiStateJSON":"{}","visState":"{\"title\":\"PHP Viz\",\"type\":\"horizontal_bar\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":200},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":75,\"filter\":true,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"normal\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"},"id":"saved_object_connected_to_saved_search","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"c45e6c50-ba72-11e7-a8f9-ad70f02e633d","name":"search_0","type":"search"}],"type":"visualization","version":1}

View file

@ -0,0 +1,2 @@
{"attributes":{"columns":["_source"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":["@timestamp","desc"],"title":"mysavedsearch"},"id":"6aea5700-ac94-11e8-a651-614b2788174a","migrationVersion":{"search":"7.0.0"},"references":[{"id":"4c3f3c30-ac94-11e8-a651-614b2788174a","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"search","version":1}
{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}"},"savedSearchRefName":"search_0","title":"mysavedviz","uiStateJSON":"{}","visState":"{\"title\":\"mysavedviz\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"},"id":"8411daa0-ac94-11e8-a651-614b2788174a","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"6aea5700-ac94-11e8-a651-614b2788174a","name":"search_0","type":"search"}],"type":"visualization","version":1}

View file

@ -1558,16 +1558,15 @@
"kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel": "全部导出",
"kbn.management.objects.objectsTable.exportObjectsConfirmModalDescription": "选择要导出的类型。括号中的数字表示可导出多少此类型的对象。",
"kbn.management.objects.objectsTable.exportObjectsConfirmModalTitle": "导出 {filteredItemCount, plural, one{# 个对象} other {# 个对象}}",
"kbn.management.objects.objectsTable.flyout.confirmImport.resolvingConflictsLoadingMessage": "正在解决冲突……",
"kbn.management.objects.objectsTable.flyout.confirmImport.retryingFailedObjectsLoadingMessage": "正在重试失败的对象……",
"kbn.management.objects.objectsTable.flyout.confirmImport.savedSearchAreLinkedProperlyLoadingMessage": "确保已保存搜索已正确链接……",
"kbn.management.objects.objectsTable.flyout.confirmImport.savingConflictsLoadingMessage": "正在保存冲突……",
"kbn.management.objects.objectsTable.flyout.confirmLegacyImport.resolvingConflictsLoadingMessage": "正在解决冲突……",
"kbn.management.objects.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage": "正在重试失败的对象……",
"kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage": "确保已保存搜索已正确链接……",
"kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage": "正在保存冲突……",
"kbn.management.objects.objectsTable.flyout.errorCalloutTitle": "抱歉,出现了错误",
"kbn.management.objects.objectsTable.flyout.import.cancelButtonLabel": "取消",
"kbn.management.objects.objectsTable.flyout.import.confirmButtonLabel": "导入",
"kbn.management.objects.objectsTable.flyout.importFailedDescription": "{totalImportCount} 个对象中有 {failedImportCount} 个无法导入。导入失败",
"kbn.management.objects.objectsTable.flyout.importFailedTitle": "导入失败",
"kbn.management.objects.objectsTable.flyout.importFileErrorMessage": "无法处理该文件。",
"kbn.management.objects.objectsTable.flyout.importPromptText": "导入",
"kbn.management.objects.objectsTable.flyout.importSavedObjectTitle": "导入已保存对象",
"kbn.management.objects.objectsTable.flyout.importSuccessful.confirmAllChangesButtonLabel": "确认所有更改",