From 14dcd6ae22767146cba9b16ed5d0be34bf02c212 Mon Sep 17 00:00:00 2001 From: Devon A Thomson Date: Tue, 4 Aug 2020 16:20:32 -0400 Subject: [PATCH 1/6] Added a first pass for an interface that determines if an embeddable can be treated as either by reference or by value --- .../public/book/book_embeddable.tsx | 17 +++++- .../public/book/book_embeddable_factory.tsx | 2 +- src/plugins/embeddable/public/index.ts | 2 + .../public/lib/embeddables/index.ts | 1 - src/plugins/embeddable/public/lib/index.ts | 1 + .../attribute_service.ts | 29 +++++++++- .../reference_or_value_embeddable/index.ts | 21 +++++++ .../reference_or_value_embeddable/types.ts | 56 +++++++++++++++++++ src/plugins/embeddable/public/plugin.tsx | 2 +- 9 files changed, 126 insertions(+), 5 deletions(-) rename src/plugins/embeddable/public/lib/{embeddables => reference_or_value_embeddable}/attribute_service.ts (77%) create mode 100644 src/plugins/embeddable/public/lib/reference_or_value_embeddable/index.ts create mode 100644 src/plugins/embeddable/public/lib/reference_or_value_embeddable/types.ts diff --git a/examples/embeddable_examples/public/book/book_embeddable.tsx b/examples/embeddable_examples/public/book/book_embeddable.tsx index d49bd3280d97d..e0b1df808805e 100644 --- a/examples/embeddable_examples/public/book/book_embeddable.tsx +++ b/examples/embeddable_examples/public/book/book_embeddable.tsx @@ -26,6 +26,8 @@ import { EmbeddableOutput, SavedObjectEmbeddableInput, AttributeService, + ReferenceOrValueEmbeddable, + isSavedObjectEmbeddableInput, } from '../../../../src/plugins/embeddable/public'; import { BookSavedObjectAttributes } from '../../common'; import { BookEmbeddableComponent } from './book_component'; @@ -59,7 +61,8 @@ function getHasMatch(search?: string, savedAttributes?: BookSavedObjectAttribute ); } -export class BookEmbeddable extends Embeddable { +export class BookEmbeddable extends Embeddable + implements ReferenceOrValueEmbeddable { public readonly type = BOOK_EMBEDDABLE; private subscription: Subscription; private node?: HTMLElement; @@ -96,6 +99,18 @@ export class BookEmbeddable extends Embeddable { + return this.attributeService.inputIsRefType(input); + }; + + getInputAsValueType = async (): Promise => { + return this.attributeService.getInputAsValueType(this.input); + }; + + getInputAsRefType = async (): Promise => { + return this.attributeService.getInputAsRefType(this.input); + }; + public render(node: HTMLElement) { if (this.node) { ReactDOM.unmountComponentAtNode(this.node); diff --git a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx index f4a32fb498a2d..30bbff480d2e5 100644 --- a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx +++ b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx @@ -122,6 +122,6 @@ export class BookEmbeddableFactoryDefinition BookByReferenceInput >(this.type); } - return this.attributeService; + return this.attributeService!; } } diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index fafbdda148de8..206aa9056a1cd 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -29,6 +29,8 @@ export { Adapters, AddPanelAction, AttributeService, + ReferenceOrValueEmbeddable, + isReferenceOrValueEmbeddable, ChartActionContext, Container, ContainerInput, 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/embeddables/attribute_service.ts b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/attribute_service.ts similarity index 77% rename from src/plugins/embeddable/public/lib/embeddables/attribute_service.ts rename to src/plugins/embeddable/public/lib/reference_or_value_embeddable/attribute_service.ts index a33f592350d9a..b51553e05c8ef 100644 --- a/src/plugins/embeddable/public/lib/embeddables/attribute_service.ts +++ b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/attribute_service.ts @@ -23,7 +23,7 @@ import { isSavedObjectEmbeddableInput, EmbeddableInput, IEmbeddable, -} from '.'; +} from '../'; import { SimpleSavedObject } from '../../../../../core/public'; export class AttributeService< @@ -65,4 +65,31 @@ export class AttributeService< return { attributes: newAttributes } as ValType; } } + + inputIsRefType = (input: ValType | RefType): input is RefType => { + return isSavedObjectEmbeddableInput(input); + }; + + getInputAsValueType = async (input: ValType | RefType): Promise => { + if (!this.inputIsRefType(input)) { + return input; + } + const attributes = await this.unwrapAttributes(input); + return { + ...input, + savedObjectId: undefined, + attributes, + }; + }; + + getInputAsRefType = async (input: ValType | RefType): Promise => { + if (this.inputIsRefType(input)) { + return input; + } + const wrappedInput = await this.wrapAttributes(input.attributes, true); + return { + id: input.id, + ...wrappedInput, + } as RefType; + }; } 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..8ebb8664d6d0e --- /dev/null +++ b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/index.ts @@ -0,0 +1,21 @@ +/* + * 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'; +export { AttributeService } from './attribute_service'; 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; + + /** + * Gets the embeddable's current input as its Reference type + */ + getInputAsRefType: () => Promise; +} + +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/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index 508c82c4247ed..e9473d0613019 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -49,7 +49,7 @@ import { isValueClickTriggerContext, } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; -import { AttributeService } from './lib/embeddables/attribute_service'; +import { AttributeService } from './lib'; import { EmbeddableStateTransfer } from './lib/state_transfer'; export interface EmbeddableSetupDependencies { From 6ba38d05d942bad35f982f9abc84b8df97996b9f Mon Sep 17 00:00:00 2001 From: Devon A Thomson Date: Wed, 5 Aug 2020 11:03:33 -0400 Subject: [PATCH 2/6] type fix --- examples/embeddable_examples/public/book/book_embeddable.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/embeddable_examples/public/book/book_embeddable.tsx b/examples/embeddable_examples/public/book/book_embeddable.tsx index e0b1df808805e..d01ac3f2a6141 100644 --- a/examples/embeddable_examples/public/book/book_embeddable.tsx +++ b/examples/embeddable_examples/public/book/book_embeddable.tsx @@ -27,7 +27,6 @@ import { SavedObjectEmbeddableInput, AttributeService, ReferenceOrValueEmbeddable, - isSavedObjectEmbeddableInput, } from '../../../../src/plugins/embeddable/public'; import { BookSavedObjectAttributes } from '../../common'; import { BookEmbeddableComponent } from './book_component'; From d21daef32e0d590b10fbb18b70efa888ee9f7c87 Mon Sep 17 00:00:00 2001 From: Devon A Thomson Date: Thu, 6 Aug 2020 15:49:11 -0400 Subject: [PATCH 3/6] Moved attributeservice to dashboard plugin. Created add and unlink actions to test the referenceOrValueEmbeddable interface. --- examples/embeddable_examples/kibana.json | 2 +- .../book/add_book_to_library_action.tsx | 55 ++++++++++++++++ .../public/book/book_component.tsx | 32 +++++++-- .../public/book/book_embeddable.tsx | 4 +- .../public/book/book_embeddable_factory.tsx | 15 ++++- .../public/book/edit_book_action.tsx | 9 +-- .../book/unlink_book_from_library_action.tsx | 55 ++++++++++++++++ examples/embeddable_examples/public/plugin.ts | 24 ++++++- .../attribute_service/attribute_service.tsx} | 66 +++++++++++++++---- src/plugins/dashboard/public/index.ts | 1 + src/plugins/dashboard/public/plugin.tsx | 18 ++++- src/plugins/embeddable/public/index.ts | 1 - .../reference_or_value_embeddable/index.ts | 1 - src/plugins/embeddable/public/mocks.tsx | 1 - src/plugins/embeddable/public/plugin.tsx | 11 ---- 15 files changed, 248 insertions(+), 47 deletions(-) create mode 100644 examples/embeddable_examples/public/book/add_book_to_library_action.tsx create mode 100644 examples/embeddable_examples/public/book/unlink_book_from_library_action.tsx rename src/plugins/{embeddable/public/lib/reference_or_value_embeddable/attribute_service.ts => dashboard/public/attribute_service/attribute_service.tsx} (58%) 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 ( - + {title ? ( -

{wrapSearchTerms(title, search)},

+

{wrapSearchTerms(title, search)}

) : null} {author ? ( -
-{wrapSearchTerms(author, search)}
+ -{wrapSearchTerms(author, search)}
) : null} @@ -76,7 +82,21 @@ export function BookEmbeddableComponentInner({ input: { search }, output: { attr
)} - +
+ + + + {' '} + + {byReference + ? i18n.translate('embeddableExamples.book.byReferenceLabel', { + defaultMessage: 'Book is By Reference', + }) + : i18n.translate('embeddableExamples.book.byValueLabel', { + defaultMessage: 'Book is By Value', + })} + + ); diff --git a/examples/embeddable_examples/public/book/book_embeddable.tsx b/examples/embeddable_examples/public/book/book_embeddable.tsx index d01ac3f2a6141..ba71222694fc8 100644 --- a/examples/embeddable_examples/public/book/book_embeddable.tsx +++ b/examples/embeddable_examples/public/book/book_embeddable.tsx @@ -25,11 +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; @@ -107,7 +107,7 @@ export class BookEmbeddable extends Embeddable => { - return this.attributeService.getInputAsRefType(this.input); + return this.attributeService.getInputAsRefType(this.input, { showSaveModal: true }); }; public render(node: HTMLElement) { diff --git a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx index 30bbff480d2e5..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', 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/embeddable/public/lib/reference_or_value_embeddable/attribute_service.ts b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx similarity index 58% rename from src/plugins/embeddable/public/lib/reference_or_value_embeddable/attribute_service.ts rename to src/plugins/dashboard/public/attribute_service/attribute_service.tsx index b51553e05c8ef..53c7ee2e74e1b 100644 --- a/src/plugins/embeddable/public/lib/reference_or_value_embeddable/attribute_service.ts +++ b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx @@ -17,24 +17,40 @@ * under the License. */ -import { SavedObjectsClientContract } from '../../../../../core/public'; +import React from 'react'; import { + EmbeddableInput, SavedObjectEmbeddableInput, isSavedObjectEmbeddableInput, - EmbeddableInput, IEmbeddable, -} from '../'; -import { SimpleSavedObject } from '../../../../../core/public'; +} from '../embeddable_plugin'; +import { SavedObjectsClientContract, SimpleSavedObject, I18nStart } 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, ValType extends EmbeddableInput & { attributes: SavedObjectAttributes }, RefType extends SavedObjectEmbeddableInput > { - constructor(private type: string, private savedObjectsClient: SavedObjectsClientContract) {} + constructor( + private type: string, + private savedObjectsClient: SavedObjectsClientContract, + private i18nContext: I18nStart['Context'] + ) {} public async unwrapAttributes(input: RefType | ValType): Promise { - if (isSavedObjectEmbeddableInput(input)) { + if (this.inputIsRefType(input)) { const savedObject: SimpleSavedObject = await this.savedObjectsClient.get< SavedObjectAttributes >(this.type, input.savedObjectId); @@ -82,14 +98,40 @@ export class AttributeService< }; }; - getInputAsRefType = async (input: ValType | RefType): Promise => { + getInputAsRefType = async ( + input: ValType | RefType, + saveOptions?: { showSaveModal: boolean } | { title: string } + ): Promise => { if (this.inputIsRefType(input)) { return input; } - const wrappedInput = await this.wrapAttributes(input.attributes, true); - return { - id: input.id, - ...wrappedInput, - } as RefType; + + return new Promise((resolve, reject) => { + const onSave = async (props: OnSaveProps): Promise => { + try { + const wrappedInput = (await this.wrapAttributes(input.attributes, true)) as RefType; + wrappedInput.title = props.newTitle; + resolve(wrappedInput); + return { id: wrappedInput.savedObjectId }; + } catch (error) { + reject(); + return { error }; + } + }; + + if (saveOptions && (saveOptions as { showSaveModal: boolean }).showSaveModal) { + showSaveModal( + reject()} + title={input.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..487b35a65227e 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; + getAttributeService: < + A, + V extends EmbeddableInput & { attributes: A }, + R extends SavedObjectEmbeddableInput + >( + type: string + ) => AttributeService; } declare module '../../../plugins/ui_actions/public' { @@ -420,6 +434,8 @@ export class DashboardPlugin DashboardContainerByValueRenderer: createDashboardContainerByValueRenderer({ factory: dashboardContainerFactory, }), + getAttributeService: (type: string) => + new AttributeService(type, core.savedObjects.client, core.i18n.Context), }; } diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 206aa9056a1cd..57253c1f741ab 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -28,7 +28,6 @@ export { ACTION_EDIT_PANEL, Adapters, AddPanelAction, - AttributeService, ReferenceOrValueEmbeddable, isReferenceOrValueEmbeddable, ChartActionContext, 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 index 8ebb8664d6d0e..e9b8521a35ba5 100644 --- a/src/plugins/embeddable/public/lib/reference_or_value_embeddable/index.ts +++ b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/index.ts @@ -18,4 +18,3 @@ */ export { ReferenceOrValueEmbeddable, isReferenceOrValueEmbeddable } from './types'; -export { AttributeService } from './attribute_service'; diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx index 48e5483124704..efd0ccdc4553d 100644 --- a/src/plugins/embeddable/public/mocks.tsx +++ b/src/plugins/embeddable/public/mocks.tsx @@ -99,7 +99,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), filtersAndTimeRangeFromContext: jest.fn(), diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index e9473d0613019..4f7d6b30c8fe1 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -43,13 +43,11 @@ import { defaultEmbeddableFactoryProvider, IEmbeddable, EmbeddablePanel, - SavedObjectEmbeddableInput, ChartActionContext, isRangeSelectTriggerContext, isValueClickTriggerContext, } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; -import { AttributeService } from './lib'; import { EmbeddableStateTransfer } from './lib/state_transfer'; export interface EmbeddableSetupDependencies { @@ -84,14 +82,6 @@ export interface EmbeddableStart { embeddableFactoryId: string ) => EmbeddableFactory | undefined; getEmbeddableFactories: () => IterableIterator; - getAttributeService: < - A, - V extends EmbeddableInput & { attributes: A }, - R extends SavedObjectEmbeddableInput - >( - type: string - ) => AttributeService; - /** * Given {@link ChartActionContext} returns a list of `data` plugin {@link Filter} entries. */ @@ -215,7 +205,6 @@ export class EmbeddablePublicPlugin implements Plugin new AttributeService(type, core.savedObjects.client), filtersFromContext, filtersAndTimeRangeFromContext, getStateTransfer: (history?: ScopedHistory) => { From 19b2bc06d3d6244c4e318093518002a0220e83ec Mon Sep 17 00:00:00 2001 From: Devon A Thomson Date: Thu, 6 Aug 2020 17:35:08 -0400 Subject: [PATCH 4/6] type fix --- src/plugins/embeddable/public/plugin.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index 3303bfa2bed0c..3cbd49279564f 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -37,7 +37,6 @@ import { defaultEmbeddableFactoryProvider, IEmbeddable, EmbeddablePanel, - SavedObjectEmbeddableInput, } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; import { EmbeddableStateTransfer } from './lib/state_transfer'; From 230341db12c7a24b11851cc0848e1e9b94ee8a60 Mon Sep 17 00:00:00 2001 From: Devon A Thomson Date: Mon, 10 Aug 2020 13:45:37 -0400 Subject: [PATCH 5/6] Added error handling to the attribute_service save methods --- .../attribute_service/attribute_service.tsx | 44 ++++++++++++++----- src/plugins/dashboard/public/plugin.tsx | 7 ++- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx index 53c7ee2e74e1b..da5171fccd8cc 100644 --- a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx +++ b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx @@ -18,13 +18,19 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { EmbeddableInput, SavedObjectEmbeddableInput, isSavedObjectEmbeddableInput, IEmbeddable, } from '../embeddable_plugin'; -import { SavedObjectsClientContract, SimpleSavedObject, I18nStart } from '../../../../core/public'; +import { + SavedObjectsClientContract, + SimpleSavedObject, + I18nStart, + NotificationsStart, +} from '../../../../core/public'; import { SavedObjectSaveModal, showSaveModal, @@ -46,7 +52,8 @@ export class AttributeService< constructor( private type: string, private savedObjectsClient: SavedObjectsClientContract, - private i18nContext: I18nStart['Context'] + private i18nContext: I18nStart['Context'], + private toasts: NotificationsStart['toasts'] ) {} public async unwrapAttributes(input: RefType | ValType): Promise { @@ -68,17 +75,29 @@ export class AttributeService< 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 { + 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 }); + } } } @@ -112,6 +131,7 @@ export class AttributeService< const wrappedInput = (await this.wrapAttributes(input.attributes, true)) as RefType; wrappedInput.title = props.newTitle; resolve(wrappedInput); + throw new Error(); return { id: wrappedInput.savedObjectId }; } catch (error) { reject(); diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 487b35a65227e..008d51f1447cc 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -435,7 +435,12 @@ export class DashboardPlugin factory: dashboardContainerFactory, }), getAttributeService: (type: string) => - new AttributeService(type, core.savedObjects.client, core.i18n.Context), + new AttributeService( + type, + core.savedObjects.client, + core.i18n.Context, + core.notifications.toasts + ), }; } From ed0cb93eb09591eaea914471fea86686332ea982 Mon Sep 17 00:00:00 2001 From: Devon A Thomson Date: Wed, 12 Aug 2020 13:30:40 -0400 Subject: [PATCH 6/6] Sorted out title issue --- .../embeddable_examples/public/book/book_embeddable.tsx | 4 ++++ .../public/attribute_service/attribute_service.tsx | 7 +++---- src/plugins/dashboard/public/plugin.tsx | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/examples/embeddable_examples/public/book/book_embeddable.tsx b/examples/embeddable_examples/public/book/book_embeddable.tsx index ba71222694fc8..dd9418c0e8596 100644 --- a/examples/embeddable_examples/public/book/book_embeddable.tsx +++ b/examples/embeddable_examples/public/book/book_embeddable.tsx @@ -127,6 +127,10 @@ export class BookEmbeddable extends Embeddable { @@ -128,10 +128,9 @@ export class AttributeService< return new Promise((resolve, reject) => { const onSave = async (props: OnSaveProps): Promise => { try { + input.attributes.title = props.newTitle; const wrappedInput = (await this.wrapAttributes(input.attributes, true)) as RefType; - wrappedInput.title = props.newTitle; resolve(wrappedInput); - throw new Error(); return { id: wrappedInput.savedObjectId }; } catch (error) { reject(); @@ -144,7 +143,7 @@ export class AttributeService< reject()} - title={input.title || ''} + title={input.attributes.title} showCopyOnSave={false} objectType={this.type} showDescription={false} diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 008d51f1447cc..3b0863a9f4651 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -139,7 +139,7 @@ export interface DashboardStart { dashboardFeatureFlagConfig: DashboardFeatureFlagConfig; DashboardContainerByValueRenderer: ReturnType; getAttributeService: < - A, + A extends { title: string }, V extends EmbeddableInput & { attributes: A }, R extends SavedObjectEmbeddableInput >(