/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. * * Any modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ /* * 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 { i18n } from '@osd/i18n'; import { CoreStart } from 'src/core/public'; import uuid from 'uuid'; import _ from 'lodash'; import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; import { ActionByType, IncompatibleActionError } from '../../../../ui_actions/public'; import { SavedObject } from '../../../../saved_objects/public'; import { PanelNotFoundError, EmbeddableInput, SavedObjectEmbeddableInput, isErrorEmbeddable, ViewMode, PanelState, IEmbeddable, } from '../../../../embeddable/public'; import { placePanelBeside, IPanelPlacementBesideArgs, } from '../embeddable/panel/dashboard_panel_placement'; import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..'; export const ACTION_CLONE_PANEL = 'clonePanel'; export interface ClonePanelActionContext { embeddable: IEmbeddable; } export class ClonePanelAction implements ActionByType { public readonly type = ACTION_CLONE_PANEL; public readonly id = ACTION_CLONE_PANEL; public order = 45; constructor(private core: CoreStart) {} public getDisplayName({ embeddable }: ClonePanelActionContext) { if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { throw new IncompatibleActionError(); } return i18n.translate('dashboard.panel.clonePanel', { defaultMessage: 'Clone panel', }); } public getIconType({ embeddable }: ClonePanelActionContext): EuiIconType { if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { throw new IncompatibleActionError(); } return 'copy'; } public async isCompatible({ embeddable }: ClonePanelActionContext) { return Boolean( !isErrorEmbeddable(embeddable) && embeddable.getInput()?.viewMode !== ViewMode.VIEW && embeddable.getRoot() && embeddable.getRoot().isContainer && embeddable.getRoot().type === DASHBOARD_CONTAINER_TYPE ); } public async execute({ embeddable }: ClonePanelActionContext) { if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { throw new IncompatibleActionError(); } const dashboard = embeddable.getRoot() as DashboardContainer; const panelToClone = dashboard.getInput().panels[embeddable.id] as DashboardPanelState; if (!panelToClone) { throw new PanelNotFoundError(); } dashboard.showPlaceholderUntil( this.cloneEmbeddable(panelToClone, embeddable.type), placePanelBeside, { width: panelToClone.gridData.w, height: panelToClone.gridData.h, currentPanels: dashboard.getInput().panels, placeBesideId: panelToClone.explicitInput.id, } as IPanelPlacementBesideArgs ); } private async getUniqueTitle(rawTitle: string, embeddableType: string): Promise { const clonedTag = i18n.translate('dashboard.panel.title.clonedTag', { defaultMessage: 'copy', }); const cloneRegex = new RegExp(`\\(${clonedTag}\\)`, 'g'); const cloneNumberRegex = new RegExp(`\\(${clonedTag} [0-9]+\\)`, 'g'); const baseTitle = rawTitle.replace(cloneNumberRegex, '').replace(cloneRegex, '').trim(); const similarSavedObjects = await this.core.savedObjects.client.find({ type: embeddableType, perPage: 0, fields: ['title'], searchFields: ['title'], search: `"${baseTitle}"`, }); const similarBaseTitlesCount: number = similarSavedObjects.total - 1; return similarBaseTitlesCount <= 0 ? baseTitle + ` (${clonedTag})` : baseTitle + ` (${clonedTag} ${similarBaseTitlesCount})`; } private async cloneEmbeddable( panelToClone: DashboardPanelState, embeddableType: string ): Promise> { const panelState: PanelState = { type: embeddableType, explicitInput: { ...panelToClone.explicitInput, id: uuid.v4(), }, }; let newTitle: string = ''; if (panelToClone.explicitInput.savedObjectId) { // Fetch existing saved object const savedObjectToClone = await this.core.savedObjects.client.get( embeddableType, panelToClone.explicitInput.savedObjectId ); // Clone the saved object newTitle = await this.getUniqueTitle(savedObjectToClone.attributes.title, embeddableType); const clonedSavedObject = await this.core.savedObjects.client.create( embeddableType, { ..._.cloneDeep(savedObjectToClone.attributes), title: newTitle, }, { references: _.cloneDeep(savedObjectToClone.references) } ); (panelState.explicitInput as SavedObjectEmbeddableInput).savedObjectId = clonedSavedObject.id; } this.core.notifications.toasts.addSuccess({ title: i18n.translate('dashboard.panel.clonedToast', { defaultMessage: 'Cloned panel', }), 'data-test-subj': 'addObjectToContainerSuccess', }); return panelState; } }