diff --git a/examples/embeddable_examples/kibana.json b/examples/embeddable_examples/kibana.json index 771c19cfdbd3d..0ac40ae1889de 100644 --- a/examples/embeddable_examples/kibana.json +++ b/examples/embeddable_examples/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["embeddable", "uiActions"], + "requiredPlugins": ["embeddable", "uiActions", "dashboard"], "optionalPlugins": [], "extraPublicDirs": ["public/todo", "public/hello_world", "public/todo/todo_ref_embeddable"], "requiredBundles": ["kibanaReact"] diff --git a/examples/embeddable_examples/public/book/add_book_to_library_action.tsx b/examples/embeddable_examples/public/book/add_book_to_library_action.tsx new file mode 100644 index 0000000000000..b74a1d5642982 --- /dev/null +++ b/examples/embeddable_examples/public/book/add_book_to_library_action.tsx @@ -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 { i18n } from '@kbn/i18n'; +import { createAction, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public'; +import { BookEmbeddable, BOOK_EMBEDDABLE } from './book_embeddable'; +import { ViewMode, isReferenceOrValueEmbeddable } from '../../../../src/plugins/embeddable/public'; + +interface ActionContext { + embeddable: BookEmbeddable; +} + +export const ACTION_ADD_BOOK_TO_LIBRARY = 'ACTION_ADD_BOOK_TO_LIBRARY'; + +export const createAddBookToLibraryAction = () => + createAction({ + getDisplayName: () => + i18n.translate('embeddableExamples.book.addToLibrary', { + defaultMessage: 'Add Book To Library', + }), + type: ACTION_ADD_BOOK_TO_LIBRARY, + order: 100, + getIconType: () => 'folderCheck', + isCompatible: async ({ embeddable }: ActionContext) => { + return ( + embeddable.type === BOOK_EMBEDDABLE && + embeddable.getInput().viewMode === ViewMode.EDIT && + isReferenceOrValueEmbeddable(embeddable) && + !embeddable.inputIsRefType(embeddable.getInput()) + ); + }, + execute: async ({ embeddable }: ActionContext) => { + if (!isReferenceOrValueEmbeddable(embeddable)) { + throw new IncompatibleActionError(); + } + const newInput = await embeddable.getInputAsRefType(); + embeddable.updateInput(newInput); + }, + }); diff --git a/examples/embeddable_examples/public/book/book_component.tsx b/examples/embeddable_examples/public/book/book_component.tsx index 064e13c131a0a..e46487641b913 100644 --- a/examples/embeddable_examples/public/book/book_component.tsx +++ b/examples/embeddable_examples/public/book/book_component.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { EuiFlexItem, EuiFlexGroup, EuiIcon } from '@elastic/eui'; import { EuiText } from '@elastic/eui'; -import { EuiFlexGrid } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { withEmbeddableSubscription } from '../../../../src/plugins/embeddable/public'; import { BookEmbeddableInput, BookEmbeddableOutput, BookEmbeddable } from './book_embeddable'; @@ -44,26 +44,32 @@ function wrapSearchTerms(task?: string, search?: string) { ); } -export function BookEmbeddableComponentInner({ input: { search }, output: { attributes } }: Props) { +export function BookEmbeddableComponentInner({ + input: { search }, + output: { attributes }, + embeddable, +}: Props) { const title = attributes?.title; const author = attributes?.author; const readIt = attributes?.readIt; + const byReference = embeddable.inputIsRefType(embeddable.getInput()); + return ( <EuiFlexGroup gutterSize="s"> <EuiFlexItem> - <EuiFlexGrid columns={1} gutterSize="none"> + <EuiFlexGroup direction="column" gutterSize="s"> {title ? ( <EuiFlexItem> <EuiText data-test-subj="bookEmbeddableTitle"> - <h3>{wrapSearchTerms(title, search)},</h3> + <h3>{wrapSearchTerms(title, search)}</h3> </EuiText> </EuiFlexItem> ) : null} {author ? ( <EuiFlexItem> <EuiText data-test-subj="bookEmbeddableAuthor"> - <h5>-{wrapSearchTerms(author, search)}</h5> + -{wrapSearchTerms(author, search)} </EuiText> </EuiFlexItem> ) : null} @@ -76,7 +82,21 @@ export function BookEmbeddableComponentInner({ input: { search }, output: { attr <EuiIcon type="cross" /> </EuiFlexItem> )} - </EuiFlexGrid> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem> + <EuiText data-test-subj="bookEmbeddableAuthor"> + <EuiIcon type={byReference ? 'folderCheck' : 'folderExclamation'} />{' '} + <span> + {byReference + ? i18n.translate('embeddableExamples.book.byReferenceLabel', { + defaultMessage: 'Book is By Reference', + }) + : i18n.translate('embeddableExamples.book.byValueLabel', { + defaultMessage: 'Book is By Value', + })} + </span> + </EuiText> </EuiFlexItem> </EuiFlexGroup> ); diff --git a/examples/embeddable_examples/public/book/book_embeddable.tsx b/examples/embeddable_examples/public/book/book_embeddable.tsx index d49bd3280d97d..dd9418c0e8596 100644 --- a/examples/embeddable_examples/public/book/book_embeddable.tsx +++ b/examples/embeddable_examples/public/book/book_embeddable.tsx @@ -25,10 +25,11 @@ import { IContainer, EmbeddableOutput, SavedObjectEmbeddableInput, - AttributeService, + ReferenceOrValueEmbeddable, } from '../../../../src/plugins/embeddable/public'; import { BookSavedObjectAttributes } from '../../common'; import { BookEmbeddableComponent } from './book_component'; +import { AttributeService } from '../../../../src/plugins/dashboard/public'; export const BOOK_EMBEDDABLE = 'book'; export type BookEmbeddableInput = BookByValueInput | BookByReferenceInput; @@ -59,7 +60,8 @@ function getHasMatch(search?: string, savedAttributes?: BookSavedObjectAttribute ); } -export class BookEmbeddable extends Embeddable<BookEmbeddableInput, BookEmbeddableOutput> { +export class BookEmbeddable extends Embeddable<BookEmbeddableInput, BookEmbeddableOutput> + implements ReferenceOrValueEmbeddable<BookByValueInput, BookByReferenceInput> { public readonly type = BOOK_EMBEDDABLE; private subscription: Subscription; private node?: HTMLElement; @@ -96,6 +98,18 @@ export class BookEmbeddable extends Embeddable<BookEmbeddableInput, BookEmbeddab }); } + inputIsRefType = (input: BookEmbeddableInput): input is BookByReferenceInput => { + return this.attributeService.inputIsRefType(input); + }; + + getInputAsValueType = async (): Promise<BookByValueInput> => { + return this.attributeService.getInputAsValueType(this.input); + }; + + getInputAsRefType = async (): Promise<BookByReferenceInput> => { + return this.attributeService.getInputAsRefType(this.input, { showSaveModal: true }); + }; + public render(node: HTMLElement) { if (this.node) { ReactDOM.unmountComponentAtNode(this.node); @@ -113,6 +127,10 @@ export class BookEmbeddable extends Embeddable<BookEmbeddableInput, BookEmbeddab }); } + public getTitle() { + return this.getOutput()?.title || this.getOutput().attributes?.title; + } + public destroy() { super.destroy(); this.subscription.unsubscribe(); diff --git a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx index f4a32fb498a2d..4c144c3843c47 100644 --- a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx +++ b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx @@ -23,9 +23,7 @@ import { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../../common'; import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; import { EmbeddableFactoryDefinition, - EmbeddableStart, IContainer, - AttributeService, EmbeddableFactory, } from '../../../../src/plugins/embeddable/public'; import { @@ -38,9 +36,10 @@ import { } from './book_embeddable'; import { CreateEditBookComponent } from './create_edit_book_component'; import { OverlayStart } from '../../../../src/core/public'; +import { DashboardStart, AttributeService } from '../../../../src/plugins/dashboard/public'; interface StartServices { - getAttributeService: EmbeddableStart['getAttributeService']; + getAttributeService: DashboardStart['getAttributeService']; openModal: OverlayStart['openModal']; } @@ -85,6 +84,16 @@ export class BookEmbeddableFactoryDefinition }); } + // This is currently required due to the distinction in container.ts and the + // default error implementation in default_embeddable_factory_provider.ts + public async createFromSavedObject( + savedObjectId: string, + input: BookEmbeddableInput, + parent?: IContainer + ) { + return this.create(input, parent); + } + public getDisplayName() { return i18n.translate('embeddableExamples.book.displayName', { defaultMessage: 'Book', @@ -122,6 +131,6 @@ export class BookEmbeddableFactoryDefinition BookByReferenceInput >(this.type); } - return this.attributeService; + return this.attributeService!; } } diff --git a/examples/embeddable_examples/public/book/edit_book_action.tsx b/examples/embeddable_examples/public/book/edit_book_action.tsx index 222f70e0be60f..b31d69696598e 100644 --- a/examples/embeddable_examples/public/book/edit_book_action.tsx +++ b/examples/embeddable_examples/public/book/edit_book_action.tsx @@ -22,11 +22,7 @@ import { i18n } from '@kbn/i18n'; import { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../../common'; import { createAction } from '../../../../src/plugins/ui_actions/public'; import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; -import { - ViewMode, - EmbeddableStart, - SavedObjectEmbeddableInput, -} from '../../../../src/plugins/embeddable/public'; +import { ViewMode, SavedObjectEmbeddableInput } from '../../../../src/plugins/embeddable/public'; import { BookEmbeddable, BOOK_EMBEDDABLE, @@ -34,10 +30,11 @@ import { BookByValueInput, } from './book_embeddable'; import { CreateEditBookComponent } from './create_edit_book_component'; +import { DashboardStart } from '../../../../src/plugins/dashboard/public'; interface StartServices { openModal: OverlayStart['openModal']; - getAttributeService: EmbeddableStart['getAttributeService']; + getAttributeService: DashboardStart['getAttributeService']; } interface ActionContext { diff --git a/examples/embeddable_examples/public/book/unlink_book_from_library_action.tsx b/examples/embeddable_examples/public/book/unlink_book_from_library_action.tsx new file mode 100644 index 0000000000000..cef77092a642a --- /dev/null +++ b/examples/embeddable_examples/public/book/unlink_book_from_library_action.tsx @@ -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 { i18n } from '@kbn/i18n'; +import { createAction, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public'; +import { BookEmbeddable, BOOK_EMBEDDABLE } from './book_embeddable'; +import { ViewMode, isReferenceOrValueEmbeddable } from '../../../../src/plugins/embeddable/public'; + +interface ActionContext { + embeddable: BookEmbeddable; +} + +export const ACTION_UNLINK_BOOK_FROM_LIBRARY = 'ACTION_UNLINK_BOOK_FROM_LIBRARY'; + +export const createUnlinkBookFromLibraryAction = () => + createAction({ + getDisplayName: () => + i18n.translate('embeddableExamples.book.unlinkFromLibrary', { + defaultMessage: 'Unlink Book from Library Item', + }), + type: ACTION_UNLINK_BOOK_FROM_LIBRARY, + order: 100, + getIconType: () => 'folderExclamation', + isCompatible: async ({ embeddable }: ActionContext) => { + return ( + embeddable.type === BOOK_EMBEDDABLE && + embeddable.getInput().viewMode === ViewMode.EDIT && + isReferenceOrValueEmbeddable(embeddable) && + embeddable.inputIsRefType(embeddable.getInput()) + ); + }, + execute: async ({ embeddable }: ActionContext) => { + if (!isReferenceOrValueEmbeddable(embeddable)) { + throw new IncompatibleActionError(); + } + const newInput = await embeddable.getInputAsValueType(); + embeddable.updateInput(newInput); + }, + }); diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index 95f4f5b41e198..0c6ed1eb3be48 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -58,6 +58,15 @@ import { BookEmbeddableFactoryDefinition, } from './book/book_embeddable_factory'; import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; +import { + ACTION_ADD_BOOK_TO_LIBRARY, + createAddBookToLibraryAction, +} from './book/add_book_to_library_action'; +import { DashboardStart } from '../../../src/plugins/dashboard/public'; +import { + ACTION_UNLINK_BOOK_FROM_LIBRARY, + createUnlinkBookFromLibraryAction, +} from './book/unlink_book_from_library_action'; export interface EmbeddableExamplesSetupDependencies { embeddable: EmbeddableSetup; @@ -66,6 +75,7 @@ export interface EmbeddableExamplesSetupDependencies { export interface EmbeddableExamplesStartDependencies { embeddable: EmbeddableStart; + dashboard: DashboardStart; } interface ExampleEmbeddableFactories { @@ -86,6 +96,8 @@ export interface EmbeddableExamplesStart { declare module '../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { [ACTION_EDIT_BOOK]: { embeddable: BookEmbeddable }; + [ACTION_ADD_BOOK_TO_LIBRARY]: { embeddable: BookEmbeddable }; + [ACTION_UNLINK_BOOK_FROM_LIBRARY]: { embeddable: BookEmbeddable }; } } @@ -144,17 +156,25 @@ export class EmbeddableExamplesPlugin this.exampleEmbeddableFactories.getBookEmbeddableFactory = deps.embeddable.registerEmbeddableFactory( BOOK_EMBEDDABLE, new BookEmbeddableFactoryDefinition(async () => ({ - getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService, + getAttributeService: (await core.getStartServices())[1].dashboard.getAttributeService, openModal: (await core.getStartServices())[0].overlays.openModal, })) ); const editBookAction = createEditBookAction(async () => ({ - getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService, + getAttributeService: (await core.getStartServices())[1].dashboard.getAttributeService, openModal: (await core.getStartServices())[0].overlays.openModal, })); deps.uiActions.registerAction(editBookAction); deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, editBookAction.id); + + const addBookToLibraryAction = createAddBookToLibraryAction(); + deps.uiActions.registerAction(addBookToLibraryAction); + deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, addBookToLibraryAction.id); + + const unlinkBookFromLibraryAction = createUnlinkBookFromLibraryAction(); + deps.uiActions.registerAction(unlinkBookFromLibraryAction); + deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkBookFromLibraryAction.id); } public start( diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx new file mode 100644 index 0000000000000..c2f529fe399f3 --- /dev/null +++ b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx @@ -0,0 +1,156 @@ +/* + * 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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EmbeddableInput, + SavedObjectEmbeddableInput, + isSavedObjectEmbeddableInput, + IEmbeddable, +} from '../embeddable_plugin'; +import { + SavedObjectsClientContract, + SimpleSavedObject, + I18nStart, + NotificationsStart, +} from '../../../../core/public'; +import { + SavedObjectSaveModal, + showSaveModal, + OnSaveProps, + SaveResult, +} from '../../../saved_objects/public'; + +/** + * The attribute service is a shared, generic service that embeddables can use to provide the functionality + * required to fulfill the requirements of the ReferenceOrValueEmbeddable interface. The attribute_service + * can also be used as a higher level wrapper to transform an embeddable input shape that references a saved object + * into an embeddable input shape that contains that saved object's attributes by value. + */ +export class AttributeService< + SavedObjectAttributes extends { title: string }, + ValType extends EmbeddableInput & { attributes: SavedObjectAttributes }, + RefType extends SavedObjectEmbeddableInput +> { + constructor( + private type: string, + private savedObjectsClient: SavedObjectsClientContract, + private i18nContext: I18nStart['Context'], + private toasts: NotificationsStart['toasts'] + ) {} + + public async unwrapAttributes(input: RefType | ValType): Promise<SavedObjectAttributes> { + if (this.inputIsRefType(input)) { + const savedObject: SimpleSavedObject<SavedObjectAttributes> = await this.savedObjectsClient.get< + SavedObjectAttributes + >(this.type, input.savedObjectId); + return savedObject.attributes; + } + return input.attributes; + } + + public async wrapAttributes( + newAttributes: SavedObjectAttributes, + useRefType: boolean, + embeddable?: IEmbeddable + ): Promise<Omit<ValType | RefType, 'id'>> { + const savedObjectId = + embeddable && isSavedObjectEmbeddableInput(embeddable.getInput()) + ? (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId + : undefined; + if (!useRefType) { + return { attributes: newAttributes } as ValType; + } else { + try { + if (savedObjectId) { + await this.savedObjectsClient.update(this.type, savedObjectId, newAttributes); + return { savedObjectId } as RefType; + } else { + const savedItem = await this.savedObjectsClient.create(this.type, newAttributes); + return { savedObjectId: savedItem.id } as RefType; + } + } catch (error) { + this.toasts.addDanger({ + title: i18n.translate('dashboard.attributeService.saveToLibraryError', { + defaultMessage: `Panel was not saved to the library. Error: {errorMessage}`, + values: { + errorMessage: error.message, + }, + }), + 'data-test-subj': 'saveDashboardFailure', + }); + return Promise.reject({ error }); + } + } + } + + inputIsRefType = (input: ValType | RefType): input is RefType => { + return isSavedObjectEmbeddableInput(input); + }; + + getInputAsValueType = async (input: ValType | RefType): Promise<ValType> => { + if (!this.inputIsRefType(input)) { + return input; + } + const attributes = await this.unwrapAttributes(input); + return { + ...input, + savedObjectId: undefined, + attributes, + }; + }; + + getInputAsRefType = async ( + input: ValType | RefType, + saveOptions?: { showSaveModal: boolean } | { title: string } + ): Promise<RefType> => { + if (this.inputIsRefType(input)) { + return input; + } + + return new Promise<RefType>((resolve, reject) => { + const onSave = async (props: OnSaveProps): Promise<SaveResult> => { + try { + input.attributes.title = props.newTitle; + const wrappedInput = (await this.wrapAttributes(input.attributes, true)) as RefType; + resolve(wrappedInput); + return { id: wrappedInput.savedObjectId }; + } catch (error) { + reject(); + return { error }; + } + }; + + if (saveOptions && (saveOptions as { showSaveModal: boolean }).showSaveModal) { + showSaveModal( + <SavedObjectSaveModal + onSave={onSave} + onClose={() => reject()} + title={input.attributes.title} + showCopyOnSave={false} + objectType={this.type} + showDescription={false} + />, + this.i18nContext + ); + } + }); + }; +} diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index dcfde67cd9f13..8a9954cc77a2e 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -40,6 +40,7 @@ export { export { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; export { SavedObjectDashboard } from './saved_dashboards'; export { SavedDashboardPanel } from './types'; +export { AttributeService } from './attribute_service/attribute_service'; export function plugin(initializerContext: PluginInitializerContext) { return new DashboardPlugin(initializerContext); diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index f1319665d258b..3b0863a9f4651 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -34,7 +34,13 @@ import { ScopedHistory, } from 'src/core/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; -import { CONTEXT_MENU_TRIGGER, EmbeddableSetup, EmbeddableStart } from '../../embeddable/public'; +import { + CONTEXT_MENU_TRIGGER, + EmbeddableSetup, + EmbeddableStart, + SavedObjectEmbeddableInput, + EmbeddableInput, +} from '../../embeddable/public'; import { DataPublicPluginSetup, DataPublicPluginStart, esFilters } from '../../data/public'; import { SharePluginSetup, SharePluginStart, UrlGeneratorContract } from '../../share/public'; import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; @@ -85,6 +91,7 @@ import { DashboardConstants } from './dashboard_constants'; import { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; import { UrlGeneratorState } from '../../share/public'; +import { AttributeService } from '.'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -131,6 +138,13 @@ export interface DashboardStart { dashboardUrlGenerator?: DashboardUrlGenerator; dashboardFeatureFlagConfig: DashboardFeatureFlagConfig; DashboardContainerByValueRenderer: ReturnType<typeof createDashboardContainerByValueRenderer>; + getAttributeService: < + A extends { title: string }, + V extends EmbeddableInput & { attributes: A }, + R extends SavedObjectEmbeddableInput + >( + type: string + ) => AttributeService<A, V, R>; } declare module '../../../plugins/ui_actions/public' { @@ -420,6 +434,13 @@ export class DashboardPlugin DashboardContainerByValueRenderer: createDashboardContainerByValueRenderer({ factory: dashboardContainerFactory, }), + getAttributeService: (type: string) => + new AttributeService( + type, + core.savedObjects.client, + core.i18n.Context, + core.notifications.toasts + ), }; } diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index fafbdda148de8..57253c1f741ab 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -28,7 +28,8 @@ export { ACTION_EDIT_PANEL, Adapters, AddPanelAction, - AttributeService, + ReferenceOrValueEmbeddable, + isReferenceOrValueEmbeddable, ChartActionContext, Container, ContainerInput, diff --git a/src/plugins/embeddable/public/lib/embeddables/attribute_service.ts b/src/plugins/embeddable/public/lib/embeddables/attribute_service.ts deleted file mode 100644 index a33f592350d9a..0000000000000 --- a/src/plugins/embeddable/public/lib/embeddables/attribute_service.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { SavedObjectsClientContract } from '../../../../../core/public'; -import { - SavedObjectEmbeddableInput, - isSavedObjectEmbeddableInput, - EmbeddableInput, - IEmbeddable, -} from '.'; -import { SimpleSavedObject } from '../../../../../core/public'; - -export class AttributeService< - SavedObjectAttributes, - ValType extends EmbeddableInput & { attributes: SavedObjectAttributes }, - RefType extends SavedObjectEmbeddableInput -> { - constructor(private type: string, private savedObjectsClient: SavedObjectsClientContract) {} - - public async unwrapAttributes(input: RefType | ValType): Promise<SavedObjectAttributes> { - if (isSavedObjectEmbeddableInput(input)) { - const savedObject: SimpleSavedObject<SavedObjectAttributes> = await this.savedObjectsClient.get< - SavedObjectAttributes - >(this.type, input.savedObjectId); - return savedObject.attributes; - } - return input.attributes; - } - - public async wrapAttributes( - newAttributes: SavedObjectAttributes, - useRefType: boolean, - embeddable?: IEmbeddable - ): Promise<Omit<ValType | RefType, 'id'>> { - const savedObjectId = - embeddable && isSavedObjectEmbeddableInput(embeddable.getInput()) - ? (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId - : undefined; - - if (useRefType) { - if (savedObjectId) { - await this.savedObjectsClient.update(this.type, savedObjectId, newAttributes); - return { savedObjectId } as RefType; - } else { - const savedItem = await this.savedObjectsClient.create(this.type, newAttributes); - return { savedObjectId: savedItem.id } as RefType; - } - } else { - return { attributes: newAttributes } as ValType; - } - } -} diff --git a/src/plugins/embeddable/public/lib/embeddables/index.ts b/src/plugins/embeddable/public/lib/embeddables/index.ts index 06cb6e322acf3..5bab5ac27f3cc 100644 --- a/src/plugins/embeddable/public/lib/embeddables/index.ts +++ b/src/plugins/embeddable/public/lib/embeddables/index.ts @@ -25,5 +25,4 @@ export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable'; export { withEmbeddableSubscription } from './with_subscription'; export { EmbeddableRoot } from './embeddable_root'; export * from './saved_object_embeddable'; -export { AttributeService } from './attribute_service'; export { EmbeddableRenderer, EmbeddableRendererProps } from './embeddable_renderer'; diff --git a/src/plugins/embeddable/public/lib/index.ts b/src/plugins/embeddable/public/lib/index.ts index b757fa59a7f3a..aef4c33ee1078 100644 --- a/src/plugins/embeddable/public/lib/index.ts +++ b/src/plugins/embeddable/public/lib/index.ts @@ -25,3 +25,4 @@ export * from './triggers'; export * from './containers'; export * from './panel'; export * from './state_transfer'; +export * from './reference_or_value_embeddable'; diff --git a/src/plugins/embeddable/public/lib/reference_or_value_embeddable/index.ts b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/index.ts new file mode 100644 index 0000000000000..e9b8521a35ba5 --- /dev/null +++ b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { ReferenceOrValueEmbeddable, isReferenceOrValueEmbeddable } from './types'; diff --git a/src/plugins/embeddable/public/lib/reference_or_value_embeddable/types.ts b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/types.ts new file mode 100644 index 0000000000000..eaf5c94a09138 --- /dev/null +++ b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/types.ts @@ -0,0 +1,56 @@ +/* + * 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 { EmbeddableInput, SavedObjectEmbeddableInput } from '..'; + +/** + * Any embeddable that implements this interface will be able to use input that is + * either by reference (backed by a saved object) OR by value, (provided + * by the container). + * @public + */ +export interface ReferenceOrValueEmbeddable< + ValTypeInput extends EmbeddableInput = EmbeddableInput, + RefTypeInput extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput +> { + /** + * determines whether the input is by value or by reference. + */ + inputIsRefType: (input: ValTypeInput | RefTypeInput) => input is RefTypeInput; + + /** + * Gets the embeddable's current input as its Value type + */ + getInputAsValueType: () => Promise<ValTypeInput>; + + /** + * Gets the embeddable's current input as its Reference type + */ + getInputAsRefType: () => Promise<RefTypeInput>; +} + +export function isReferenceOrValueEmbeddable( + incoming: unknown +): incoming is ReferenceOrValueEmbeddable { + return ( + !!(incoming as ReferenceOrValueEmbeddable).inputIsRefType && + !!(incoming as ReferenceOrValueEmbeddable).getInputAsValueType && + !!(incoming as ReferenceOrValueEmbeddable).getInputAsRefType + ); +} diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx index 94aa980e446ca..fa79af909a427 100644 --- a/src/plugins/embeddable/public/mocks.tsx +++ b/src/plugins/embeddable/public/mocks.tsx @@ -97,7 +97,6 @@ const createStartContract = (): Start => { getEmbeddableFactories: jest.fn(), getEmbeddableFactory: jest.fn(), EmbeddablePanel: jest.fn(), - getAttributeService: jest.fn(), getEmbeddablePanel: jest.fn(), getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer), }; diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index 319cbf8ec44b4..3cbd49279564f 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -37,10 +37,8 @@ import { defaultEmbeddableFactoryProvider, IEmbeddable, EmbeddablePanel, - SavedObjectEmbeddableInput, } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; -import { AttributeService } from './lib/embeddables/attribute_service'; import { EmbeddableStateTransfer } from './lib/state_transfer'; export interface EmbeddableSetupDependencies { @@ -75,14 +73,6 @@ export interface EmbeddableStart { embeddableFactoryId: string ) => EmbeddableFactory<I, O, E> | undefined; getEmbeddableFactories: () => IterableIterator<EmbeddableFactory>; - getAttributeService: < - A, - V extends EmbeddableInput & { attributes: A }, - R extends SavedObjectEmbeddableInput - >( - type: string - ) => AttributeService<A, V, R>; - EmbeddablePanel: EmbeddablePanelHOC; getEmbeddablePanel: (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC; getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer; @@ -159,7 +149,6 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl return { getEmbeddableFactory: this.getEmbeddableFactory, getEmbeddableFactories: this.getEmbeddableFactories, - getAttributeService: (type: string) => new AttributeService(type, core.savedObjects.client), getStateTransfer: (history?: ScopedHistory) => { return history ? new EmbeddableStateTransfer(core.application.navigateToApp, history)