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 1bdf6dede4de..50bd0e3e993c 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 8d4fce3073ba..672cb23baf5a 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 000000000000..c7223a859ee3 --- /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 000000000000..a09f842e3671 --- /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 000000000000..902b7f01c19f --- /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 000000000000..f4085a674f49 --- /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 21ef201a4d3a..be60a83c1983 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 000000000000..58c36da33dbd --- /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 000000000000..4ed1937ebf78 --- /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 000000000000..be23d90cc242 --- /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 000000000000..7517fa48ad8b --- /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 000000000000..7eddf3f4891e --- /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 000000000000..d2576ca5c6c1 --- /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 7d4fb1b90fe1..4d7a9251228e 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 cd2939f83e20..d39b751e30a8 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 8cf4e6d57991..e7cbdfe2de7e 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 b34b52cd48c7..3c3fa502a917 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 000000000000..0ad5f72ba3e4 --- /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 000000000000..9e8f1e7c1a6f --- /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 000000000000..4663b73f1cb7 --- /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 000000000000..f8d6fdf85205 --- /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 000000000000..2a7e17c253f0 --- /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 000000000000..071ae95b8c27 --- /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 000000000000..1b712e84d4a0 --- /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 000000000000..42d570753138 --- /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 000000000000..b27be4d1715e --- /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 000000000000..43639641d541 --- /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 4a7f419bde82..24296bf0fa76 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 656193b417aa..46a718bbc6f3 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 a59ad8084de2..369218179507 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 c30433fb3bf0..c0d04342a69c 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 de0039e6e39c..54c778ae3839 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 5e82b75ce601..78ad10bbd916 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 04bb30b1a84e..6d30084d0dc8 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 ec1a4b5d0baf..fd6e60ccb0b0 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 000000000000..c4e905b3babd --- /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 56e2d410678f..7cc704a41bec 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 000000000000..944b91e8be11 --- /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 000000000000..2ec403e51fca --- /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 000000000000..3908b2ddecf1 --- /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 c80b8c76c639..690a77ff00aa 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, };