mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
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:
parent
5e32992a54
commit
962722ae8a
38 changed files with 2411 additions and 628 deletions
|
@ -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`] = `
|
||||
|
|
|
@ -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' });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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 {}}
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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>,
|
||||
]
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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:
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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 });
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -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
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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';
|
||||
|
|
34
src/legacy/server/saved_objects/routes/log_legacy_import.ts
Normal file
34
src/legacy/server/saved_objects/routes/log_legacy_import.ts
Normal 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 };
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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}
|
|
@ -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}
|
|
@ -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}
|
|
@ -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}
|
|
@ -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
|
@ -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}
|
|
@ -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}
|
|
@ -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": "确认所有更改",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue