From 33fd5cf4579f90337fe6de43bc188d6f9f9866e6 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Thu, 9 Jul 2020 12:22:04 -0400 Subject: [PATCH] [Example] Embeddable by Reference and Value (#68719) Added an attribute service to embeddable start contract which provides a higher level abstraction for embeddables that can be by reference OR by value. Added an example that uses this service. --- .../common/book_saved_object_attributes.ts | 28 ++++ examples/embeddable_examples/common/index.ts | 1 + examples/embeddable_examples/kibana.json | 2 +- .../public/book/book_component.tsx | 90 +++++++++++++ .../public/book/book_embeddable.tsx | 123 +++++++++++++++++ .../public/book/book_embeddable_factory.tsx | 127 ++++++++++++++++++ .../book/create_edit_book_component.tsx | 88 ++++++++++++ .../public/book/edit_book_action.tsx | 93 +++++++++++++ .../embeddable_examples/public/book/index.ts | 21 +++ .../public/create_sample_data.ts | 19 ++- examples/embeddable_examples/public/index.ts | 2 + examples/embeddable_examples/public/plugin.ts | 40 +++++- .../server/book_saved_object.ts | 40 ++++++ examples/embeddable_examples/server/plugin.ts | 2 + .../public/embeddable_panel_example.tsx | 30 +++++ src/plugins/embeddable/public/index.ts | 1 + .../lib/embeddables/attribute_service.ts | 68 ++++++++++ .../public/lib/embeddables/index.ts | 1 + .../embeddables/saved_object_embeddable.ts | 2 +- src/plugins/embeddable/public/mocks.tsx | 1 + src/plugins/embeddable/public/plugin.tsx | 10 ++ 21 files changed, 781 insertions(+), 8 deletions(-) create mode 100644 examples/embeddable_examples/common/book_saved_object_attributes.ts create mode 100644 examples/embeddable_examples/public/book/book_component.tsx create mode 100644 examples/embeddable_examples/public/book/book_embeddable.tsx create mode 100644 examples/embeddable_examples/public/book/book_embeddable_factory.tsx create mode 100644 examples/embeddable_examples/public/book/create_edit_book_component.tsx create mode 100644 examples/embeddable_examples/public/book/edit_book_action.tsx create mode 100644 examples/embeddable_examples/public/book/index.ts create mode 100644 examples/embeddable_examples/server/book_saved_object.ts create mode 100644 src/plugins/embeddable/public/lib/embeddables/attribute_service.ts diff --git a/examples/embeddable_examples/common/book_saved_object_attributes.ts b/examples/embeddable_examples/common/book_saved_object_attributes.ts new file mode 100644 index 0000000000000..62c08b7b81362 --- /dev/null +++ b/examples/embeddable_examples/common/book_saved_object_attributes.ts @@ -0,0 +1,28 @@ +/* + * 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 { SavedObjectAttributes } from '../../../src/core/types'; + +export const BOOK_SAVED_OBJECT = 'book'; + +export interface BookSavedObjectAttributes extends SavedObjectAttributes { + title: string; + author?: string; + readIt?: boolean; +} diff --git a/examples/embeddable_examples/common/index.ts b/examples/embeddable_examples/common/index.ts index 726420fb9bdc3..55715113a12a2 100644 --- a/examples/embeddable_examples/common/index.ts +++ b/examples/embeddable_examples/common/index.ts @@ -18,3 +18,4 @@ */ export { TodoSavedObjectAttributes } from './todo_saved_object_attributes'; +export { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from './book_saved_object_attributes'; diff --git a/examples/embeddable_examples/kibana.json b/examples/embeddable_examples/kibana.json index 486c6322fad93..8ae04c1f6c644 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"], + "requiredPlugins": ["embeddable", "uiActions"], "optionalPlugins": [], "extraPublicDirs": ["public/todo", "public/hello_world", "public/todo/todo_ref_embeddable"] } diff --git a/examples/embeddable_examples/public/book/book_component.tsx b/examples/embeddable_examples/public/book/book_component.tsx new file mode 100644 index 0000000000000..064e13c131a0a --- /dev/null +++ b/examples/embeddable_examples/public/book/book_component.tsx @@ -0,0 +1,90 @@ +/* + * 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 { EuiFlexItem, EuiFlexGroup, EuiIcon } from '@elastic/eui'; + +import { EuiText } from '@elastic/eui'; +import { EuiFlexGrid } from '@elastic/eui'; +import { withEmbeddableSubscription } from '../../../../src/plugins/embeddable/public'; +import { BookEmbeddableInput, BookEmbeddableOutput, BookEmbeddable } from './book_embeddable'; + +interface Props { + input: BookEmbeddableInput; + output: BookEmbeddableOutput; + embeddable: BookEmbeddable; +} + +function wrapSearchTerms(task?: string, search?: string) { + if (!search || !task) return task; + const parts = task.split(new RegExp(`(${search})`, 'g')); + return parts.map((part, i) => + part === search ? ( + + {part} + + ) : ( + part + ) + ); +} + +export function BookEmbeddableComponentInner({ input: { search }, output: { attributes } }: Props) { + const title = attributes?.title; + const author = attributes?.author; + const readIt = attributes?.readIt; + + return ( + + + + {title ? ( + + +

{wrapSearchTerms(title, search)},

+
+
+ ) : null} + {author ? ( + + +
-{wrapSearchTerms(author, search)}
+
+
+ ) : null} + {readIt ? ( + + + + ) : ( + + + + )} +
+
+
+ ); +} + +export const BookEmbeddableComponent = withEmbeddableSubscription< + BookEmbeddableInput, + BookEmbeddableOutput, + BookEmbeddable, + {} +>(BookEmbeddableComponentInner); diff --git a/examples/embeddable_examples/public/book/book_embeddable.tsx b/examples/embeddable_examples/public/book/book_embeddable.tsx new file mode 100644 index 0000000000000..d49bd3280d97d --- /dev/null +++ b/examples/embeddable_examples/public/book/book_embeddable.tsx @@ -0,0 +1,123 @@ +/* + * 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 ReactDOM from 'react-dom'; +import { Subscription } from 'rxjs'; +import { + Embeddable, + EmbeddableInput, + IContainer, + EmbeddableOutput, + SavedObjectEmbeddableInput, + AttributeService, +} from '../../../../src/plugins/embeddable/public'; +import { BookSavedObjectAttributes } from '../../common'; +import { BookEmbeddableComponent } from './book_component'; + +export const BOOK_EMBEDDABLE = 'book'; +export type BookEmbeddableInput = BookByValueInput | BookByReferenceInput; +export interface BookEmbeddableOutput extends EmbeddableOutput { + hasMatch: boolean; + attributes: BookSavedObjectAttributes; +} + +interface BookInheritedInput extends EmbeddableInput { + search?: string; +} + +export type BookByValueInput = { attributes: BookSavedObjectAttributes } & BookInheritedInput; +export type BookByReferenceInput = SavedObjectEmbeddableInput & BookInheritedInput; + +/** + * Returns whether any attributes contain the search string. If search is empty, true is returned. If + * there are no savedAttributes, false is returned. + * @param search - the search string + * @param savedAttributes - the saved object attributes for the saved object with id `input.savedObjectId` + */ +function getHasMatch(search?: string, savedAttributes?: BookSavedObjectAttributes): boolean { + if (!search) return true; + if (!savedAttributes) return false; + return Boolean( + (savedAttributes.author && savedAttributes.author.match(search)) || + (savedAttributes.title && savedAttributes.title.match(search)) + ); +} + +export class BookEmbeddable extends Embeddable { + public readonly type = BOOK_EMBEDDABLE; + private subscription: Subscription; + private node?: HTMLElement; + private savedObjectId?: string; + private attributes?: BookSavedObjectAttributes; + + constructor( + initialInput: BookEmbeddableInput, + private attributeService: AttributeService< + BookSavedObjectAttributes, + BookByValueInput, + BookByReferenceInput + >, + { + parent, + }: { + parent?: IContainer; + } + ) { + super(initialInput, {} as BookEmbeddableOutput, parent); + + this.subscription = this.getInput$().subscribe(async () => { + const savedObjectId = (this.getInput() as BookByReferenceInput).savedObjectId; + const attributes = (this.getInput() as BookByValueInput).attributes; + if (this.attributes !== attributes || this.savedObjectId !== savedObjectId) { + this.savedObjectId = savedObjectId; + this.reload(); + } else { + this.updateOutput({ + attributes: this.attributes, + hasMatch: getHasMatch(this.input.search, this.attributes), + }); + } + }); + } + + public render(node: HTMLElement) { + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + this.node = node; + ReactDOM.render(, node); + } + + public async reload() { + this.attributes = await this.attributeService.unwrapAttributes(this.input); + + this.updateOutput({ + attributes: this.attributes, + hasMatch: getHasMatch(this.input.search, this.attributes), + }); + } + + public destroy() { + super.destroy(); + this.subscription.unsubscribe(); + 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 new file mode 100644 index 0000000000000..f4a32fb498a2d --- /dev/null +++ b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx @@ -0,0 +1,127 @@ +/* + * 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 { 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 { + BookEmbeddable, + BOOK_EMBEDDABLE, + BookEmbeddableInput, + BookEmbeddableOutput, + BookByValueInput, + BookByReferenceInput, +} from './book_embeddable'; +import { CreateEditBookComponent } from './create_edit_book_component'; +import { OverlayStart } from '../../../../src/core/public'; + +interface StartServices { + getAttributeService: EmbeddableStart['getAttributeService']; + openModal: OverlayStart['openModal']; +} + +export type BookEmbeddableFactory = EmbeddableFactory< + BookEmbeddableInput, + BookEmbeddableOutput, + BookEmbeddable, + BookSavedObjectAttributes +>; + +export class BookEmbeddableFactoryDefinition + implements + EmbeddableFactoryDefinition< + BookEmbeddableInput, + BookEmbeddableOutput, + BookEmbeddable, + BookSavedObjectAttributes + > { + public readonly type = BOOK_EMBEDDABLE; + public savedObjectMetaData = { + name: 'Book', + includeFields: ['title', 'author', 'readIt'], + type: BOOK_SAVED_OBJECT, + getIconForSavedObject: () => 'pencil', + }; + + private attributeService?: AttributeService< + BookSavedObjectAttributes, + BookByValueInput, + BookByReferenceInput + >; + + constructor(private getStartServices: () => Promise) {} + + public async isEditable() { + return true; + } + + public async create(input: BookEmbeddableInput, parent?: IContainer) { + return new BookEmbeddable(input, await this.getAttributeService(), { + parent, + }); + } + + public getDisplayName() { + return i18n.translate('embeddableExamples.book.displayName', { + defaultMessage: 'Book', + }); + } + + public async getExplicitInput(): Promise> { + const { openModal } = await this.getStartServices(); + return new Promise>((resolve) => { + const onSave = async (attributes: BookSavedObjectAttributes, useRefType: boolean) => { + const wrappedAttributes = (await this.getAttributeService()).wrapAttributes( + attributes, + useRefType + ); + resolve(wrappedAttributes); + }; + const overlay = openModal( + toMountPoint( + { + onSave(attributes, useRefType); + overlay.close(); + }} + /> + ) + ); + }); + } + + private async getAttributeService() { + if (!this.attributeService) { + this.attributeService = await (await this.getStartServices()).getAttributeService< + BookSavedObjectAttributes, + BookByValueInput, + BookByReferenceInput + >(this.type); + } + return this.attributeService; + } +} diff --git a/examples/embeddable_examples/public/book/create_edit_book_component.tsx b/examples/embeddable_examples/public/book/create_edit_book_component.tsx new file mode 100644 index 0000000000000..7e2d3cb9d88ab --- /dev/null +++ b/examples/embeddable_examples/public/book/create_edit_book_component.tsx @@ -0,0 +1,88 @@ +/* + * 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, { useState } from 'react'; +import { EuiModalBody, EuiCheckbox } from '@elastic/eui'; +import { EuiFieldText } from '@elastic/eui'; +import { EuiButton } from '@elastic/eui'; +import { EuiModalFooter } from '@elastic/eui'; +import { EuiModalHeader } from '@elastic/eui'; +import { EuiFormRow } from '@elastic/eui'; +import { BookSavedObjectAttributes } from '../../common'; + +export function CreateEditBookComponent({ + savedObjectId, + attributes, + onSave, +}: { + savedObjectId?: string; + attributes?: BookSavedObjectAttributes; + onSave: (attributes: BookSavedObjectAttributes, useRefType: boolean) => void; +}) { + const [title, setTitle] = useState(attributes?.title ?? ''); + const [author, setAuthor] = useState(attributes?.author ?? ''); + const [readIt, setReadIt] = useState(attributes?.readIt ?? false); + return ( + + +

{`${savedObjectId ? 'Create new ' : 'Edit '}`}

+
+ + + setTitle(e.target.value)} + /> + + + setAuthor(e.target.value)} + /> + + + setReadIt(event.target.checked)} + /> + + + + onSave({ title, author, readIt }, false)} + > + {savedObjectId ? 'Unlink from library item' : 'Save and Return'} + + onSave({ title, author, readIt }, true)} + > + {savedObjectId ? 'Update library item' : 'Save to library'} + + +
+ ); +} diff --git a/examples/embeddable_examples/public/book/edit_book_action.tsx b/examples/embeddable_examples/public/book/edit_book_action.tsx new file mode 100644 index 0000000000000..222f70e0be60f --- /dev/null +++ b/examples/embeddable_examples/public/book/edit_book_action.tsx @@ -0,0 +1,93 @@ +/* + * 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 { OverlayStart } from 'kibana/public'; +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 { + BookEmbeddable, + BOOK_EMBEDDABLE, + BookByReferenceInput, + BookByValueInput, +} from './book_embeddable'; +import { CreateEditBookComponent } from './create_edit_book_component'; + +interface StartServices { + openModal: OverlayStart['openModal']; + getAttributeService: EmbeddableStart['getAttributeService']; +} + +interface ActionContext { + embeddable: BookEmbeddable; +} + +export const ACTION_EDIT_BOOK = 'ACTION_EDIT_BOOK'; + +export const createEditBookAction = (getStartServices: () => Promise) => + createAction({ + getDisplayName: () => + i18n.translate('embeddableExamples.book.edit', { defaultMessage: 'Edit Book' }), + type: ACTION_EDIT_BOOK, + order: 100, + getIconType: () => 'documents', + isCompatible: async ({ embeddable }: ActionContext) => { + return ( + embeddable.type === BOOK_EMBEDDABLE && embeddable.getInput().viewMode === ViewMode.EDIT + ); + }, + execute: async ({ embeddable }: ActionContext) => { + const { openModal, getAttributeService } = await getStartServices(); + const attributeService = getAttributeService< + BookSavedObjectAttributes, + BookByValueInput, + BookByReferenceInput + >(BOOK_SAVED_OBJECT); + const onSave = async (attributes: BookSavedObjectAttributes, useRefType: boolean) => { + const newInput = await attributeService.wrapAttributes(attributes, useRefType, embeddable); + if (!useRefType && (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId) { + // Remove the savedObejctId when un-linking + newInput.savedObjectId = null; + } + embeddable.updateInput(newInput); + if (useRefType) { + // Ensures that any duplicate embeddables also register the changes. This mirrors the behavior of going back and forth between apps + embeddable.getRoot().reload(); + } + }; + const overlay = openModal( + toMountPoint( + { + overlay.close(); + onSave(attributes, useRefType); + }} + /> + ) + ); + }, + }); diff --git a/examples/embeddable_examples/public/book/index.ts b/examples/embeddable_examples/public/book/index.ts new file mode 100644 index 0000000000000..46f44926e2152 --- /dev/null +++ b/examples/embeddable_examples/public/book/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 * from './book_embeddable'; +export * from './book_embeddable_factory'; diff --git a/examples/embeddable_examples/public/create_sample_data.ts b/examples/embeddable_examples/public/create_sample_data.ts index bd5ade18aa91e..d598c32a182fe 100644 --- a/examples/embeddable_examples/public/create_sample_data.ts +++ b/examples/embeddable_examples/public/create_sample_data.ts @@ -18,9 +18,9 @@ */ import { SavedObjectsClientContract } from 'kibana/public'; -import { TodoSavedObjectAttributes } from '../common'; +import { TodoSavedObjectAttributes, BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../common'; -export async function createSampleData(client: SavedObjectsClientContract) { +export async function createSampleData(client: SavedObjectsClientContract, overwrite = true) { await client.create( 'todo', { @@ -30,7 +30,20 @@ export async function createSampleData(client: SavedObjectsClientContract) { }, { id: 'sample-todo-saved-object', - overwrite: true, + overwrite, + } + ); + + await client.create( + BOOK_SAVED_OBJECT, + { + title: 'Pillars of the Earth', + author: 'Ken Follett', + readIt: true, + }, + { + id: 'sample-book-saved-object', + overwrite, } ); } diff --git a/examples/embeddable_examples/public/index.ts b/examples/embeddable_examples/public/index.ts index ec007f7c626f0..86f50f2b6e114 100644 --- a/examples/embeddable_examples/public/index.ts +++ b/examples/embeddable_examples/public/index.ts @@ -26,6 +26,8 @@ export { export { ListContainer, LIST_CONTAINER, ListContainerFactory } from './list_container'; export { TODO_EMBEDDABLE, TodoEmbeddableFactory } from './todo'; +export { BOOK_EMBEDDABLE } from './book'; + import { EmbeddableExamplesPlugin } from './plugin'; export { diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index d65ca1e8e7e8d..95f4f5b41e198 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -17,14 +17,19 @@ * under the License. */ -import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public'; -import { CoreSetup, CoreStart, Plugin } from '../../../src/core/public'; import { + EmbeddableSetup, + EmbeddableStart, + CONTEXT_MENU_TRIGGER, +} from '../../../src/plugins/embeddable/public'; +import { Plugin, CoreSetup, CoreStart } from '../../../src/core/public'; +import { + HelloWorldEmbeddableFactory, HELLO_WORLD_EMBEDDABLE, HelloWorldEmbeddableFactoryDefinition, - HelloWorldEmbeddableFactory, } from './hello_world'; import { TODO_EMBEDDABLE, TodoEmbeddableFactory, TodoEmbeddableFactoryDefinition } from './todo'; + import { MULTI_TASK_TODO_EMBEDDABLE, MultiTaskTodoEmbeddableFactory, @@ -46,9 +51,17 @@ import { TodoRefEmbeddableFactory, TodoRefEmbeddableFactoryDefinition, } from './todo/todo_ref_embeddable_factory'; +import { ACTION_EDIT_BOOK, createEditBookAction } from './book/edit_book_action'; +import { BookEmbeddable, BOOK_EMBEDDABLE } from './book/book_embeddable'; +import { + BookEmbeddableFactory, + BookEmbeddableFactoryDefinition, +} from './book/book_embeddable_factory'; +import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; export interface EmbeddableExamplesSetupDependencies { embeddable: EmbeddableSetup; + uiActions: UiActionsStart; } export interface EmbeddableExamplesStartDependencies { @@ -62,6 +75,7 @@ interface ExampleEmbeddableFactories { getListContainerEmbeddableFactory: () => ListContainerFactory; getTodoEmbeddableFactory: () => TodoEmbeddableFactory; getTodoRefEmbeddableFactory: () => TodoRefEmbeddableFactory; + getBookEmbeddableFactory: () => BookEmbeddableFactory; } export interface EmbeddableExamplesStart { @@ -69,6 +83,12 @@ export interface EmbeddableExamplesStart { factories: ExampleEmbeddableFactories; } +declare module '../../../src/plugins/ui_actions/public' { + export interface ActionContextMapping { + [ACTION_EDIT_BOOK]: { embeddable: BookEmbeddable }; + } +} + export class EmbeddableExamplesPlugin implements Plugin< @@ -121,6 +141,20 @@ export class EmbeddableExamplesPlugin getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory, })) ); + this.exampleEmbeddableFactories.getBookEmbeddableFactory = deps.embeddable.registerEmbeddableFactory( + BOOK_EMBEDDABLE, + new BookEmbeddableFactoryDefinition(async () => ({ + getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService, + openModal: (await core.getStartServices())[0].overlays.openModal, + })) + ); + + const editBookAction = createEditBookAction(async () => ({ + getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService, + openModal: (await core.getStartServices())[0].overlays.openModal, + })); + deps.uiActions.registerAction(editBookAction); + deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, editBookAction.id); } public start( diff --git a/examples/embeddable_examples/server/book_saved_object.ts b/examples/embeddable_examples/server/book_saved_object.ts new file mode 100644 index 0000000000000..f0aca57f7925f --- /dev/null +++ b/examples/embeddable_examples/server/book_saved_object.ts @@ -0,0 +1,40 @@ +/* + * 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 { SavedObjectsType } from 'kibana/server'; + +export const bookSavedObject: SavedObjectsType = { + name: 'book', + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + title: { + type: 'keyword', + }, + author: { + type: 'keyword', + }, + readIt: { + type: 'boolean', + }, + }, + }, + migrations: {}, +}; diff --git a/examples/embeddable_examples/server/plugin.ts b/examples/embeddable_examples/server/plugin.ts index d956b834d0d3c..1308ac9e0fc5e 100644 --- a/examples/embeddable_examples/server/plugin.ts +++ b/examples/embeddable_examples/server/plugin.ts @@ -19,10 +19,12 @@ import { Plugin, CoreSetup, CoreStart } from 'kibana/server'; import { todoSavedObject } from './todo_saved_object'; +import { bookSavedObject } from './book_saved_object'; export class EmbeddableExamplesPlugin implements Plugin { public setup(core: CoreSetup) { core.savedObjects.registerType(todoSavedObject); + core.savedObjects.registerType(bookSavedObject); } public start(core: CoreStart) {} diff --git a/examples/embeddable_explorer/public/embeddable_panel_example.tsx b/examples/embeddable_explorer/public/embeddable_panel_example.tsx index b2807f9a4c346..ca9675bb7f5a1 100644 --- a/examples/embeddable_explorer/public/embeddable_panel_example.tsx +++ b/examples/embeddable_explorer/public/embeddable_panel_example.tsx @@ -33,6 +33,7 @@ import { EmbeddableStart, IEmbeddable } from '../../../src/plugins/embeddable/pu import { HELLO_WORLD_EMBEDDABLE, TODO_EMBEDDABLE, + BOOK_EMBEDDABLE, MULTI_TASK_TODO_EMBEDDABLE, SearchableListContainerFactory, } from '../../embeddable_examples/public'; @@ -72,6 +73,35 @@ export function EmbeddablePanelExample({ embeddableServices, searchListContainer tasks: ['Go to school', 'Watch planet earth', 'Read the encyclopedia'], }, }, + '4': { + type: BOOK_EMBEDDABLE, + explicitInput: { + id: '4', + savedObjectId: 'sample-book-saved-object', + }, + }, + '5': { + type: BOOK_EMBEDDABLE, + explicitInput: { + id: '5', + attributes: { + title: 'The Sympathizer', + author: 'Viet Thanh Nguyen', + readIt: true, + }, + }, + }, + '6': { + type: BOOK_EMBEDDABLE, + explicitInput: { + id: '6', + attributes: { + title: 'The Hobbit', + author: 'J.R.R. Tolkien', + readIt: false, + }, + }, + }, }, }; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 6960550b59d1c..fafbdda148de8 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -28,6 +28,7 @@ export { ACTION_EDIT_PANEL, Adapters, AddPanelAction, + AttributeService, 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 new file mode 100644 index 0000000000000..a33f592350d9a --- /dev/null +++ b/src/plugins/embeddable/public/lib/embeddables/attribute_service.ts @@ -0,0 +1,68 @@ +/* + * 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 { + if (isSavedObjectEmbeddableInput(input)) { + const savedObject: SimpleSavedObject = 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> { + 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 5bab5ac27f3cc..06cb6e322acf3 100644 --- a/src/plugins/embeddable/public/lib/embeddables/index.ts +++ b/src/plugins/embeddable/public/lib/embeddables/index.ts @@ -25,4 +25,5 @@ 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/embeddables/saved_object_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts index 6ca1800b16de4..5f093c55e94e4 100644 --- a/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts @@ -26,5 +26,5 @@ export interface SavedObjectEmbeddableInput extends EmbeddableInput { export function isSavedObjectEmbeddableInput( input: EmbeddableInput | SavedObjectEmbeddableInput ): input is SavedObjectEmbeddableInput { - return (input as SavedObjectEmbeddableInput).savedObjectId !== undefined; + return Boolean((input as SavedObjectEmbeddableInput).savedObjectId); } diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx index efd0ccdc4553d..48e5483124704 100644 --- a/src/plugins/embeddable/public/mocks.tsx +++ b/src/plugins/embeddable/public/mocks.tsx @@ -99,6 +99,7 @@ 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 03bb4a4779267..508c82c4247ed 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -43,11 +43,13 @@ import { defaultEmbeddableFactoryProvider, IEmbeddable, EmbeddablePanel, + SavedObjectEmbeddableInput, ChartActionContext, isRangeSelectTriggerContext, isValueClickTriggerContext, } 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 { @@ -82,6 +84,13 @@ 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. @@ -206,6 +215,7 @@ export class EmbeddablePublicPlugin implements Plugin new AttributeService(type, core.savedObjects.client), filtersFromContext, filtersAndTimeRangeFromContext, getStateTransfer: (history?: ScopedHistory) => {