/* * 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 { SavedObjectMetaData, OnSaveProps } from 'src/plugins/saved_objects/public'; import { first } from 'rxjs/operators'; import { SavedObjectAttributes } from '../../../../core/public'; import { EmbeddableFactoryDefinition, EmbeddableOutput, ErrorEmbeddable, IContainer, } from '../../../embeddable/public'; import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; import { VisualizeByReferenceInput, VisualizeByValueInput, VisualizeEmbeddable, VisualizeInput, VisualizeOutput, VisualizeSavedObjectAttributes, } from './visualize_embeddable'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { SerializedVis, Vis } from '../vis'; import { getCapabilities, getTypes, getUISettings, getSavedVisualizationsLoader, } from '../services'; import { showNewVisModal } from '../wizard'; import { convertToSerializedVis } from '../saved_visualizations/_saved_vis'; import { createVisEmbeddableFromObject } from './create_vis_embeddable_from_object'; import { StartServicesGetter } from '../../../opensearch_dashboards_utils/public'; import { VisualizationsStartDeps } from '../plugin'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; import { AttributeService } from '../../../dashboard/public'; import { checkForDuplicateTitle } from '../../../saved_objects/public'; interface VisualizationAttributes extends SavedObjectAttributes { visState: string; } export interface VisualizeEmbeddableFactoryDeps { start: StartServicesGetter< Pick >; } export class VisualizeEmbeddableFactory implements EmbeddableFactoryDefinition< VisualizeInput, VisualizeOutput | EmbeddableOutput, VisualizeEmbeddable | DisabledLabEmbeddable, VisualizationAttributes > { public readonly type = VISUALIZE_EMBEDDABLE_TYPE; private attributeService?: AttributeService< VisualizeSavedObjectAttributes, VisualizeByValueInput, VisualizeByReferenceInput >; public readonly savedObjectMetaData: SavedObjectMetaData = { name: i18n.translate('visualizations.savedObjectName', { defaultMessage: 'Visualization' }), includeFields: ['visState'], type: 'visualization', getIconForSavedObject: (savedObject) => { return ( getTypes().get(JSON.parse(savedObject.attributes.visState).type).icon || 'visualizeApp' ); }, getTooltipForSavedObject: (savedObject) => { return `${savedObject.attributes.title} (${ getTypes().get(JSON.parse(savedObject.attributes.visState).type).title })`; }, showSavedObject: (savedObject) => { const typeName: string = JSON.parse(savedObject.attributes.visState).type; const visType = getTypes().get(typeName); if (!visType) { return false; } if (getUISettings().get(VISUALIZE_ENABLE_LABS_SETTING)) { return true; } return visType.stage !== 'experimental'; }, }; constructor(private readonly deps: VisualizeEmbeddableFactoryDeps) {} public async isEditable() { return getCapabilities().visualize.save as boolean; } public getDisplayName() { return i18n.translate('visualizations.displayName', { defaultMessage: 'visualization', }); } public async getCurrentAppId() { return await this.deps.start().core.application.currentAppId$.pipe(first()).toPromise(); } private async getAttributeService() { if (!this.attributeService) { this.attributeService = await this.deps .start() .plugins.dashboard.getAttributeService< VisualizeSavedObjectAttributes, VisualizeByValueInput, VisualizeByReferenceInput >(this.type, { saveMethod: this.saveMethod.bind(this), checkForDuplicateTitle: this.checkTitle.bind(this), }); } return this.attributeService!; } public async createFromSavedObject( savedObjectId: string, input: Partial & { id: string }, parent?: IContainer ): Promise { const savedVisualizations = getSavedVisualizationsLoader(); try { const savedObject = await savedVisualizations.get(savedObjectId); const visState = convertToSerializedVis(savedObject); const vis = new Vis(savedObject.visState.type, visState); await vis.setState(visState); return createVisEmbeddableFromObject(this.deps)( vis, input, savedVisualizations, await this.getAttributeService(), parent ); } catch (e) { console.error(e); // eslint-disable-line no-console return new ErrorEmbeddable(e, input, parent); } } public async create(input: VisualizeInput & { savedVis?: SerializedVis }, parent?: IContainer) { // TODO: This is a bit of a hack to preserve the original functionality. Ideally we will clean this up // to allow for in place creation of visualizations without having to navigate away to a new URL. if (input.savedVis) { const visState = input.savedVis; const vis = new Vis(visState.type, visState); await vis.setState(visState); const savedVisualizations = getSavedVisualizationsLoader(); return createVisEmbeddableFromObject(this.deps)( vis, input, savedVisualizations, await this.getAttributeService(), parent ); } else { showNewVisModal({ originatingApp: await this.getCurrentAppId(), outsideVisualizeApp: true, }); return undefined; } } private async saveMethod(attributes: VisualizeSavedObjectAttributes): Promise<{ id: string }> { try { const { title, savedVis } = attributes; const visObj = attributes.vis; if (!savedVis) { throw new Error('No Saved Vis'); } const saveOptions = { confirmOverwrite: false, returnToOrigin: true, }; savedVis.title = title; savedVis.copyOnSave = false; savedVis.description = ''; savedVis.searchSourceFields = visObj?.data.searchSource?.getSerializedFields(); const serializedVis = ((visObj as unknown) as Vis).serialize(); const { params, data } = serializedVis; savedVis.visState = { title, type: serializedVis.type, params, aggs: data.aggs, }; if (visObj) { savedVis.uiStateJSON = visObj?.uiState.toString(); } const id = await savedVis.save(saveOptions); if (!id || id === '') { throw new Error( i18n.translate('visualizations.savingVisualizationFailed.errorMsg', { defaultMessage: 'Saving a visualization failed', }) ); } return { id }; } catch (error) { throw error; } } public async checkTitle(props: OnSaveProps): Promise { const savedObjectsClient = await this.deps.start().core.savedObjects.client; const overlays = await this.deps.start().core.overlays; return checkForDuplicateTitle( { title: props.newTitle, copyOnSave: false, lastSavedTitle: '', getOpenSearchType: () => this.type, getDisplayName: this.getDisplayName || (() => this.type), }, props.isTitleDuplicateConfirmed, props.onTitleDuplicate, { savedObjectsClient, overlays, } ); } }