From 6a9844c223f0cccf4ec0e6be78d7199b10136ec7 Mon Sep 17 00:00:00 2001
From: Larry Gregory
Date: Fri, 23 Aug 2019 14:24:53 -0400
Subject: [PATCH] Spaces - Copy Saved Objects to Spaces UI (#39002)
---
.../objects_table/components/flyout/flyout.js | 2 +-
.../objects_table/components/table/table.js | 28 ++
...response.js => process_import_response.ts} | 45 +-
.../saved_objects_management/index.ts | 29 ++
.../saved_objects_management_action.ts | 67 +++
...objects_management_action_registry.test.ts | 55 +++
...aved_objects_management_action_registry.ts | 37 ++
x-pack/dev-tools/jest/create_jest_config.js | 1 +
.../plugins/spaces/common/model/types.ts | 7 +
.../copy_saved_objects_to_space_action.tsx | 56 +++
.../lib/copy_saved_objects_to_space/index.ts | 8 +
.../summarize_copy_result.test.ts | 284 +++++++++++
.../summarize_copy_result.ts | 133 +++++
.../lib/copy_saved_objects_to_space/types.ts | 19 +
.../spaces/public/lib/spaces_manager.mock.ts | 8 +-
.../spaces/public/lib/spaces_manager.ts | 41 +-
.../public/views/management/_index.scss | 1 +
.../components/confirm_delete_modal.test.tsx | 5 +-
.../copy_saved_objects_to_space/_index.scss | 33 ++
.../copy_status_indicator.tsx | 96 ++++
.../copy_status_summary_indicator.tsx | 82 ++++
.../copy_to_space_flyout.test.tsx | 455 ++++++++++++++++++
.../copy_to_space_flyout.tsx | 261 ++++++++++
.../copy_to_space_flyout_footer.tsx | 199 ++++++++
.../copy_to_space_form.tsx | 83 ++++
.../copy_saved_objects_to_space/index.ts | 7 +
.../processing_copy_to_space.tsx | 115 +++++
.../selectable_spaces_control.tsx | 81 ++++
.../space_result.tsx | 71 +++
.../space_result_details.tsx | 124 +++++
.../edit_space/delete_spaces_button.test.tsx | 3 +-
.../edit_space/manage_space_page.test.tsx | 9 +-
.../spaces/public/views/management/index.tsx | 18 +-
.../spaces_grid/spaces_grid_pages.test.tsx | 5 +-
.../nav_control/nav_control_popover.test.tsx | 5 +-
.../spaces/server/lib/spaces_client/index.ts | 2 +-
.../lib/spaces_client/spaces_client.test.ts | 3 +-
.../server/lib/spaces_client/spaces_client.ts | 2 +-
.../spaces/server/routes/api/external/get.ts | 3 +-
.../apps/spaces/copy_saved_objects.ts | 119 +++++
x-pack/test/functional/apps/spaces/index.ts | 1 +
.../spaces/copy_saved_objects/data.json | 111 +++++
.../spaces/copy_saved_objects/mappings.json | 333 +++++++++++++
.../copy_saved_objects_to_space_page.ts | 97 ++++
x-pack/test/functional/page_objects/index.ts | 2 +
45 files changed, 3117 insertions(+), 29 deletions(-)
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{process_import_response.js => process_import_response.ts} (61%)
create mode 100644 src/legacy/ui/public/management/saved_objects_management/index.ts
create mode 100644 src/legacy/ui/public/management/saved_objects_management/saved_objects_management_action.ts
create mode 100644 src/legacy/ui/public/management/saved_objects_management/saved_objects_management_action_registry.test.ts
create mode 100644 src/legacy/ui/public/management/saved_objects_management/saved_objects_management_action_registry.ts
create mode 100644 x-pack/legacy/plugins/spaces/common/model/types.ts
create mode 100644 x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx
create mode 100644 x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/index.ts
create mode 100644 x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.test.ts
create mode 100644 x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.ts
create mode 100644 x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/types.ts
create mode 100644 x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/_index.scss
create mode 100644 x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_status_indicator.tsx
create mode 100644 x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_status_summary_indicator.tsx
create mode 100644 x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.test.tsx
create mode 100644 x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.tsx
create mode 100644 x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout_footer.tsx
create mode 100644 x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_form.tsx
create mode 100644 x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/index.ts
create mode 100644 x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/processing_copy_to_space.tsx
create mode 100644 x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/selectable_spaces_control.tsx
create mode 100644 x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result.tsx
create mode 100644 x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result_details.tsx
create mode 100644 x-pack/test/functional/apps/spaces/copy_saved_objects.ts
create mode 100644 x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json
create mode 100644 x-pack/test/functional/es_archives/spaces/copy_saved_objects/mappings.json
create mode 100644 x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js
index 1bdf6dede4dee..50bd0e3e993cf 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js
@@ -50,9 +50,9 @@ import {
importLegacyFile,
resolveImportErrors,
logLegacyImport,
- processImportResponse,
getDefaultTitle,
} from '../../../../lib';
+import { processImportResponse } from '../../../../lib/process_import_response';
import {
resolveSavedObjects,
resolveSavedSearches,
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js
index 8d4fce3073ba5..672cb23baf5a2 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js
@@ -18,6 +18,7 @@
*/
import chrome from 'ui/chrome';
+import { SavedObjectsManagementActionRegistry } from 'ui/management/saved_objects_management';
import React, { PureComponent, Fragment } from 'react';
import PropTypes from 'prop-types';
@@ -73,6 +74,12 @@ class TableUI extends PureComponent {
parseErrorMessage: null,
isExportPopoverOpen: false,
isIncludeReferencesDeepChecked: true,
+ activeAction: null,
+ }
+
+ constructor(props) {
+ super(props);
+ this.extraActions = SavedObjectsManagementActionRegistry.get();
}
onChange = ({ query, error }) => {
@@ -238,6 +245,24 @@ class TableUI extends PureComponent {
icon: 'kqlSelector',
onClick: object => onShowRelationships(object),
},
+ ...this.extraActions.map(action => {
+ return {
+ ...action.euiAction,
+ onClick: (object) => {
+ this.setState({
+ activeAction: action
+ });
+
+ action.registerOnFinishCallback(() => {
+ this.setState({
+ activeAction: null,
+ });
+ });
+
+ action.euiAction.onClick(object);
+ }
+ };
+ })
],
},
];
@@ -269,8 +294,11 @@ class TableUI extends PureComponent {
);
+ const activeActionContents = this.state.activeAction ? this.state.activeAction.render() : null;
+
return (
+ {activeActionContents}
;
+ error:
+ | SavedObjectsImportConflictError
+ | SavedObjectsImportUnsupportedTypeError
+ | SavedObjectsImportMissingReferencesError
+ | SavedObjectsImportUnknownError;
+ }>;
+ unmatchedReferences: Array<{
+ existingIndexPatternId: string;
+ list: Array>;
+ newIndexPatternId: string | undefined;
+ }>;
+ status: 'success' | 'idle';
+ importCount: number;
+ conflictedSavedObjectsLinkedToSavedSearches: undefined;
+ conflictedSearchDocs: undefined;
+}
+
+export function processImportResponse(
+ response: SavedObjectsImportResponse
+): ProcessedImportResponse {
// Go through the failures and split between unmatchedReferences and failedImports
const failedImports = [];
const unmatchedReferences = new Map();
@@ -29,7 +60,9 @@ export function processImportResponse(response) {
// 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}`) || {
+ const conflict = unmatchedReferences.get(
+ `${missingReference.type}:${missingReference.id}`
+ ) || {
existingIndexPatternId: missingReference.id,
list: [],
newIndexPatternId: undefined,
@@ -44,9 +77,11 @@ export function processImportResponse(response) {
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',
+ status:
+ unmatchedReferences.size === 0 &&
+ !failedImports.some(issue => issue.error.type === 'conflict')
+ ? 'success'
+ : 'idle',
importCount: response.successCount,
conflictedSavedObjectsLinkedToSavedSearches: undefined,
conflictedSearchDocs: undefined,
diff --git a/src/legacy/ui/public/management/saved_objects_management/index.ts b/src/legacy/ui/public/management/saved_objects_management/index.ts
new file mode 100644
index 0000000000000..c7223a859ee37
--- /dev/null
+++ b/src/legacy/ui/public/management/saved_objects_management/index.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 { SavedObjectsManagementActionRegistry } from './saved_objects_management_action_registry';
+export {
+ SavedObjectsManagementAction,
+ SavedObjectsManagementRecord,
+ SavedObjectsManagementRecordReference,
+} from './saved_objects_management_action';
+export {
+ processImportResponse,
+ ProcessedImportResponse,
+} from '../../../../core_plugins/kibana/public/management/sections/objects/lib/process_import_response';
diff --git a/src/legacy/ui/public/management/saved_objects_management/saved_objects_management_action.ts b/src/legacy/ui/public/management/saved_objects_management/saved_objects_management_action.ts
new file mode 100644
index 0000000000000..a09f842e36713
--- /dev/null
+++ b/src/legacy/ui/public/management/saved_objects_management/saved_objects_management_action.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 { ReactNode } from '@elastic/eui/node_modules/@types/react';
+
+export interface SavedObjectsManagementRecordReference {
+ type: string;
+ id: string;
+ name: string;
+}
+export interface SavedObjectsManagementRecord {
+ type: string;
+ id: string;
+ meta: {
+ icon: string;
+ title: string;
+ };
+ references: SavedObjectsManagementRecordReference[];
+}
+
+export abstract class SavedObjectsManagementAction {
+ public abstract render: () => ReactNode;
+ public abstract id: string;
+ public abstract euiAction: {
+ name: string;
+ description: string;
+ icon: string;
+ type: string;
+ available?: (item: SavedObjectsManagementRecord) => boolean;
+ enabled?: (item: SavedObjectsManagementRecord) => boolean;
+ onClick?: (item: SavedObjectsManagementRecord) => void;
+ render?: (item: SavedObjectsManagementRecord) => any;
+ };
+
+ private callbacks: Function[] = [];
+
+ protected record: SavedObjectsManagementRecord | null = null;
+
+ public registerOnFinishCallback(callback: Function) {
+ this.callbacks.push(callback);
+ }
+
+ protected start(record: SavedObjectsManagementRecord) {
+ this.record = record;
+ }
+
+ protected finish() {
+ this.record = null;
+ this.callbacks.forEach(callback => callback());
+ }
+}
diff --git a/src/legacy/ui/public/management/saved_objects_management/saved_objects_management_action_registry.test.ts b/src/legacy/ui/public/management/saved_objects_management/saved_objects_management_action_registry.test.ts
new file mode 100644
index 0000000000000..902b7f01c19f4
--- /dev/null
+++ b/src/legacy/ui/public/management/saved_objects_management/saved_objects_management_action_registry.test.ts
@@ -0,0 +1,55 @@
+/*
+ * 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 { SavedObjectsManagementActionRegistry } from './saved_objects_management_action_registry';
+import { SavedObjectsManagementAction } from './saved_objects_management_action';
+
+describe('SavedObjectsManagementActionRegistry', () => {
+ it('allows actions to be registered and retrieved', () => {
+ const action = { id: 'foo' } as SavedObjectsManagementAction;
+ SavedObjectsManagementActionRegistry.register(action);
+ expect(SavedObjectsManagementActionRegistry.get()).toContain(action);
+ });
+
+ it('requires an "id" property', () => {
+ expect(() =>
+ SavedObjectsManagementActionRegistry.register({} as SavedObjectsManagementAction)
+ ).toThrowErrorMatchingInlineSnapshot(`"Saved Objects Management Actions must have an id"`);
+ });
+
+ it('does not allow actions with duplicate ids to be registered', () => {
+ const action = { id: 'my-action' } as SavedObjectsManagementAction;
+ SavedObjectsManagementActionRegistry.register(action);
+ expect(() =>
+ SavedObjectsManagementActionRegistry.register(action)
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"Saved Objects Management Action with id 'my-action' already exists"`
+ );
+ });
+
+ it('#has returns true when an action with a matching ID exists', () => {
+ const action = { id: 'existing-action' } as SavedObjectsManagementAction;
+ SavedObjectsManagementActionRegistry.register(action);
+ expect(SavedObjectsManagementActionRegistry.has('existing-action')).toEqual(true);
+ });
+
+ it(`#has returns false when an action with doesn't exist`, () => {
+ expect(SavedObjectsManagementActionRegistry.has('missing-action')).toEqual(false);
+ });
+});
diff --git a/src/legacy/ui/public/management/saved_objects_management/saved_objects_management_action_registry.ts b/src/legacy/ui/public/management/saved_objects_management/saved_objects_management_action_registry.ts
new file mode 100644
index 0000000000000..f4085a674f496
--- /dev/null
+++ b/src/legacy/ui/public/management/saved_objects_management/saved_objects_management_action_registry.ts
@@ -0,0 +1,37 @@
+/*
+ * 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 { SavedObjectsManagementAction } from './saved_objects_management_action';
+
+const actions: Map = new Map();
+
+export const SavedObjectsManagementActionRegistry = {
+ register: (action: SavedObjectsManagementAction) => {
+ if (!action.id) {
+ throw new TypeError('Saved Objects Management Actions must have an id');
+ }
+ if (actions.has(action.id)) {
+ throw new Error(`Saved Objects Management Action with id '${action.id}' already exists`);
+ }
+ actions.set(action.id, action);
+ },
+
+ has: (actionId: string) => actions.has(actionId),
+
+ get: () => Array.from(actions.values()),
+};
diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js
index 21ef201a4d3a8..be60a83c1983c 100644
--- a/x-pack/dev-tools/jest/create_jest_config.js
+++ b/x-pack/dev-tools/jest/create_jest_config.js
@@ -26,6 +26,7 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': fileMockPath,
'\\.(css|less|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/style_mock.js`,
'^test_utils/enzyme_helpers': `${xPackKibanaDirectory}/test_utils/enzyme_helpers.tsx`,
+ '^test_utils/find_test_subject': `${xPackKibanaDirectory}/test_utils/find_test_subject.ts`,
},
setupFiles: [
`${kibanaDirectory}/src/dev/jest/setup/babel_polyfill.js`,
diff --git a/x-pack/legacy/plugins/spaces/common/model/types.ts b/x-pack/legacy/plugins/spaces/common/model/types.ts
new file mode 100644
index 0000000000000..58c36da33dbd7
--- /dev/null
+++ b/x-pack/legacy/plugins/spaces/common/model/types.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export type GetSpacePurpose = 'any' | 'copySavedObjectsIntoSpace';
diff --git a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx b/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx
new file mode 100644
index 0000000000000..4ed1937ebf782
--- /dev/null
+++ b/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx
@@ -0,0 +1,56 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import {
+ SavedObjectsManagementAction,
+ SavedObjectsManagementRecord,
+} from 'ui/management/saved_objects_management';
+import { i18n } from '@kbn/i18n';
+import { toastNotifications } from 'ui/notify';
+import { CopySavedObjectsToSpaceFlyout } from '../../views/management/components/copy_saved_objects_to_space';
+import { Space } from '../../../common/model/space';
+import { SpacesManager } from '../spaces_manager';
+
+export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagementAction {
+ public id: string = 'copy_saved_objects_to_space';
+
+ public euiAction = {
+ name: i18n.translate('xpack.spaces.management.copyToSpace.actionTitle', {
+ defaultMessage: 'Copy to space',
+ }),
+ description: i18n.translate('xpack.spaces.management.copyToSpace.actionDescription', {
+ defaultMessage: 'Copy this saved object to one or more spaces',
+ }),
+ icon: 'spacesApp',
+ type: 'icon',
+ onClick: (object: SavedObjectsManagementRecord) => {
+ this.start(object);
+ },
+ };
+
+ constructor(private readonly spacesManager: SpacesManager, private readonly activeSpace: Space) {
+ super();
+ }
+
+ public render = () => {
+ if (!this.record) {
+ throw new Error('No record available! `render()` was likely called before `start()`.');
+ }
+ return (
+
+ );
+ };
+
+ private onClose = () => {
+ this.finish();
+ };
+}
diff --git a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/index.ts b/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/index.ts
new file mode 100644
index 0000000000000..be23d90cc242a
--- /dev/null
+++ b/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export * from './summarize_copy_result';
+export { CopyToSpaceSavedObjectsManagementAction } from './copy_saved_objects_to_space_action';
diff --git a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.test.ts b/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.test.ts
new file mode 100644
index 0000000000000..7517fa48ad8b8
--- /dev/null
+++ b/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.test.ts
@@ -0,0 +1,284 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { summarizeCopyResult } from './summarize_copy_result';
+import { ProcessedImportResponse } from 'ui/management/saved_objects_management';
+
+const createSavedObjectsManagementRecord = () => ({
+ type: 'dashboard',
+ id: 'foo',
+ meta: { icon: 'foo-icon', title: 'my-dashboard' },
+ references: [
+ {
+ type: 'visualization',
+ id: 'foo-viz',
+ name: 'Foo Viz',
+ },
+ {
+ type: 'visualization',
+ id: 'bar-viz',
+ name: 'Bar Viz',
+ },
+ ],
+});
+
+const createCopyResult = (
+ opts: { withConflicts?: boolean; withUnresolvableError?: boolean } = {}
+) => {
+ const failedImports: ProcessedImportResponse['failedImports'] = [];
+ if (opts.withConflicts) {
+ failedImports.push(
+ {
+ obj: { type: 'visualization', id: 'foo-viz' },
+ error: { type: 'conflict' },
+ },
+ {
+ obj: { type: 'index-pattern', id: 'transient-index-pattern-conflict' },
+ error: { type: 'conflict' },
+ }
+ );
+ }
+ if (opts.withUnresolvableError) {
+ failedImports.push({
+ obj: { type: 'visualization', id: 'bar-viz' },
+ error: { type: 'missing_references', blocking: [], references: [] },
+ });
+ }
+
+ const copyResult: ProcessedImportResponse = {
+ failedImports,
+ } as ProcessedImportResponse;
+
+ return copyResult;
+};
+
+describe('summarizeCopyResult', () => {
+ it('indicates the result is processing when not provided', () => {
+ const SavedObjectsManagementRecord = createSavedObjectsManagementRecord();
+ const copyResult = undefined;
+ const includeRelated = true;
+
+ const summarizedResult = summarizeCopyResult(
+ SavedObjectsManagementRecord,
+ copyResult,
+ includeRelated
+ );
+
+ expect(summarizedResult).toMatchInlineSnapshot(`
+ Object {
+ "objects": Array [
+ Object {
+ "conflicts": Array [],
+ "hasUnresolvableErrors": false,
+ "id": "foo",
+ "name": "my-dashboard",
+ "type": "dashboard",
+ },
+ Object {
+ "conflicts": Array [],
+ "hasUnresolvableErrors": false,
+ "id": "foo-viz",
+ "name": "Foo Viz",
+ "type": "visualization",
+ },
+ Object {
+ "conflicts": Array [],
+ "hasUnresolvableErrors": false,
+ "id": "bar-viz",
+ "name": "Bar Viz",
+ "type": "visualization",
+ },
+ ],
+ "processing": true,
+ }
+ `);
+ });
+
+ it('processes failedImports to extract conflicts, including transient conflicts', () => {
+ const SavedObjectsManagementRecord = createSavedObjectsManagementRecord();
+ const copyResult = createCopyResult({ withConflicts: true });
+ const includeRelated = true;
+
+ const summarizedResult = summarizeCopyResult(
+ SavedObjectsManagementRecord,
+ copyResult,
+ includeRelated
+ );
+ expect(summarizedResult).toMatchInlineSnapshot(`
+ Object {
+ "hasConflicts": true,
+ "hasUnresolvableErrors": false,
+ "objects": Array [
+ Object {
+ "conflicts": Array [],
+ "hasUnresolvableErrors": false,
+ "id": "foo",
+ "name": "my-dashboard",
+ "type": "dashboard",
+ },
+ Object {
+ "conflicts": Array [
+ Object {
+ "error": Object {
+ "type": "conflict",
+ },
+ "obj": Object {
+ "id": "foo-viz",
+ "type": "visualization",
+ },
+ },
+ ],
+ "hasUnresolvableErrors": false,
+ "id": "foo-viz",
+ "name": "Foo Viz",
+ "type": "visualization",
+ },
+ Object {
+ "conflicts": Array [],
+ "hasUnresolvableErrors": false,
+ "id": "bar-viz",
+ "name": "Bar Viz",
+ "type": "visualization",
+ },
+ Object {
+ "conflicts": Array [
+ Object {
+ "error": Object {
+ "type": "conflict",
+ },
+ "obj": Object {
+ "id": "transient-index-pattern-conflict",
+ "type": "index-pattern",
+ },
+ },
+ ],
+ "hasUnresolvableErrors": false,
+ "id": "transient-index-pattern-conflict",
+ "name": "transient-index-pattern-conflict",
+ "type": "index-pattern",
+ },
+ ],
+ "processing": false,
+ "successful": false,
+ }
+ `);
+ });
+
+ it('processes failedImports to extract unresolvable errors', () => {
+ const SavedObjectsManagementRecord = createSavedObjectsManagementRecord();
+ const copyResult = createCopyResult({ withUnresolvableError: true });
+ const includeRelated = true;
+
+ const summarizedResult = summarizeCopyResult(
+ SavedObjectsManagementRecord,
+ copyResult,
+ includeRelated
+ );
+ expect(summarizedResult).toMatchInlineSnapshot(`
+ Object {
+ "hasConflicts": false,
+ "hasUnresolvableErrors": true,
+ "objects": Array [
+ Object {
+ "conflicts": Array [],
+ "hasUnresolvableErrors": false,
+ "id": "foo",
+ "name": "my-dashboard",
+ "type": "dashboard",
+ },
+ Object {
+ "conflicts": Array [],
+ "hasUnresolvableErrors": false,
+ "id": "foo-viz",
+ "name": "Foo Viz",
+ "type": "visualization",
+ },
+ Object {
+ "conflicts": Array [],
+ "hasUnresolvableErrors": true,
+ "id": "bar-viz",
+ "name": "Bar Viz",
+ "type": "visualization",
+ },
+ ],
+ "processing": false,
+ "successful": false,
+ }
+ `);
+ });
+
+ it('processes a result without errors', () => {
+ const SavedObjectsManagementRecord = createSavedObjectsManagementRecord();
+ const copyResult = createCopyResult();
+ const includeRelated = true;
+
+ const summarizedResult = summarizeCopyResult(
+ SavedObjectsManagementRecord,
+ copyResult,
+ includeRelated
+ );
+ expect(summarizedResult).toMatchInlineSnapshot(`
+ Object {
+ "hasConflicts": false,
+ "hasUnresolvableErrors": false,
+ "objects": Array [
+ Object {
+ "conflicts": Array [],
+ "hasUnresolvableErrors": false,
+ "id": "foo",
+ "name": "my-dashboard",
+ "type": "dashboard",
+ },
+ Object {
+ "conflicts": Array [],
+ "hasUnresolvableErrors": false,
+ "id": "foo-viz",
+ "name": "Foo Viz",
+ "type": "visualization",
+ },
+ Object {
+ "conflicts": Array [],
+ "hasUnresolvableErrors": false,
+ "id": "bar-viz",
+ "name": "Bar Viz",
+ "type": "visualization",
+ },
+ ],
+ "processing": false,
+ "successful": true,
+ }
+ `);
+ });
+
+ it('does not include references unless requested', () => {
+ const SavedObjectsManagementRecord = createSavedObjectsManagementRecord();
+ const copyResult = createCopyResult();
+ const includeRelated = false;
+
+ const summarizedResult = summarizeCopyResult(
+ SavedObjectsManagementRecord,
+ copyResult,
+ includeRelated
+ );
+ expect(summarizedResult).toMatchInlineSnapshot(`
+ Object {
+ "hasConflicts": false,
+ "hasUnresolvableErrors": false,
+ "objects": Array [
+ Object {
+ "conflicts": Array [],
+ "hasUnresolvableErrors": false,
+ "id": "foo",
+ "name": "my-dashboard",
+ "type": "dashboard",
+ },
+ ],
+ "processing": false,
+ "successful": true,
+ }
+ `);
+ });
+});
diff --git a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.ts b/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.ts
new file mode 100644
index 0000000000000..7eddf3f4891e5
--- /dev/null
+++ b/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/summarize_copy_result.ts
@@ -0,0 +1,133 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ ProcessedImportResponse,
+ SavedObjectsManagementRecord,
+} from 'ui/management/saved_objects_management';
+
+export interface SummarizedSavedObjectResult {
+ type: string;
+ id: string;
+ name: string;
+ conflicts: ProcessedImportResponse['failedImports'];
+ hasUnresolvableErrors: boolean;
+}
+
+interface SuccessfulResponse {
+ successful: true;
+ hasConflicts: false;
+ hasUnresolvableErrors: false;
+ objects: SummarizedSavedObjectResult[];
+ processing: false;
+}
+interface UnsuccessfulResponse {
+ successful: false;
+ hasConflicts: boolean;
+ hasUnresolvableErrors: boolean;
+ objects: SummarizedSavedObjectResult[];
+ processing: false;
+}
+
+interface ProcessingResponse {
+ objects: SummarizedSavedObjectResult[];
+ processing: true;
+}
+
+export type SummarizedCopyToSpaceResult =
+ | SuccessfulResponse
+ | UnsuccessfulResponse
+ | ProcessingResponse;
+
+export function summarizeCopyResult(
+ savedObject: SavedObjectsManagementRecord,
+ copyResult: ProcessedImportResponse | undefined,
+ includeRelated: boolean
+): SummarizedCopyToSpaceResult {
+ const successful = Boolean(copyResult && copyResult.failedImports.length === 0);
+
+ const conflicts = copyResult
+ ? copyResult.failedImports.filter(failed => failed.error.type === 'conflict')
+ : [];
+
+ const unresolvableErrors = copyResult
+ ? copyResult.failedImports.filter(failed => failed.error.type !== 'conflict')
+ : [];
+
+ const hasConflicts = conflicts.length > 0;
+
+ const hasUnresolvableErrors = Boolean(
+ copyResult && copyResult.failedImports.some(failed => failed.error.type !== 'conflict')
+ );
+
+ const objectMap = new Map();
+ objectMap.set(`${savedObject.type}:${savedObject.id}`, {
+ type: savedObject.type,
+ id: savedObject.id,
+ name: savedObject.meta.title,
+ conflicts: conflicts.filter(
+ c => c.obj.type === savedObject.type && c.obj.id === savedObject.id
+ ),
+ hasUnresolvableErrors: unresolvableErrors.some(
+ e => e.obj.type === savedObject.type && e.obj.id === savedObject.id
+ ),
+ });
+
+ if (includeRelated) {
+ savedObject.references.forEach(ref => {
+ objectMap.set(`${ref.type}:${ref.id}`, {
+ type: ref.type,
+ id: ref.id,
+ name: ref.name,
+ conflicts: conflicts.filter(c => c.obj.type === ref.type && c.obj.id === ref.id),
+ hasUnresolvableErrors: unresolvableErrors.some(
+ e => e.obj.type === ref.type && e.obj.id === ref.id
+ ),
+ });
+ });
+
+ // The `savedObject.references` array only includes the direct references. It does not include any references of references.
+ // Therefore, if there are conflicts detected in these transitive references, we need to include them here so that they are visible
+ // in the UI as resolvable conflicts.
+ const transitiveConflicts = conflicts.filter(c => !objectMap.has(`${c.obj.type}:${c.obj.id}`));
+ transitiveConflicts.forEach(conflict => {
+ objectMap.set(`${conflict.obj.type}:${conflict.obj.id}`, {
+ type: conflict.obj.type,
+ id: conflict.obj.id,
+ name: conflict.obj.title || conflict.obj.id,
+ conflicts: conflicts.filter(c => c.obj.type === conflict.obj.type && conflict.obj.id),
+ hasUnresolvableErrors: unresolvableErrors.some(
+ e => e.obj.type === conflict.obj.type && e.obj.id === conflict.obj.id
+ ),
+ });
+ });
+ }
+
+ if (typeof copyResult === 'undefined') {
+ return {
+ processing: true,
+ objects: Array.from(objectMap.values()),
+ };
+ }
+
+ if (successful) {
+ return {
+ successful,
+ hasConflicts: false,
+ objects: Array.from(objectMap.values()),
+ hasUnresolvableErrors: false,
+ processing: false,
+ };
+ }
+
+ return {
+ successful,
+ hasConflicts,
+ objects: Array.from(objectMap.values()),
+ hasUnresolvableErrors,
+ processing: false,
+ };
+}
diff --git a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/types.ts b/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/types.ts
new file mode 100644
index 0000000000000..d2576ca5c6c16
--- /dev/null
+++ b/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/types.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedObjectsImportRetry, SavedObjectsImportResponse } from 'src/core/server';
+
+export interface CopyOptions {
+ includeRelated: boolean;
+ overwrite: boolean;
+ selectedSpaceIds: string[];
+}
+
+export type ImportRetry = Omit;
+
+export interface CopySavedObjectsToSpaceResponse {
+ [spaceId: string]: SavedObjectsImportResponse;
+}
diff --git a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.mock.ts b/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.mock.ts
index 7d4fb1b90fe11..4d7a9251228e8 100644
--- a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.mock.ts
+++ b/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.mock.ts
@@ -4,19 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SpacesManager } from './spaces_manager';
-
function createSpacesManagerMock() {
- return ({
+ return {
getSpaces: jest.fn().mockResolvedValue([]),
getSpace: jest.fn().mockResolvedValue(undefined),
createSpace: jest.fn().mockResolvedValue(undefined),
updateSpace: jest.fn().mockResolvedValue(undefined),
deleteSpace: jest.fn().mockResolvedValue(undefined),
+ copySavedObjects: jest.fn().mockResolvedValue(undefined),
+ resolveCopySavedObjectsErrors: jest.fn().mockResolvedValue(undefined),
redirectToSpaceSelector: jest.fn().mockResolvedValue(undefined),
requestRefresh: jest.fn(),
on: jest.fn(),
- } as unknown) as SpacesManager;
+ };
}
export const spacesManagerMock = {
diff --git a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts b/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts
index cd2939f83e20b..d39b751e30a8a 100644
--- a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts
+++ b/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts
@@ -7,7 +7,10 @@ import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
import { EventEmitter } from 'events';
import { kfetch } from 'ui/kfetch';
+import { SavedObjectsManagementRecord } from 'ui/management/saved_objects_management';
import { Space } from '../../common/model/space';
+import { GetSpacePurpose } from '../../common/model/types';
+import { CopySavedObjectsToSpaceResponse } from './copy_saved_objects_to_space/types';
export class SpacesManager extends EventEmitter {
private spaceSelectorURL: string;
@@ -17,8 +20,8 @@ export class SpacesManager extends EventEmitter {
this.spaceSelectorURL = spaceSelectorURL;
}
- public async getSpaces(): Promise {
- return await kfetch({ pathname: '/api/spaces/space' });
+ public async getSpaces(purpose?: GetSpacePurpose): Promise {
+ return await kfetch({ pathname: '/api/spaces/space', query: { purpose } });
}
public async getSpace(id: string): Promise {
@@ -51,6 +54,40 @@ export class SpacesManager extends EventEmitter {
});
}
+ public async copySavedObjects(
+ objects: Array>,
+ spaces: string[],
+ includeReferences: boolean,
+ overwrite: boolean
+ ): Promise {
+ return await kfetch({
+ pathname: `/api/spaces/_copy_saved_objects`,
+ method: 'POST',
+ body: JSON.stringify({
+ objects,
+ spaces,
+ includeReferences,
+ overwrite,
+ }),
+ });
+ }
+
+ public async resolveCopySavedObjectsErrors(
+ objects: Array>,
+ retries: unknown,
+ includeReferences: boolean
+ ): Promise {
+ return await kfetch({
+ pathname: `/api/spaces/_resolve_copy_saved_objects_errors`,
+ method: 'POST',
+ body: JSON.stringify({
+ objects,
+ includeReferences,
+ retries,
+ }),
+ });
+ }
+
public async changeSelectedSpace(space: Space) {
await kfetch({
pathname: `/api/spaces/v1/space/${encodeURIComponent(space.id)}/select`,
diff --git a/x-pack/legacy/plugins/spaces/public/views/management/_index.scss b/x-pack/legacy/plugins/spaces/public/views/management/_index.scss
index 8cf4e6d579919..e7cbdfe2de7e8 100644
--- a/x-pack/legacy/plugins/spaces/public/views/management/_index.scss
+++ b/x-pack/legacy/plugins/spaces/public/views/management/_index.scss
@@ -1,2 +1,3 @@
@import './components/confirm_delete_modal';
@import './edit_space/enabled_features/index';
+@import './components/copy_saved_objects_to_space/index';
diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.test.tsx b/x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.test.tsx
index b34b52cd48c7a..3c3fa502a917d 100644
--- a/x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.test.tsx
+++ b/x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.test.tsx
@@ -9,6 +9,7 @@ import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { SpacesNavState } from '../../nav_control';
import { ConfirmDeleteModal } from './confirm_delete_modal';
import { spacesManagerMock } from '../../../lib/mocks';
+import { SpacesManager } from '../../../lib';
describe('ConfirmDeleteModal', () => {
it('renders as expected', () => {
@@ -32,7 +33,7 @@ describe('ConfirmDeleteModal', () => {
shallowWithIntl(
{
const wrapper = mountWithIntl(
{
+ const { summarizedCopyResult, conflictResolutionInProgress } = props;
+ if (summarizedCopyResult.processing || conflictResolutionInProgress) {
+ return ;
+ }
+
+ const objectResult = summarizedCopyResult.objects.find(
+ o => o.type === props.object!.type && o.id === props.object!.id
+ ) as SummarizedSavedObjectResult;
+
+ const successful =
+ !objectResult.hasUnresolvableErrors &&
+ (objectResult.conflicts.length === 0 || props.overwritePending === true);
+ const successColor = props.overwritePending ? 'warning' : 'success';
+ const hasConflicts = objectResult.conflicts.length > 0;
+ const hasUnresolvableErrors = objectResult.hasUnresolvableErrors;
+
+ if (successful) {
+ const message = props.overwritePending ? (
+
+ ) : (
+
+ );
+ return ;
+ }
+ if (hasUnresolvableErrors) {
+ return (
+
+ }
+ />
+ );
+ }
+ if (hasConflicts) {
+ return (
+
+
+
+
+
+
+
+
+ }
+ />
+ );
+ }
+ return null;
+};
diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_status_summary_indicator.tsx b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_status_summary_indicator.tsx
new file mode 100644
index 0000000000000..0ad5f72ba3e45
--- /dev/null
+++ b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_status_summary_indicator.tsx
@@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiLoadingSpinner, EuiIconTip } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { Space } from '../../../../../common/model/space';
+import { SummarizedCopyToSpaceResult } from '../../../../lib/copy_saved_objects_to_space';
+
+interface Props {
+ space: Space;
+ summarizedCopyResult: SummarizedCopyToSpaceResult;
+ conflictResolutionInProgress: boolean;
+}
+
+export const CopyStatusSummaryIndicator = (props: Props) => {
+ const { summarizedCopyResult } = props;
+ const getDataTestSubj = (status: string) => `cts-summary-indicator-${status}-${props.space.id}`;
+
+ if (summarizedCopyResult.processing || props.conflictResolutionInProgress) {
+ return ;
+ }
+
+ if (summarizedCopyResult.successful) {
+ return (
+
+ }
+ />
+ );
+ }
+ if (summarizedCopyResult.hasUnresolvableErrors) {
+ return (
+
+ }
+ />
+ );
+ }
+ if (summarizedCopyResult.hasConflicts) {
+ return (
+
+ }
+ />
+ );
+ }
+ return null;
+};
diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.test.tsx b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.test.tsx
new file mode 100644
index 0000000000000..9e8f1e7c1a6f4
--- /dev/null
+++ b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.test.tsx
@@ -0,0 +1,455 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import Boom from 'boom';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { CopySavedObjectsToSpaceFlyout } from './copy_to_space_flyout';
+import { CopyToSpaceForm } from './copy_to_space_form';
+import { EuiLoadingSpinner, EuiEmptyPrompt } from '@elastic/eui';
+import { Space } from '../../../../../common/model/space';
+import { findTestSubject } from 'test_utils/find_test_subject';
+import { SelectableSpacesControl } from './selectable_spaces_control';
+import { act } from 'react-testing-library';
+import { ProcessingCopyToSpace } from './processing_copy_to_space';
+import { spacesManagerMock } from '../../../../lib/mocks';
+import { SpacesManager } from '../../../../lib';
+import { ToastNotifications } from 'ui/notify/toasts/toast_notifications';
+
+interface SetupOpts {
+ mockSpaces?: Space[];
+ returnBeforeSpacesLoad?: boolean;
+}
+
+const setup = async (opts: SetupOpts = {}) => {
+ const onClose = jest.fn();
+
+ const mockSpacesManager = spacesManagerMock.create();
+ mockSpacesManager.getSpaces.mockResolvedValue(
+ opts.mockSpaces || [
+ {
+ id: 'space-1',
+ name: 'Space 1',
+ disabledFeatures: [],
+ },
+ {
+ id: 'space-2',
+ name: 'Space 2',
+ disabledFeatures: [],
+ },
+ {
+ id: 'space-3',
+ name: 'Space 3',
+ disabledFeatures: [],
+ },
+ {
+ id: 'my-active-space',
+ name: 'my active space',
+ disabledFeatures: [],
+ },
+ ]
+ );
+
+ const mockToastNotifications = {
+ addError: jest.fn(),
+ addSuccess: jest.fn(),
+ };
+ const savedObjectToCopy = {
+ type: 'dashboard',
+ id: 'my-dash',
+ references: [
+ {
+ type: 'visualization',
+ id: 'my-viz',
+ name: 'My Viz',
+ },
+ ],
+ meta: { icon: 'dashboard', title: 'foo' },
+ };
+
+ const wrapper = mountWithIntl(
+
+ );
+
+ if (!opts.returnBeforeSpacesLoad) {
+ // Wait for spaces manager to complete and flyout to rerender
+ await Promise.resolve();
+ wrapper.update();
+ }
+
+ return { wrapper, onClose, mockSpacesManager, mockToastNotifications, savedObjectToCopy };
+};
+
+describe('CopyToSpaceFlyout', () => {
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ it('waits for spaces to load', async () => {
+ const { wrapper } = await setup({ returnBeforeSpacesLoad: true });
+
+ expect(wrapper.find(CopyToSpaceForm)).toHaveLength(0);
+ expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(0);
+ expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1);
+
+ await Promise.resolve();
+ wrapper.update();
+
+ expect(wrapper.find(CopyToSpaceForm)).toHaveLength(1);
+ expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0);
+ expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(0);
+ });
+
+ it('shows a message within an EuiEmptyPrompt when no spaces are available', async () => {
+ const { wrapper, onClose } = await setup({ mockSpaces: [] });
+
+ expect(wrapper.find(CopyToSpaceForm)).toHaveLength(0);
+ expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0);
+ expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
+ expect(onClose).toHaveBeenCalledTimes(0);
+ });
+
+ it('shows a message within an EuiEmptyPrompt when only the active space is available', async () => {
+ const { wrapper, onClose } = await setup({
+ mockSpaces: [{ id: 'my-active-space', name: '', disabledFeatures: [] }],
+ });
+
+ expect(wrapper.find(CopyToSpaceForm)).toHaveLength(0);
+ expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0);
+ expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
+ expect(onClose).toHaveBeenCalledTimes(0);
+ });
+
+ it('handles errors thrown from copySavedObjects API call', async () => {
+ const { wrapper, mockSpacesManager, mockToastNotifications } = await setup();
+
+ mockSpacesManager.copySavedObjects.mockImplementation(() => {
+ return Promise.reject(Boom.serverUnavailable('Something bad happened'));
+ });
+
+ expect(wrapper.find(CopyToSpaceForm)).toHaveLength(1);
+ expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0);
+ expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(0);
+
+ // Using props callback instead of simulating clicks,
+ // because EuiSelectable uses a virtualized list, which isn't easily testable via test subjects
+ const spaceSelector = wrapper.find(SelectableSpacesControl);
+ act(() => {
+ spaceSelector.props().onChange(['space-1']);
+ });
+
+ const startButton = findTestSubject(wrapper, 'cts-initiate-button');
+ act(() => {
+ startButton.simulate('click');
+ });
+
+ await Promise.resolve();
+
+ wrapper.update();
+
+ expect(mockSpacesManager.copySavedObjects).toHaveBeenCalled();
+ expect(mockToastNotifications.addError).toHaveBeenCalled();
+ });
+
+ it('handles errors thrown from resolveCopySavedObjectsErrors API call', async () => {
+ const { wrapper, mockSpacesManager, mockToastNotifications } = await setup();
+
+ mockSpacesManager.copySavedObjects.mockResolvedValue({
+ 'space-1': {
+ success: true,
+ successCount: 3,
+ },
+ 'space-2': {
+ success: false,
+ successCount: 1,
+ errors: [
+ {
+ type: 'index-pattern',
+ id: 'conflicting-ip',
+ error: { type: 'conflict' },
+ },
+ {
+ type: 'visualization',
+ id: 'my-viz',
+ error: { type: 'conflict' },
+ },
+ ],
+ },
+ });
+
+ mockSpacesManager.resolveCopySavedObjectsErrors.mockImplementation(() => {
+ return Promise.reject(Boom.serverUnavailable('Something bad happened'));
+ });
+
+ expect(wrapper.find(CopyToSpaceForm)).toHaveLength(1);
+ expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0);
+ expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(0);
+
+ // Using props callback instead of simulating clicks,
+ // because EuiSelectable uses a virtualized list, which isn't easily testable via test subjects
+ const spaceSelector = wrapper.find(SelectableSpacesControl);
+ act(() => {
+ spaceSelector.props().onChange(['space-2']);
+ });
+
+ const startButton = findTestSubject(wrapper, 'cts-initiate-button');
+ act(() => {
+ startButton.simulate('click');
+ });
+
+ await Promise.resolve();
+ wrapper.update();
+
+ expect(mockSpacesManager.copySavedObjects).toHaveBeenCalled();
+ expect(mockToastNotifications.addError).not.toHaveBeenCalled();
+
+ const spaceResult = findTestSubject(wrapper, `cts-space-result-space-2`);
+ spaceResult.simulate('click');
+
+ const overwriteButton = findTestSubject(wrapper, `cts-overwrite-conflict-conflicting-ip`);
+ overwriteButton.simulate('click');
+
+ const finishButton = findTestSubject(wrapper, 'cts-finish-button');
+ act(() => {
+ finishButton.simulate('click');
+ });
+
+ await Promise.resolve();
+ wrapper.update();
+
+ expect(mockSpacesManager.resolveCopySavedObjectsErrors).toHaveBeenCalled();
+ expect(mockToastNotifications.addError).toHaveBeenCalled();
+ });
+
+ it('allows the form to be filled out', async () => {
+ const {
+ wrapper,
+ onClose,
+ mockSpacesManager,
+ mockToastNotifications,
+ savedObjectToCopy,
+ } = await setup();
+
+ mockSpacesManager.copySavedObjects.mockResolvedValue({
+ 'space-1': {
+ success: true,
+ successCount: 3,
+ },
+ 'space-2': {
+ success: true,
+ successCount: 3,
+ },
+ });
+
+ expect(wrapper.find(CopyToSpaceForm)).toHaveLength(1);
+ expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0);
+ expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(0);
+
+ // Using props callback instead of simulating clicks,
+ // because EuiSelectable uses a virtualized list, which isn't easily testable via test subjects
+ const spaceSelector = wrapper.find(SelectableSpacesControl);
+
+ act(() => {
+ spaceSelector.props().onChange(['space-1', 'space-2']);
+ });
+
+ const startButton = findTestSubject(wrapper, 'cts-initiate-button');
+ act(() => {
+ startButton.simulate('click');
+ });
+
+ await Promise.resolve();
+
+ wrapper.update();
+
+ expect(mockSpacesManager.copySavedObjects).toHaveBeenCalledWith(
+ [{ type: savedObjectToCopy.type, id: savedObjectToCopy.id }],
+ ['space-1', 'space-2'],
+ true,
+ true
+ );
+
+ expect(wrapper.find(CopyToSpaceForm)).toHaveLength(0);
+ expect(wrapper.find(ProcessingCopyToSpace)).toHaveLength(1);
+
+ const finishButton = findTestSubject(wrapper, 'cts-finish-button');
+ act(() => {
+ finishButton.simulate('click');
+ });
+
+ expect(onClose).toHaveBeenCalledTimes(1);
+ expect(mockToastNotifications.addError).not.toHaveBeenCalled();
+ });
+
+ it('allows conflicts to be resolved', async () => {
+ const {
+ wrapper,
+ onClose,
+ mockSpacesManager,
+ mockToastNotifications,
+ savedObjectToCopy,
+ } = await setup();
+
+ mockSpacesManager.copySavedObjects.mockResolvedValue({
+ 'space-1': {
+ success: true,
+ successCount: 3,
+ },
+ 'space-2': {
+ success: false,
+ successCount: 1,
+ errors: [
+ {
+ type: 'index-pattern',
+ id: 'conflicting-ip',
+ error: { type: 'conflict' },
+ },
+ {
+ type: 'visualization',
+ id: 'my-viz',
+ error: { type: 'conflict' },
+ },
+ ],
+ },
+ });
+
+ mockSpacesManager.resolveCopySavedObjectsErrors.mockResolvedValue({
+ 'space-2': {
+ success: true,
+ successCount: 2,
+ },
+ });
+
+ // Using props callback instead of simulating clicks,
+ // because EuiSelectable uses a virtualized list, which isn't easily testable via test subjects
+ const spaceSelector = wrapper.find(SelectableSpacesControl);
+
+ act(() => {
+ spaceSelector.props().onChange(['space-1', 'space-2']);
+ });
+
+ const startButton = findTestSubject(wrapper, 'cts-initiate-button');
+ act(() => {
+ startButton.simulate('click');
+ });
+
+ await Promise.resolve();
+
+ wrapper.update();
+
+ expect(wrapper.find(CopyToSpaceForm)).toHaveLength(0);
+ expect(wrapper.find(ProcessingCopyToSpace)).toHaveLength(1);
+
+ const spaceResult = findTestSubject(wrapper, `cts-space-result-space-2`);
+ spaceResult.simulate('click');
+
+ const overwriteButton = findTestSubject(wrapper, `cts-overwrite-conflict-conflicting-ip`);
+ overwriteButton.simulate('click');
+
+ const finishButton = findTestSubject(wrapper, 'cts-finish-button');
+ act(() => {
+ finishButton.simulate('click');
+ });
+
+ await Promise.resolve();
+ wrapper.update();
+
+ expect(mockSpacesManager.resolveCopySavedObjectsErrors).toHaveBeenCalledWith(
+ [{ type: savedObjectToCopy.type, id: savedObjectToCopy.id }],
+ {
+ 'space-2': [{ type: 'index-pattern', id: 'conflicting-ip', overwrite: true }],
+ },
+ true
+ );
+
+ expect(onClose).toHaveBeenCalledTimes(1);
+ expect(mockToastNotifications.addError).not.toHaveBeenCalled();
+ });
+
+ it('displays an error when missing references are encountered', async () => {
+ const { wrapper, onClose, mockSpacesManager, mockToastNotifications } = await setup();
+
+ mockSpacesManager.copySavedObjects.mockResolvedValue({
+ 'space-1': {
+ success: true,
+ successCount: 3,
+ },
+ 'space-2': {
+ success: false,
+ successCount: 1,
+ errors: [
+ {
+ type: 'visualization',
+ id: 'my-viz',
+ error: {
+ type: 'missing_references',
+ references: [{ type: 'index-pattern', id: 'missing-index-pattern' }],
+ },
+ },
+ ],
+ },
+ });
+
+ // Using props callback instead of simulating clicks,
+ // because EuiSelectable uses a virtualized list, which isn't easily testable via test subjects
+ const spaceSelector = wrapper.find(SelectableSpacesControl);
+
+ act(() => {
+ spaceSelector.props().onChange(['space-1', 'space-2']);
+ });
+
+ const startButton = findTestSubject(wrapper, 'cts-initiate-button');
+ act(() => {
+ startButton.simulate('click');
+ });
+
+ await Promise.resolve();
+
+ wrapper.update();
+
+ expect(wrapper.find(CopyToSpaceForm)).toHaveLength(0);
+ expect(wrapper.find(ProcessingCopyToSpace)).toHaveLength(1);
+
+ const spaceResult = findTestSubject(wrapper, `cts-space-result-space-2`);
+ spaceResult.simulate('click');
+
+ const errorIconTip = spaceResult.find(
+ 'EuiIconTip[data-test-subj="cts-object-result-error-my-viz"]'
+ );
+
+ expect(errorIconTip.props()).toMatchInlineSnapshot(`
+ Object {
+ "color": "danger",
+ "content": ,
+ "data-test-subj": "cts-object-result-error-my-viz",
+ "type": "cross",
+ }
+ `);
+
+ const finishButton = findTestSubject(wrapper, 'cts-finish-button');
+ act(() => {
+ finishButton.simulate('click');
+ });
+
+ expect(mockSpacesManager.resolveCopySavedObjectsErrors).not.toHaveBeenCalled();
+
+ expect(onClose).toHaveBeenCalledTimes(1);
+ expect(mockToastNotifications.addError).not.toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.tsx b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.tsx
new file mode 100644
index 0000000000000..4663b73f1cb7e
--- /dev/null
+++ b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.tsx
@@ -0,0 +1,261 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { useState, useEffect } from 'react';
+import {
+ EuiFlyout,
+ EuiIcon,
+ EuiFlyoutHeader,
+ EuiTitle,
+ EuiText,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiLoadingSpinner,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiEmptyPrompt,
+} from '@elastic/eui';
+import { mapValues } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ SavedObjectsManagementRecord,
+ processImportResponse,
+ ProcessedImportResponse,
+} from 'ui/management/saved_objects_management';
+import { ToastNotifications } from 'ui/notify/toasts/toast_notifications';
+import { Space } from '../../../../../common/model/space';
+import { SpacesManager } from '../../../../lib';
+import { ProcessingCopyToSpace } from './processing_copy_to_space';
+import { CopyToSpaceFlyoutFooter } from './copy_to_space_flyout_footer';
+import { CopyToSpaceForm } from './copy_to_space_form';
+import { CopyOptions, ImportRetry } from '../../../../lib/copy_saved_objects_to_space/types';
+
+interface Props {
+ onClose: () => void;
+ savedObject: SavedObjectsManagementRecord;
+ spacesManager: SpacesManager;
+ activeSpace: Space;
+ toastNotifications: ToastNotifications;
+}
+
+export const CopySavedObjectsToSpaceFlyout = (props: Props) => {
+ const { onClose, savedObject, spacesManager, toastNotifications } = props;
+ const [copyOptions, setCopyOptions] = useState({
+ includeRelated: true,
+ overwrite: true,
+ selectedSpaceIds: [],
+ });
+
+ const [{ isLoading, spaces }, setSpacesState] = useState<{ isLoading: boolean; spaces: Space[] }>(
+ {
+ isLoading: true,
+ spaces: [],
+ }
+ );
+ useEffect(() => {
+ spacesManager
+ .getSpaces('copySavedObjectsIntoSpace')
+ .then(response => {
+ setSpacesState({
+ isLoading: false,
+ spaces: response,
+ });
+ })
+ .catch(e => {
+ toastNotifications.addError(e, {
+ title: i18n.translate('xpack.spaces.management.copyToSpace.spacesLoadErrorTitle', {
+ defaultMessage: 'Error loading available spaces',
+ }),
+ });
+ });
+ }, []);
+ const eligibleSpaces = spaces.filter(space => space.id !== props.activeSpace.id);
+
+ const [copyInProgress, setCopyInProgress] = useState(false);
+ const [conflictResolutionInProgress, setConflictResolutionInProgress] = useState(false);
+ const [copyResult, setCopyResult] = useState>({});
+ const [retries, setRetries] = useState>({});
+
+ const initialCopyFinished = Object.values(copyResult).length > 0;
+
+ const onRetriesChange = (updatedRetries: Record) => {
+ setRetries(updatedRetries);
+ };
+
+ async function startCopy() {
+ setCopyInProgress(true);
+ setCopyResult({});
+ try {
+ const copySavedObjectsResult = await spacesManager.copySavedObjects(
+ [
+ {
+ type: savedObject.type,
+ id: savedObject.id,
+ },
+ ],
+ copyOptions.selectedSpaceIds,
+ copyOptions.includeRelated,
+ copyOptions.overwrite
+ );
+ const processedResult = mapValues(copySavedObjectsResult, processImportResponse);
+ setCopyResult(processedResult);
+ } catch (e) {
+ setCopyInProgress(false);
+ toastNotifications.addError(e, {
+ title: i18n.translate('xpack.spaces.management.copyToSpace.copyErrorTitle', {
+ defaultMessage: 'Error copying saved object',
+ }),
+ });
+ }
+ }
+
+ async function finishCopy() {
+ const needsConflictResolution = Object.values(retries).some(spaceRetry =>
+ spaceRetry.some(retry => retry.overwrite)
+ );
+
+ if (needsConflictResolution) {
+ setConflictResolutionInProgress(true);
+ try {
+ await spacesManager.resolveCopySavedObjectsErrors(
+ [
+ {
+ type: savedObject.type,
+ id: savedObject.id,
+ },
+ ],
+ retries,
+ copyOptions.includeRelated
+ );
+
+ toastNotifications.addSuccess(
+ i18n.translate('xpack.spaces.management.copyToSpace.resolveCopySuccessTitle', {
+ defaultMessage: 'Overwrite successful',
+ })
+ );
+
+ onClose();
+ } catch (e) {
+ setCopyInProgress(false);
+ toastNotifications.addError(e, {
+ title: i18n.translate('xpack.spaces.management.copyToSpace.resolveCopyErrorTitle', {
+ defaultMessage: 'Error resolving saved object conflicts',
+ }),
+ });
+ }
+ } else {
+ onClose();
+ }
+ }
+
+ const getFlyoutBody = () => {
+ // Step 1: loading assets for main form
+ if (isLoading) {
+ return ;
+ }
+
+ // Step 1a: assets loaded, but no spaces are available for copy.
+ if (eligibleSpaces.length === 0) {
+ return (
+
+
+
+ }
+ title={
+
+
+
+ }
+ />
+ );
+ }
+
+ // Step 2: Copy has not been initiated yet; User must fill out form to continue.
+ if (!copyInProgress) {
+ return (
+
+ );
+ }
+
+ // Step3: Copy operation is in progress
+ return (
+
+ );
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {savedObject.meta.title}
+
+
+
+
+
+
+ {getFlyoutBody()}
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout_footer.tsx b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout_footer.tsx
new file mode 100644
index 0000000000000..f8d6fdf85205e
--- /dev/null
+++ b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout_footer.tsx
@@ -0,0 +1,199 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { Fragment } from 'react';
+import { ProcessedImportResponse } from 'ui/management/saved_objects_management';
+import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiStat, EuiHorizontalRule } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import { ImportRetry } from '../../../../lib/copy_saved_objects_to_space/types';
+
+interface Props {
+ copyInProgress: boolean;
+ conflictResolutionInProgress: boolean;
+ initialCopyFinished: boolean;
+ copyResult: Record;
+ retries: Record;
+ numberOfSelectedSpaces: number;
+ onCopyStart: () => void;
+ onCopyFinish: () => void;
+}
+export const CopyToSpaceFlyoutFooter = (props: Props) => {
+ const { copyInProgress, initialCopyFinished, copyResult, retries } = props;
+
+ let summarizedResults = {
+ successCount: 0,
+ overwriteConflictCount: 0,
+ conflictCount: 0,
+ unresolvableErrorCount: 0,
+ };
+ if (copyResult) {
+ summarizedResults = Object.entries(copyResult).reduce((acc, result) => {
+ const [spaceId, spaceResult] = result;
+ const overwriteCount = (retries[spaceId] || []).filter(c => c.overwrite).length;
+ return {
+ loading: false,
+ successCount: acc.successCount + spaceResult.importCount,
+ overwriteConflictCount: acc.overwriteConflictCount + overwriteCount,
+ conflictCount:
+ acc.conflictCount +
+ spaceResult.failedImports.filter(i => i.error.type === 'conflict').length -
+ overwriteCount,
+ unresolvableErrorCount:
+ acc.unresolvableErrorCount +
+ spaceResult.failedImports.filter(i => i.error.type !== 'conflict').length,
+ };
+ }, summarizedResults);
+ }
+
+ const getButton = () => {
+ let actionButton;
+ if (initialCopyFinished) {
+ const hasPendingOverwrites = summarizedResults.overwriteConflictCount > 0;
+
+ const buttonText = hasPendingOverwrites ? (
+
+ ) : (
+
+ );
+ actionButton = (
+ props.onCopyFinish()}
+ data-test-subj="cts-finish-button"
+ >
+ {buttonText}
+
+ );
+ } else {
+ actionButton = (
+ props.onCopyStart()}
+ data-test-subj="cts-initiate-button"
+ disabled={props.numberOfSelectedSpaces === 0 || copyInProgress}
+ >
+ {props.numberOfSelectedSpaces > 0 ? (
+
+ ) : (
+
+ )}
+
+ );
+ }
+
+ return (
+
+ {actionButton}
+
+ );
+ };
+
+ if (!copyInProgress) {
+ return getButton();
+ }
+
+ return (
+
+
+
+
+ }
+ />
+
+ {summarizedResults.overwriteConflictCount > 0 && (
+
+ 0 ? 'primary' : 'subdued'}
+ isLoading={!initialCopyFinished}
+ textAlign="center"
+ description={
+
+ }
+ />
+
+ )}
+
+ 0 ? 'primary' : 'subdued'}
+ isLoading={!initialCopyFinished}
+ textAlign="center"
+ description={
+
+ }
+ />
+
+
+ 0 ? 'danger' : 'subdued'}
+ isLoading={!initialCopyFinished}
+ textAlign="center"
+ description={
+
+ }
+ />
+
+
+
+ {getButton()}
+
+ );
+};
diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_form.tsx b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_form.tsx
new file mode 100644
index 0000000000000..2a7e17c253f0b
--- /dev/null
+++ b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_form.tsx
@@ -0,0 +1,83 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import {
+ EuiSwitch,
+ EuiSpacer,
+ EuiHorizontalRule,
+ EuiFormRow,
+ EuiListGroup,
+ EuiListGroupItem,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { CopyOptions } from '../../../../lib/copy_saved_objects_to_space/types';
+import { Space } from '../../../../../common/model/space';
+import { SelectableSpacesControl } from './selectable_spaces_control';
+
+interface Props {
+ spaces: Space[];
+ onUpdate: (copyOptions: CopyOptions) => void;
+ copyOptions: CopyOptions;
+}
+
+export const CopyToSpaceForm = (props: Props) => {
+ const setOverwrite = (overwrite: boolean) => props.onUpdate({ ...props.copyOptions, overwrite });
+
+ const setSelectedSpaceIds = (selectedSpaceIds: string[]) =>
+ props.onUpdate({ ...props.copyOptions, selectedSpaceIds });
+
+ return (
+
+
+
+
+
+ }
+ />
+
+
+
+
+
+ }
+ checked={props.copyOptions.overwrite}
+ onChange={e => setOverwrite(e.target.checked)}
+ />
+
+
+
+
+ }
+ fullWidth
+ >
+ setSelectedSpaceIds(selection)}
+ />
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/index.ts b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/index.ts
new file mode 100644
index 0000000000000..071ae95b8c274
--- /dev/null
+++ b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { CopySavedObjectsToSpaceFlyout } from './copy_to_space_flyout';
diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/processing_copy_to_space.tsx b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/processing_copy_to_space.tsx
new file mode 100644
index 0000000000000..1b712e84d4a05
--- /dev/null
+++ b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/processing_copy_to_space.tsx
@@ -0,0 +1,115 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { Fragment } from 'react';
+import {
+ ProcessedImportResponse,
+ SavedObjectsManagementRecord,
+} from 'ui/management/saved_objects_management';
+import {
+ EuiSpacer,
+ EuiText,
+ EuiListGroup,
+ EuiListGroupItem,
+ EuiHorizontalRule,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { summarizeCopyResult } from '../../../../lib/copy_saved_objects_to_space';
+import { Space } from '../../../../../common/model/space';
+import { CopyOptions, ImportRetry } from '../../../../lib/copy_saved_objects_to_space/types';
+import { SpaceResult } from './space_result';
+
+interface Props {
+ savedObject: SavedObjectsManagementRecord;
+ copyInProgress: boolean;
+ conflictResolutionInProgress: boolean;
+ copyResult: Record;
+ retries: Record;
+ onRetriesChange: (retries: Record) => void;
+ spaces: Space[];
+ copyOptions: CopyOptions;
+}
+
+export const ProcessingCopyToSpace = (props: Props) => {
+ function updateRetries(spaceId: string, updatedRetries: ImportRetry[]) {
+ props.onRetriesChange({
+ ...props.retries,
+ [spaceId]: updatedRetries,
+ });
+ }
+
+ return (
+
+
+
+ ) : (
+
+ )
+ }
+ />
+
+ ) : (
+
+ )
+ }
+ />
+
+
+
+
+
+
+
+
+ {props.copyOptions.selectedSpaceIds.map(id => {
+ const space = props.spaces.find(s => s.id === id) as Space;
+ const spaceCopyResult = props.copyResult[space.id];
+ const summarizedSpaceCopyResult = summarizeCopyResult(
+ props.savedObject,
+ spaceCopyResult,
+ props.copyOptions.includeRelated
+ );
+
+ return (
+
+ updateRetries(space.id, retries)}
+ conflictResolutionInProgress={props.conflictResolutionInProgress}
+ />
+
+
+ );
+ })}
+
+ );
+};
diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/selectable_spaces_control.tsx b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/selectable_spaces_control.tsx
new file mode 100644
index 0000000000000..42d5707531380
--- /dev/null
+++ b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/selectable_spaces_control.tsx
@@ -0,0 +1,81 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { Fragment, useState } from 'react';
+import { EuiSelectable, EuiLoadingSpinner } from '@elastic/eui';
+import { SpaceAvatar } from '../../../../components';
+import { Space } from '../../../../../common/model/space';
+
+interface Props {
+ spaces: Space[];
+ selectedSpaceIds: string[];
+ onChange: (selectedSpaceIds: string[]) => void;
+ disabled?: boolean;
+}
+
+interface SpaceOption {
+ label: string;
+ prepend?: any;
+ checked: 'on' | 'off' | null;
+ ['data-space-id']: string;
+ disabled?: boolean;
+}
+
+export const SelectableSpacesControl = (props: Props) => {
+ const [options, setOptions] = useState([]);
+
+ // TODO: update once https://github.com/elastic/eui/issues/2071 is fixed
+ if (options.length === 0) {
+ setOptions(
+ props.spaces.map(space => ({
+ label: space.name,
+ prepend: ,
+ checked: props.selectedSpaceIds.includes(space.id) ? 'on' : null,
+ ['data-space-id']: space.id,
+ ['data-test-subj']: `cts-space-selector-row-${space.id}`,
+ }))
+ );
+ }
+
+ function updateSelectedSpaces(selectedOptions: SpaceOption[]) {
+ if (props.disabled) return;
+
+ const selectedSpaceIds = selectedOptions
+ .filter(opt => opt.checked)
+ .map(opt => opt['data-space-id']);
+
+ props.onChange(selectedSpaceIds);
+ // TODO: remove once https://github.com/elastic/eui/issues/2071 is fixed
+ setOptions(selectedOptions);
+ }
+
+ if (options.length === 0) {
+ return ;
+ }
+
+ return (
+ updateSelectedSpaces(newOptions as SpaceOption[])}
+ listProps={{
+ bordered: true,
+ rowHeight: 40,
+ className: 'spcCopyToSpace__spacesList',
+ 'data-test-subj': 'cts-form-space-selector',
+ }}
+ searchable
+ >
+ {(list, search) => {
+ return (
+
+ {search}
+ {list}
+
+ );
+ }}
+
+ );
+};
diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result.tsx b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result.tsx
new file mode 100644
index 0000000000000..b27be4d1715e8
--- /dev/null
+++ b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result.tsx
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer } from '@elastic/eui';
+import { SavedObjectsManagementRecord } from 'ui/management/saved_objects_management';
+import { SummarizedCopyToSpaceResult } from '../../../../lib/copy_saved_objects_to_space';
+import { SpaceAvatar } from '../../../../components';
+import { Space } from '../../../../../common/model/space';
+import { CopyStatusSummaryIndicator } from './copy_status_summary_indicator';
+import { SpaceCopyResultDetails } from './space_result_details';
+import { ImportRetry } from '../../../../lib/copy_saved_objects_to_space/types';
+
+interface Props {
+ savedObject: SavedObjectsManagementRecord;
+ space: Space;
+ summarizedCopyResult: SummarizedCopyToSpaceResult;
+ retries: ImportRetry[];
+ onRetriesChange: (retries: ImportRetry[]) => void;
+ conflictResolutionInProgress: boolean;
+}
+
+export const SpaceResult = (props: Props) => {
+ const {
+ space,
+ summarizedCopyResult,
+ retries,
+ onRetriesChange,
+ savedObject,
+ conflictResolutionInProgress,
+ } = props;
+ const spaceHasPendingOverwrites = retries.some(r => r.overwrite);
+
+ return (
+
+
+
+
+
+ {space.name}
+
+
+ }
+ extraAction={
+
+ }
+ >
+
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result_details.tsx b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result_details.tsx
new file mode 100644
index 0000000000000..43639641d541c
--- /dev/null
+++ b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/space_result_details.tsx
@@ -0,0 +1,124 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { SavedObjectsManagementRecord } from 'ui/management/saved_objects_management';
+import { SummarizedCopyToSpaceResult } from 'plugins/spaces/lib/copy_saved_objects_to_space';
+import { EuiText, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { Space } from '../../../../../common/model/space';
+import { CopyStatusIndicator } from './copy_status_indicator';
+import { ImportRetry } from '../../../../lib/copy_saved_objects_to_space/types';
+
+interface Props {
+ savedObject: SavedObjectsManagementRecord;
+ summarizedCopyResult: SummarizedCopyToSpaceResult;
+ space: Space;
+ retries: ImportRetry[];
+ onRetriesChange: (retries: ImportRetry[]) => void;
+ conflictResolutionInProgress: boolean;
+}
+
+export const SpaceCopyResultDetails = (props: Props) => {
+ const onOverwriteClick = (object: { type: string; id: string }) => {
+ const retry = props.retries.find(r => r.type === object.type && r.id === object.id);
+
+ props.onRetriesChange([
+ ...props.retries.filter(r => r !== retry),
+ {
+ type: object.type,
+ id: object.id,
+ overwrite: retry ? !retry.overwrite : true,
+ },
+ ]);
+ };
+
+ const hasPendingOverwrite = (object: { type: string; id: string }) => {
+ const retry = props.retries.find(r => r.type === object.type && r.id === object.id);
+
+ return Boolean(retry && retry.overwrite);
+ };
+
+ const { objects } = props.summarizedCopyResult;
+
+ return (
+
+ {objects.map((object, index) => {
+ const objectOverwritePending = hasPendingOverwrite(object);
+
+ const showOverwriteButton =
+ object.conflicts.length > 0 &&
+ !objectOverwritePending &&
+ !props.conflictResolutionInProgress;
+
+ const showSkipButton =
+ !showOverwriteButton && objectOverwritePending && !props.conflictResolutionInProgress;
+
+ return (
+
+
+
+
+ {object.type}: {object.name || object.id}
+
+
+
+ {showOverwriteButton && (
+
+
+ onOverwriteClick(object)}
+ size="xs"
+ data-test-subj={`cts-overwrite-conflict-${object.id}`}
+ >
+
+
+
+
+ )}
+ {showSkipButton && (
+
+
+ onOverwriteClick(object)}
+ size="xs"
+ data-test-subj={`cts-skip-conflict-${object.id}`}
+ >
+
+
+
+
+ )}
+
+
+
+
+
+
+ );
+ })}
+
+ );
+};
diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.test.tsx b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.test.tsx
index 4a7f419bde82c..24296bf0fa763 100644
--- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.test.tsx
+++ b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.test.tsx
@@ -9,6 +9,7 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { SpacesNavState } from '../../nav_control';
import { DeleteSpacesButton } from './delete_spaces_button';
import { spacesManagerMock } from '../../../lib/mocks';
+import { SpacesManager } from '../../../lib';
const space = {
id: 'my-space',
@@ -28,7 +29,7 @@ describe('DeleteSpacesButton', () => {
const wrapper = shallowWithIntl(
{
const wrapper = mountWithIntl(
@@ -81,7 +82,7 @@ describe('ManageSpacePage', () => {
const wrapper = mountWithIntl(
@@ -127,7 +128,7 @@ describe('ManageSpacePage', () => {
const wrapper = mountWithIntl(
@@ -182,7 +183,7 @@ describe('ManageSpacePage', () => {
const wrapper = mountWithIntl(
diff --git a/x-pack/legacy/plugins/spaces/public/views/management/index.tsx b/x-pack/legacy/plugins/spaces/public/views/management/index.tsx
index 656193b417aa7..46a718bbc6f35 100644
--- a/x-pack/legacy/plugins/spaces/public/views/management/index.tsx
+++ b/x-pack/legacy/plugins/spaces/public/views/management/index.tsx
@@ -11,18 +11,20 @@ import {
PAGE_SUBTITLE_COMPONENT,
PAGE_TITLE_COMPONENT,
registerSettingsComponent,
- // @ts-ignore
} from 'ui/management';
+import { SavedObjectsManagementActionRegistry } from 'ui/management/saved_objects_management';
// @ts-ignore
import routes from 'ui/routes';
+import { SpacesManager } from '../../lib';
import { AdvancedSettingsSubtitle } from './components/advanced_settings_subtitle';
import { AdvancedSettingsTitle } from './components/advanced_settings_title';
+import { CopyToSpaceSavedObjectsManagementAction } from '../../lib/copy_saved_objects_to_space';
const MANAGE_SPACES_KEY = 'spaces';
routes.defaults(/\/management/, {
resolve: {
- spacesManagementSection(activeSpace: any) {
+ spacesManagementSection(activeSpace: any, spaceSelectorURL: string) {
function getKibanaSection() {
return management.getSection('kibana');
}
@@ -45,6 +47,18 @@ routes.defaults(/\/management/, {
});
}
+ // Customize Saved Objects Management
+ const action = new CopyToSpaceSavedObjectsManagementAction(
+ new SpacesManager(spaceSelectorURL),
+ activeSpace.space
+ );
+ // This route resolve function executes any time the management screen is loaded, and we want to ensure
+ // that this action is only registered once.
+ if (!SavedObjectsManagementActionRegistry.has(action.id)) {
+ SavedObjectsManagementActionRegistry.register(action);
+ }
+
+ // Customize Advanced Settings
const PageTitle = () => ;
registerSettingsComponent(PAGE_TITLE_COMPONENT, PageTitle, true);
diff --git a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_pages.test.tsx b/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_pages.test.tsx
index a59ad8084de22..369218179507b 100644
--- a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_pages.test.tsx
+++ b/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_pages.test.tsx
@@ -11,6 +11,7 @@ import React from 'react';
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { SpaceAvatar } from '../../../components';
import { spacesManagerMock } from '../../../lib/mocks';
+import { SpacesManager } from '../../../lib';
import { SpacesNavState } from '../../nav_control';
import { SpacesGridPage } from './spaces_grid_page';
@@ -49,7 +50,7 @@ describe('SpacesGridPage', () => {
expect(
shallowWithIntl(
@@ -60,7 +61,7 @@ describe('SpacesGridPage', () => {
it('renders the list of spaces', async () => {
const wrapper = mountWithIntl(
diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx b/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx
index c30433fb3bf08..c0d04342a69c8 100644
--- a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx
+++ b/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx
@@ -8,6 +8,7 @@ import { mount, shallow } from 'enzyme';
import React from 'react';
import { SpaceAvatar } from '../../components';
import { spacesManagerMock } from '../../lib/mocks';
+import { SpacesManager } from '../../lib';
import { SpacesHeaderNavButton } from './components/spaces_header_nav_button';
import { NavControlPopover } from './nav_control_popover';
@@ -23,7 +24,7 @@ describe('NavControlPopover', () => {
const wrapper = shallow(
@@ -54,7 +55,7 @@ describe('NavControlPopover', () => {
const wrapper = mount(
diff --git a/x-pack/legacy/plugins/spaces/server/lib/spaces_client/index.ts b/x-pack/legacy/plugins/spaces/server/lib/spaces_client/index.ts
index de0039e6e39c2..54c778ae3839e 100644
--- a/x-pack/legacy/plugins/spaces/server/lib/spaces_client/index.ts
+++ b/x-pack/legacy/plugins/spaces/server/lib/spaces_client/index.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { SpacesClient, GetSpacePurpose } from './spaces_client';
+export { SpacesClient } from './spaces_client';
diff --git a/x-pack/legacy/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts b/x-pack/legacy/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts
index 5e82b75ce6014..78ad10bbd9164 100644
--- a/x-pack/legacy/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts
+++ b/x-pack/legacy/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts
@@ -4,10 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SpacesClient, GetSpacePurpose } from './spaces_client';
+import { SpacesClient } from './spaces_client';
import { AuthorizationService } from '../../../../security/server/lib/authorization/service';
import { actionsFactory } from '../../../../security/server/lib/authorization/actions';
import { SpacesConfigType, config } from '../../new_platform/config';
+import { GetSpacePurpose } from '../../../common/model/types';
const createMockAuditLogger = () => {
return {
diff --git a/x-pack/legacy/plugins/spaces/server/lib/spaces_client/spaces_client.ts b/x-pack/legacy/plugins/spaces/server/lib/spaces_client/spaces_client.ts
index 04bb30b1a84e1..6d30084d0dc86 100644
--- a/x-pack/legacy/plugins/spaces/server/lib/spaces_client/spaces_client.ts
+++ b/x-pack/legacy/plugins/spaces/server/lib/spaces_client/spaces_client.ts
@@ -12,10 +12,10 @@ import { isReservedSpace } from '../../../common/is_reserved_space';
import { Space } from '../../../common/model/space';
import { SpacesAuditLogger } from '../audit_logger';
import { SpacesConfigType } from '../../new_platform/config';
+import { GetSpacePurpose } from '../../../common/model/types';
type SpacesClientRequestFacade = Legacy.Request | KibanaRequest;
-export type GetSpacePurpose = 'any' | 'copySavedObjectsIntoSpace';
const SUPPORTED_GET_SPACE_PURPOSES: GetSpacePurpose[] = ['any', 'copySavedObjectsIntoSpace'];
const PURPOSE_PRIVILEGE_MAP: Record<
diff --git a/x-pack/legacy/plugins/spaces/server/routes/api/external/get.ts b/x-pack/legacy/plugins/spaces/server/routes/api/external/get.ts
index ec1a4b5d0baf0..fd6e60ccb0b07 100644
--- a/x-pack/legacy/plugins/spaces/server/routes/api/external/get.ts
+++ b/x-pack/legacy/plugins/spaces/server/routes/api/external/get.ts
@@ -7,9 +7,10 @@
import Boom from 'boom';
import Joi from 'joi';
import { RequestQuery } from 'hapi';
+import { GetSpacePurpose } from '../../../../common/model/types';
import { Space } from '../../../../common/model/space';
import { wrapError } from '../../../lib/errors';
-import { SpacesClient, GetSpacePurpose } from '../../../lib/spaces_client';
+import { SpacesClient } from '../../../lib/spaces_client';
import { ExternalRouteDeps, ExternalRouteRequestFacade } from '.';
export function initGetSpacesApi(deps: ExternalRouteDeps) {
diff --git a/x-pack/test/functional/apps/spaces/copy_saved_objects.ts b/x-pack/test/functional/apps/spaces/copy_saved_objects.ts
new file mode 100644
index 0000000000000..c4e905b3babd0
--- /dev/null
+++ b/x-pack/test/functional/apps/spaces/copy_saved_objects.ts
@@ -0,0 +1,119 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import expect from '@kbn/expect';
+import { SpacesService } from '../../../common/services';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function spaceSelectorFunctonalTests({
+ getService,
+ getPageObjects,
+}: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const spaces: SpacesService = getService('spaces');
+ const testSubjects = getService('testSubjects');
+ const PageObjects = getPageObjects(['security', 'settings', 'copySavedObjectsToSpace']);
+
+ describe('Copy Saved Objects to Space', function() {
+ before(async () => {
+ await esArchiver.load('spaces/copy_saved_objects');
+
+ await spaces.create({
+ id: 'marketing',
+ name: 'Marketing',
+ disabledFeatures: [],
+ });
+
+ await spaces.create({
+ id: 'sales',
+ name: 'Sales',
+ disabledFeatures: [],
+ });
+
+ await PageObjects.security.login(null, null, {
+ expectSpaceSelector: true,
+ });
+
+ await PageObjects.settings.navigateTo();
+ await PageObjects.settings.clickKibanaSavedObjects();
+ });
+
+ after(async () => {
+ await spaces.delete('sales');
+ await spaces.delete('marketing');
+ await esArchiver.unload('spaces/copy_saved_objects');
+ });
+
+ it('allows a dashboard to be copied to the marketing space, with all references', async () => {
+ const destinationSpaceId = 'marketing';
+
+ await PageObjects.copySavedObjectsToSpace.openCopyToSpaceFlyoutForObject('A Dashboard');
+
+ await PageObjects.copySavedObjectsToSpace.setupForm({
+ overwrite: true,
+ destinationSpaceId,
+ });
+
+ await PageObjects.copySavedObjectsToSpace.startCopy();
+
+ // Wait for successful copy
+ await testSubjects.waitForDeleted(`cts-summary-indicator-loading-${destinationSpaceId}`);
+ await testSubjects.existOrFail(`cts-summary-indicator-success-${destinationSpaceId}`);
+
+ const summaryCounts = await PageObjects.copySavedObjectsToSpace.getSummaryCounts();
+
+ expect(summaryCounts).to.eql({
+ copied: 3,
+ skipped: 0,
+ errors: 0,
+ overwrite: undefined,
+ });
+
+ await PageObjects.copySavedObjectsToSpace.finishCopy();
+ });
+
+ it('allows conflicts to be resolved', async () => {
+ const destinationSpaceId = 'sales';
+
+ await PageObjects.copySavedObjectsToSpace.openCopyToSpaceFlyoutForObject('A Dashboard');
+
+ await PageObjects.copySavedObjectsToSpace.setupForm({
+ overwrite: false,
+ destinationSpaceId,
+ });
+
+ await PageObjects.copySavedObjectsToSpace.startCopy();
+
+ // Wait for successful copy with conflict warning
+ await testSubjects.waitForDeleted(`cts-summary-indicator-loading-${destinationSpaceId}`);
+ await testSubjects.existOrFail(`cts-summary-indicator-conflicts-${destinationSpaceId}`);
+
+ const summaryCounts = await PageObjects.copySavedObjectsToSpace.getSummaryCounts();
+
+ expect(summaryCounts).to.eql({
+ copied: 2,
+ skipped: 1,
+ errors: 0,
+ overwrite: undefined,
+ });
+
+ // Mark conflict for overwrite
+ await testSubjects.click(`cts-space-result-${destinationSpaceId}`);
+ await testSubjects.click(`cts-overwrite-conflict-logstash-*`);
+
+ // Verify summary changed
+ const updatedSummaryCounts = await PageObjects.copySavedObjectsToSpace.getSummaryCounts(true);
+
+ expect(updatedSummaryCounts).to.eql({
+ copied: 2,
+ skipped: 0,
+ overwrite: 1,
+ errors: 0,
+ });
+
+ await PageObjects.copySavedObjectsToSpace.finishCopy();
+ });
+ });
+}
diff --git a/x-pack/test/functional/apps/spaces/index.ts b/x-pack/test/functional/apps/spaces/index.ts
index 56e2d410678ff..7cc704a41becc 100644
--- a/x-pack/test/functional/apps/spaces/index.ts
+++ b/x-pack/test/functional/apps/spaces/index.ts
@@ -9,6 +9,7 @@ export default function spacesApp({ loadTestFile }: FtrProviderContext) {
describe('Spaces app', function spacesAppTestSuite() {
this.tags('ciGroup4');
+ loadTestFile(require.resolve('./copy_saved_objects'));
loadTestFile(require.resolve('./feature_controls/spaces_security'));
loadTestFile(require.resolve('./spaces_selection'));
});
diff --git a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json
new file mode 100644
index 0000000000000..944b91e8be114
--- /dev/null
+++ b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json
@@ -0,0 +1,111 @@
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "type": "doc",
+ "id": "space:default",
+ "source": {
+ "space": {
+ "name": "Default",
+ "description": "This is the default space!",
+ "disabledFeatures": [],
+ "_reserved": true
+ },
+ "type": "space",
+ "migrationVersion": {
+ "space": "6.6.0"
+ }
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "type": "doc",
+ "id": "index-pattern:logstash-*",
+ "source": {
+ "index-pattern": {
+ "title": "logstash-*",
+ "timeFieldName": "@timestamp",
+ "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]"
+ },
+ "type": "index-pattern",
+ "migrationVersion": {
+ "index-pattern": "6.5.0"
+ },
+ "updated_at": "2018-12-21T00:43:07.096Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "type": "doc",
+ "id": "sales:index-pattern:logstash-*",
+ "source": {
+ "namespace": "sales",
+ "index-pattern": {
+ "title": "logstash-*",
+ "timeFieldName": "@timestamp",
+ "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]"
+ },
+ "type": "index-pattern",
+ "migrationVersion": {
+ "index-pattern": "6.5.0"
+ },
+ "updated_at": "2018-12-21T00:43:07.096Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "type": "doc",
+ "id": "visualization:75c3e060-1e7c-11e9-8488-65449e65d0ed",
+ "source": {
+ "visualization": {
+ "title": "A Pie",
+ "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}",
+ "uiStateJSON": "{}",
+ "description": "",
+ "version": 1,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}"
+ }
+ },
+ "type": "visualization",
+ "updated_at": "2019-01-22T19:32:31.206Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "type": "doc",
+ "id": "dashboard:my-dashboard",
+ "source": {
+ "dashboard": {
+ "title": "A Dashboard",
+ "hits": 0,
+ "description": "",
+ "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"75c3e060-1e7c-11e9-8488-65449e65d0ed\",\"embeddableConfig\":{}}]",
+ "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}",
+ "version": 1,
+ "timeRestore": false,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}"
+ }
+ },
+ "type": "dashboard",
+ "updated_at": "2019-01-22T19:32:47.232Z"
+ }
+ }
+}
diff --git a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/mappings.json b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/mappings.json
new file mode 100644
index 0000000000000..2ec403e51fca9
--- /dev/null
+++ b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/mappings.json
@@ -0,0 +1,333 @@
+{
+ "type": "index",
+ "value": {
+ "index": ".kibana",
+ "mappings": {
+ "doc": {
+ "dynamic": "strict",
+ "properties": {
+ "migrationVersion": {
+ "dynamic": "true",
+ "properties": {
+ "index-pattern": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ }
+ }
+ }
+ }
+ },
+ "config": {
+ "dynamic": "true",
+ "properties": {
+ "buildNum": {
+ "type": "keyword"
+ },
+ "dateFormat:tz": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "defaultIndex": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "notifications:lifetime:banner": {
+ "type": "long"
+ },
+ "notifications:lifetime:error": {
+ "type": "long"
+ },
+ "notifications:lifetime:info": {
+ "type": "long"
+ },
+ "notifications:lifetime:warning": {
+ "type": "long"
+ }
+ }
+ },
+ "dashboard": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "optionsJSON": {
+ "type": "text"
+ },
+ "panelsJSON": {
+ "type": "text"
+ },
+ "refreshInterval": {
+ "properties": {
+ "display": {
+ "type": "keyword"
+ },
+ "pause": {
+ "type": "boolean"
+ },
+ "section": {
+ "type": "integer"
+ },
+ "value": {
+ "type": "integer"
+ }
+ }
+ },
+ "timeFrom": {
+ "type": "keyword"
+ },
+ "timeRestore": {
+ "type": "boolean"
+ },
+ "timeTo": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "index-pattern": {
+ "properties": {
+ "fieldFormatMap": {
+ "type": "text"
+ },
+ "fields": {
+ "type": "text"
+ },
+ "intervalName": {
+ "type": "keyword"
+ },
+ "notExpandable": {
+ "type": "boolean"
+ },
+ "sourceFilters": {
+ "type": "text"
+ },
+ "timeFieldName": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ }
+ }
+ },
+ "search": {
+ "properties": {
+ "columns": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "sort": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "server": {
+ "properties": {
+ "uuid": {
+ "type": "keyword"
+ }
+ }
+ },
+ "timelion-sheet": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "timelion_chart_height": {
+ "type": "integer"
+ },
+ "timelion_columns": {
+ "type": "integer"
+ },
+ "timelion_interval": {
+ "type": "keyword"
+ },
+ "timelion_other_interval": {
+ "type": "keyword"
+ },
+ "timelion_rows": {
+ "type": "integer"
+ },
+ "timelion_sheet": {
+ "type": "text"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "updated_at": {
+ "type": "date"
+ },
+ "url": {
+ "properties": {
+ "accessCount": {
+ "type": "long"
+ },
+ "accessDate": {
+ "type": "date"
+ },
+ "createDate": {
+ "type": "date"
+ },
+ "url": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 2048,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ }
+ }
+ },
+ "visualization": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "savedSearchId": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ },
+ "visState": {
+ "type": "text"
+ }
+ }
+ },
+ "namespace": {
+ "type": "keyword"
+ },
+ "space": {
+ "properties": {
+ "_reserved": {
+ "type": "boolean"
+ },
+ "color": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "disabledFeatures": {
+ "type": "keyword"
+ },
+ "initials": {
+ "type": "keyword"
+ },
+ "name": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 2048,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ }
+ }
+ },
+ "references": {
+ "type": "nested",
+ "properties": {
+ "name": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "id": {
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ }
+ },
+ "settings": {
+ "index": {
+ "auto_expand_replicas": "0-1",
+ "number_of_replicas": "0",
+ "number_of_shards": "1"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts b/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts
new file mode 100644
index 0000000000000..3908b2ddecf1d
--- /dev/null
+++ b/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts
@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { FtrProviderContext } from '../ftr_provider_context';
+
+function extractCountFromSummary(str: string) {
+ return parseInt(str.split('\n')[1], 10);
+}
+
+export function CopySavedObjectsToSpacePageProvider({ getService }: FtrProviderContext) {
+ const testSubjects = getService('testSubjects');
+ const browser = getService('browser');
+ const find = getService('find');
+
+ return {
+ async searchForObject(objectName: string) {
+ const searchBox = await testSubjects.find('savedObjectSearchBar');
+ await searchBox.clearValue();
+ await searchBox.type(objectName);
+ await searchBox.pressKeys(browser.keys.ENTER);
+ },
+
+ async openCopyToSpaceFlyoutForObject(objectName: string) {
+ await this.searchForObject(objectName);
+
+ // Click action button to show context menu
+ await find.clickByCssSelector(
+ 'table.euiTable tbody tr.euiTableRow td.euiTableRowCell:last-child .euiButtonIcon'
+ );
+
+ const actions = await find.allByCssSelector('.euiContextMenuItem');
+
+ for (const action of actions) {
+ const actionText = await action.getVisibleText();
+ if (actionText === 'Copy to space') {
+ await action.click();
+ break;
+ }
+ }
+
+ await testSubjects.existOrFail('copy-to-space-flyout');
+ },
+
+ async setupForm({
+ overwrite,
+ destinationSpaceId,
+ }: {
+ overwrite?: boolean;
+ destinationSpaceId: string;
+ }) {
+ if (!overwrite) {
+ await testSubjects.click('cts-form-overwrite');
+ }
+ await testSubjects.click(`cts-space-selector-row-${destinationSpaceId}`);
+ },
+
+ async startCopy() {
+ await testSubjects.click('cts-initiate-button');
+ },
+
+ async finishCopy() {
+ await testSubjects.click('cts-finish-button');
+ await testSubjects.waitForDeleted('copy-to-space-flyout');
+ },
+
+ async getSummaryCounts(includeOverwrite: boolean = false) {
+ const copied = extractCountFromSummary(
+ await testSubjects.getVisibleText('cts-summary-success-count')
+ );
+ const skipped = extractCountFromSummary(
+ await testSubjects.getVisibleText('cts-summary-conflict-count')
+ );
+ const errors = extractCountFromSummary(
+ await testSubjects.getVisibleText('cts-summary-error-count')
+ );
+
+ let overwrite;
+ if (includeOverwrite) {
+ overwrite = extractCountFromSummary(
+ await testSubjects.getVisibleText('cts-summary-overwrite-count')
+ );
+ } else {
+ await testSubjects.missingOrFail('cts-summary-overwrite-count', { timeout: 250 });
+ }
+
+ return {
+ copied,
+ skipped,
+ errors,
+ overwrite,
+ };
+ },
+ };
+}
diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts
index c80b8c76c639c..690a77ff00aa1 100644
--- a/x-pack/test/functional/page_objects/index.ts
+++ b/x-pack/test/functional/page_objects/index.ts
@@ -43,6 +43,7 @@ import { IndexLifecycleManagementPageProvider } from './index_lifecycle_manageme
import { SnapshotRestorePageProvider } from './snapshot_restore_page';
import { CrossClusterReplicationPageProvider } from './cross_cluster_replication_page';
import { RemoteClustersPageProvider } from './remote_clusters_page';
+import { CopySavedObjectsToSpacePageProvider } from './copy_saved_objects_to_space_page';
// just like services, PageObjects are defined as a map of
// names to Providers. Merge in Kibana's or pick specific ones
@@ -72,4 +73,5 @@ export const pageObjects = {
snapshotRestore: SnapshotRestorePageProvider,
crossClusterReplication: CrossClusterReplicationPageProvider,
remoteClusters: RemoteClustersPageProvider,
+ copySavedObjectsToSpace: CopySavedObjectsToSpacePageProvider,
};