From fcd2ccf71ce2c8feac3c0093e1792ce192d8653d Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Tue, 10 Mar 2020 16:00:22 -0400 Subject: [PATCH] wip Add embeddable via saved object example --- examples/embeddable_examples/common/index.ts | 20 ++ examples/embeddable_examples/common/types.ts | 26 +++ examples/embeddable_examples/kibana.json | 4 +- .../public/create_sample_data.ts | 36 +++ examples/embeddable_examples/public/index.ts | 22 +- .../list_container/embeddable_list_item.tsx | 52 ++++- .../public/list_container/index.ts | 4 +- .../public/list_container/list_container.tsx | 16 +- .../list_container_component.tsx | 21 +- .../list_container/list_container_factory.ts | 16 +- .../multi_task_todo_component.tsx | 8 +- examples/embeddable_examples/public/plugin.ts | 110 +++++++-- .../searchable_list_container.tsx | 15 +- .../searchable_list_container_component.tsx | 15 +- .../searchable_list_container_factory.ts | 16 +- .../public/todo/todo_component.tsx | 11 +- .../create_edit_todo_component.tsx | 83 +++++++ .../todo_saved_object/edit_todo_action.tsx | 83 +++++++ .../public/todo_saved_object/index.ts | 22 ++ .../todo_saved_object/todo_so_component.tsx | 93 ++++++++ .../todo_saved_object/todo_so_embeddable.tsx | 145 ++++++++++++ .../todo_so_embeddable_factory.tsx | 157 +++++++++++++ examples/embeddable_examples/server/index.ts | 24 ++ examples/embeddable_examples/server/plugin.ts | 31 +++ .../server/todo_saved_object.ts | 40 ++++ examples/embeddable_examples/tsconfig.json | 1 + examples/embeddable_explorer/public/app.tsx | 21 ++ .../public/embeddable_panel_example.tsx | 2 + .../embeddable_explorer/public/plugin.tsx | 3 + .../saved_object_embeddable_example.tsx | 221 ++++++++++++++++++ .../public/todo_embeddable_example.tsx | 3 +- .../np_ready/dashboard_app_controller.tsx | 3 +- ...embeddable_saved_object_converters.test.ts | 10 +- .../lib/embeddable_saved_object_converters.ts | 8 +- .../public/actions/replace_panel_flyout.tsx | 11 +- src/plugins/dashboard/public/bwc/types.ts | 5 + .../dashboard/public/embeddable/types.ts | 6 +- src/plugins/dashboard/public/plugin.tsx | 1 + .../saved_dashboards/saved_dashboards.ts | 6 +- .../public/saved_dashboards/types.ts | 131 +++++++++++ src/plugins/embeddable/public/bootstrap.ts | 6 +- src/plugins/embeddable/public/index.ts | 3 + .../public/lib/containers/container.ts | 39 +--- .../public/lib/containers/i_container.ts | 15 +- .../public/lib/embeddables/embeddable.tsx | 12 +- .../public/lib/embeddables/i_embeddable.ts | 8 + .../public/lib/embeddables/index.ts | 2 + .../embeddables/saved_object_embeddable.ts | 35 +++ .../saved_object_embeddable_factory.ts | 52 +++++ .../lib/embeddables/with_subscription.tsx | 12 +- .../public/lib/panel/embeddable_panel.tsx | 24 +- .../add_panel/add_panel_flyout.tsx | 28 ++- src/plugins/embeddable/public/plugin.ts | 14 +- .../embeddable/public/tests/container.test.ts | 6 +- .../public/service/ui_actions_service.ts | 2 +- test/examples/embeddables/adding_children.ts | 12 + test/examples/embeddables/index.ts | 1 + .../embeddables/saved_object_embeddable.ts | 80 +++++++ .../np_ready/public/app/dashboard_input.ts | 8 +- 59 files changed, 1681 insertions(+), 180 deletions(-) create mode 100644 examples/embeddable_examples/common/index.ts create mode 100644 examples/embeddable_examples/common/types.ts create mode 100644 examples/embeddable_examples/public/create_sample_data.ts create mode 100644 examples/embeddable_examples/public/todo_saved_object/create_edit_todo_component.tsx create mode 100644 examples/embeddable_examples/public/todo_saved_object/edit_todo_action.tsx create mode 100644 examples/embeddable_examples/public/todo_saved_object/index.ts create mode 100644 examples/embeddable_examples/public/todo_saved_object/todo_so_component.tsx create mode 100644 examples/embeddable_examples/public/todo_saved_object/todo_so_embeddable.tsx create mode 100644 examples/embeddable_examples/public/todo_saved_object/todo_so_embeddable_factory.tsx create mode 100644 examples/embeddable_examples/server/index.ts create mode 100644 examples/embeddable_examples/server/plugin.ts create mode 100644 examples/embeddable_examples/server/todo_saved_object.ts create mode 100644 examples/embeddable_explorer/public/saved_object_embeddable_example.tsx create mode 100644 src/plugins/dashboard/public/saved_dashboards/types.ts create mode 100644 src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts create mode 100644 src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable_factory.ts create mode 100644 test/examples/embeddables/saved_object_embeddable.ts diff --git a/examples/embeddable_examples/common/index.ts b/examples/embeddable_examples/common/index.ts new file mode 100644 index 0000000000000..0f1d72af49314 --- /dev/null +++ b/examples/embeddable_examples/common/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 { TodoSavedObjectAttributes } from './types'; diff --git a/examples/embeddable_examples/common/types.ts b/examples/embeddable_examples/common/types.ts new file mode 100644 index 0000000000000..22fddd900e646 --- /dev/null +++ b/examples/embeddable_examples/common/types.ts @@ -0,0 +1,26 @@ +/* + * 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 'kibana/public'; + +export interface TodoSavedObjectAttributes extends SavedObjectAttributes { + task: string; + icon?: string; + title?: string; +} diff --git a/examples/embeddable_examples/kibana.json b/examples/embeddable_examples/kibana.json index c70bc7009ff51..c650d58efb883 100644 --- a/examples/embeddable_examples/kibana.json +++ b/examples/embeddable_examples/kibana.json @@ -3,8 +3,8 @@ "version": "0.0.1", "kibanaVersion": "kibana", "configPath": ["embeddable_examples"], - "server": false, + "server": true, "ui": true, - "requiredPlugins": ["embeddable"], + "requiredPlugins": ["embeddable", "uiActions", "inspector"], "optionalPlugins": [] } diff --git a/examples/embeddable_examples/public/create_sample_data.ts b/examples/embeddable_examples/public/create_sample_data.ts new file mode 100644 index 0000000000000..52185845d7bbe --- /dev/null +++ b/examples/embeddable_examples/public/create_sample_data.ts @@ -0,0 +1,36 @@ +/* + * 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 'kibana/public'; +import { TodoSavedObjectAttributes } from '../common'; + +export async function createSampleData(client: SavedObjectsClientContract, overwrite = true) { + await client.create( + 'todo', + { + task: 'Take the garbage out', + title: 'Garbage', + icon: 'trash', + }, + { + id: 'sample-todo-saved-object', + overwrite, + } + ); +} diff --git a/examples/embeddable_examples/public/index.ts b/examples/embeddable_examples/public/index.ts index 5fcd454b17a5c..c87d0e14ae522 100644 --- a/examples/embeddable_examples/public/index.ts +++ b/examples/embeddable_examples/public/index.ts @@ -17,7 +17,6 @@ * under the License. */ -import { PluginInitializer } from 'kibana/public'; export { HELLO_WORLD_EMBEDDABLE, HelloWorldEmbeddable, @@ -26,18 +25,15 @@ export { export { ListContainer, LIST_CONTAINER } from './list_container'; export { TODO_EMBEDDABLE } from './todo'; -import { - EmbeddableExamplesPlugin, - EmbeddableExamplesSetupDependencies, - EmbeddableExamplesStartDependencies, -} from './plugin'; +import { EmbeddableExamplesPlugin } from './plugin'; export { SearchableListContainer, SEARCHABLE_LIST_CONTAINER } from './searchable_list_container'; -export { MULTI_TASK_TODO_EMBEDDABLE } from './multi_task_todo'; +export { + TODO_SO_EMBEDDABLE, + TodoSoEmbeddable, + TodoSoEmbeddableInput, + TodoSoEmbeddableOutput, +} from './todo_saved_object'; -export const plugin: PluginInitializer< - void, - void, - EmbeddableExamplesSetupDependencies, - EmbeddableExamplesStartDependencies -> = () => new EmbeddableExamplesPlugin(); +export { MULTI_TASK_TODO_EMBEDDABLE } from './multi_task_todo'; +export const plugin = () => new EmbeddableExamplesPlugin(); diff --git a/examples/embeddable_examples/public/list_container/embeddable_list_item.tsx b/examples/embeddable_examples/public/list_container/embeddable_list_item.tsx index 2c80cef8a6364..ddf6d0df42b29 100644 --- a/examples/embeddable_examples/public/list_container/embeddable_list_item.tsx +++ b/examples/embeddable_examples/public/list_container/embeddable_list_item.tsx @@ -18,11 +18,27 @@ */ import React from 'react'; -import { EuiPanel, EuiLoadingSpinner, EuiFlexItem } from '@elastic/eui'; -import { IEmbeddable } from '../../../../src/plugins/embeddable/public'; +import { EuiLoadingSpinner, EuiFlexItem } from '@elastic/eui'; +import { CoreStart, IUiSettingsClient, SavedObjectsStart } from 'kibana/public'; +import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; +import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; +import { getSavedObjectFinder } from '../../../../src/plugins/saved_objects/public'; +import { + IEmbeddable, + EmbeddableStart, + EmbeddablePanel, +} from '../../../../src/plugins/embeddable/public'; interface Props { embeddable: IEmbeddable; + uiActionsApi: UiActionsStart; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; + overlays: CoreStart['overlays']; + notifications: CoreStart['notifications']; + inspector: InspectorStart; + savedObject: SavedObjectsStart; + uiSettingsClient: IUiSettingsClient; } export class EmbeddableListItem extends React.Component { @@ -49,15 +65,33 @@ export class EmbeddableListItem extends React.Component { } public render() { + const { + embeddable, + uiActionsApi, + getAllEmbeddableFactories, + getEmbeddableFactory, + savedObject, + uiSettingsClient, + notifications, + inspector, + overlays, + } = this.props; return ( - - {this.props.embeddable ? ( -
- ) : ( - - )} - + {embeddable ? ( + + ) : ( + + )} ); } diff --git a/examples/embeddable_examples/public/list_container/index.ts b/examples/embeddable_examples/public/list_container/index.ts index 581739f6b5458..76418ea88ea45 100644 --- a/examples/embeddable_examples/public/list_container/index.ts +++ b/examples/embeddable_examples/public/list_container/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { ListContainer, LIST_CONTAINER } from './list_container'; -export { ListContainerFactory } from './list_container_factory'; +export * from './list_container'; +export * from './list_container_factory'; diff --git a/examples/embeddable_examples/public/list_container/list_container.tsx b/examples/embeddable_examples/public/list_container/list_container.tsx index bbbd0d6e32304..913401a58ef88 100644 --- a/examples/embeddable_examples/public/list_container/list_container.tsx +++ b/examples/embeddable_examples/public/list_container/list_container.tsx @@ -18,12 +18,9 @@ */ import React from 'react'; import ReactDOM from 'react-dom'; -import { - Container, - ContainerInput, - EmbeddableStart, -} from '../../../../src/plugins/embeddable/public'; +import { Container, ContainerInput } from '../../../../src/plugins/embeddable/public'; import { ListContainerComponent } from './list_container_component'; +import { StartServices } from './list_container_factory'; export const LIST_CONTAINER = 'LIST_CONTAINER'; @@ -31,11 +28,8 @@ export class ListContainer extends Container<{}, ContainerInput> { public readonly type = LIST_CONTAINER; private node?: HTMLElement; - constructor( - input: ContainerInput, - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'] - ) { - super(input, { embeddableLoaded: {} }, getEmbeddableFactory); + constructor(input: ContainerInput, private services: StartServices) { + super(input, { embeddableLoaded: {} }, services.getEmbeddableFactory); } // This container has no input itself. @@ -48,7 +42,7 @@ export class ListContainer extends Container<{}, ContainerInput> { if (this.node) { ReactDOM.unmountComponentAtNode(this.node); } - ReactDOM.render(, node); + ReactDOM.render(, node); } public destroy() { diff --git a/examples/embeddable_examples/public/list_container/list_container_component.tsx b/examples/embeddable_examples/public/list_container/list_container_component.tsx index f6e04933ee897..124a62203a784 100644 --- a/examples/embeddable_examples/public/list_container/list_container_component.tsx +++ b/examples/embeddable_examples/public/list_container/list_container_component.tsx @@ -26,28 +26,34 @@ import { ContainerOutput, } from '../../../../src/plugins/embeddable/public'; import { EmbeddableListItem } from './embeddable_list_item'; +import { StartServices } from './list_container_factory'; interface Props { embeddable: IContainer; input: ContainerInput; output: ContainerOutput; + services: StartServices; } -function renderList(embeddable: IContainer, panels: ContainerInput['panels']) { +function renderList( + embeddable: IContainer, + panels: ContainerInput['panels'], + services: StartServices +) { let number = 0; const list = Object.values(panels).map(panel => { const child = embeddable.getChild(panel.explicitInput.id); number++; return ( - +

{number}

- +
@@ -61,7 +67,7 @@ export function ListContainerComponentInner(props: Props) {

{props.embeddable.getTitle()}

- {renderList(props.embeddable, props.input.panels)} + {renderList(props.embeddable, props.input.panels, props.services)}
); } @@ -71,4 +77,9 @@ export function ListContainerComponentInner(props: Props) { // anything on input or output state changes. If you don't want that to happen (for example // if you expect something on input or output state to change frequently that your react // component does not care about, then you should probably hook this up manually). -export const ListContainerComponent = withEmbeddableSubscription(ListContainerComponentInner); +export const ListContainerComponent = withEmbeddableSubscription< + ContainerInput, + ContainerOutput, + IContainer, + { services: StartServices } +>(ListContainerComponentInner); diff --git a/examples/embeddable_examples/public/list_container/list_container_factory.ts b/examples/embeddable_examples/public/list_container/list_container_factory.ts index 247cf48b41bde..5aa4eeae1c056 100644 --- a/examples/embeddable_examples/public/list_container/list_container_factory.ts +++ b/examples/embeddable_examples/public/list_container/list_container_factory.ts @@ -18,6 +18,9 @@ */ import { i18n } from '@kbn/i18n'; +import { UiActionsStart } from 'src/plugins/ui_actions/public'; +import { OverlayStart, CoreStart, SavedObjectsStart, IUiSettingsClient } from 'kibana/public'; +import { Start as InspectorStart } from 'src/plugins/inspector/public'; import { EmbeddableFactory, ContainerInput, @@ -25,8 +28,15 @@ import { } from '../../../../src/plugins/embeddable/public'; import { LIST_CONTAINER, ListContainer } from './list_container'; -interface StartServices { +export interface StartServices { + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + uiActionsApi: UiActionsStart; + overlays: OverlayStart; + notifications: CoreStart['notifications']; + inspector: InspectorStart; + savedObject: SavedObjectsStart; + uiSettingsClient: IUiSettingsClient; } export class ListContainerFactory extends EmbeddableFactory { @@ -42,8 +52,8 @@ export class ListContainerFactory extends EmbeddableFactory { } public async create(initialInput: ContainerInput) { - const { getEmbeddableFactory } = await this.getStartServices(); - return new ListContainer(initialInput, getEmbeddableFactory); + const services = await this.getStartServices(); + return new ListContainer(initialInput, services); } public getDisplayName() { diff --git a/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_component.tsx b/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_component.tsx index e33dfab0eaf4a..683016b66052e 100644 --- a/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_component.tsx +++ b/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_component.tsx @@ -89,6 +89,8 @@ export function MultiTaskTodoEmbeddableComponentInner({ ); } -export const MultiTaskTodoEmbeddableComponent = withEmbeddableSubscription( - MultiTaskTodoEmbeddableComponentInner -); +export const MultiTaskTodoEmbeddableComponent = withEmbeddableSubscription< + MultiTaskTodoInput, + MultiTaskTodoOutput, + MultiTaskTodoEmbeddable +>(MultiTaskTodoEmbeddableComponentInner); diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index 5c202d96ceb1a..403ed35b90a42 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -17,28 +17,69 @@ * under the License. */ -import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public'; +import { UiActionsStart } from 'src/plugins/ui_actions/public'; +import { Start as InspectorStart } from 'src/plugins/inspector/public'; +import { + EmbeddableSetup, + EmbeddableStart, + EmbeddableOutput, + CONTEXT_MENU_TRIGGER, +} from '../../../src/plugins/embeddable/public'; import { Plugin, CoreSetup, CoreStart } from '../../../src/core/public'; import { HelloWorldEmbeddableFactory, HELLO_WORLD_EMBEDDABLE } from './hello_world'; +import { + TODO_SO_EMBEDDABLE, + TodoSoEmbeddableFactory, + TodoSoEmbeddableInput, + createEditTodoAction, + TodoSoEmbeddable, + EDIT_TODO_ACTION, +} from './todo_saved_object'; import { TODO_EMBEDDABLE, TodoEmbeddableFactory, TodoInput, TodoOutput } from './todo'; -import { MULTI_TASK_TODO_EMBEDDABLE, MultiTaskTodoEmbeddableFactory } from './multi_task_todo'; + +import { + MULTI_TASK_TODO_EMBEDDABLE, + MultiTaskTodoEmbeddableFactory, + MultiTaskTodoInput, + MultiTaskTodoOutput, +} from './multi_task_todo'; import { SEARCHABLE_LIST_CONTAINER, SearchableListContainerFactory, } from './searchable_list_container'; import { LIST_CONTAINER, ListContainerFactory } from './list_container'; +import { createSampleData } from './create_sample_data'; +import { StartServices } from './list_container/list_container_factory'; export interface EmbeddableExamplesSetupDependencies { embeddable: EmbeddableSetup; + uiActions: UiActionsStart; } export interface EmbeddableExamplesStartDependencies { embeddable: EmbeddableStart; + uiActions: UiActionsStart; + inspector: InspectorStart; +} + +export interface EmbeddableExamplesStart { + createSampleData: (overwrite: boolean) => Promise; +} + +declare module '../../../src/plugins/ui_actions/public' { + export interface ActionContextMapping { + [EDIT_TODO_ACTION]: { embeddable: TodoSoEmbeddable }; + } } export class EmbeddableExamplesPlugin implements - Plugin { + Plugin< + void, + EmbeddableExamplesStart, + EmbeddableExamplesSetupDependencies, + EmbeddableExamplesStartDependencies + > { public setup( core: CoreSetup, deps: EmbeddableExamplesSetupDependencies @@ -48,37 +89,78 @@ export class EmbeddableExamplesPlugin new HelloWorldEmbeddableFactory() ); - deps.embeddable.registerEmbeddableFactory( + deps.embeddable.registerEmbeddableFactory( MULTI_TASK_TODO_EMBEDDABLE, new MultiTaskTodoEmbeddableFactory() ); - // These are registered in the start method because `getEmbeddableFactory ` - // is only available in start. We could reconsider this I think and make it - // available in both. + deps.embeddable.registerEmbeddableFactory( + TODO_EMBEDDABLE, + new TodoEmbeddableFactory(async () => ({ + openModal: (await core.getStartServices())[0].overlays.openModal, + })) + ); + deps.embeddable.registerEmbeddableFactory( SEARCHABLE_LIST_CONTAINER, new SearchableListContainerFactory(async () => ({ getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory, + getAllEmbeddableFactories: (await core.getStartServices())[1].embeddable + .getEmbeddableFactories, + uiActionsApi: (await core.getStartServices())[1].uiActions, + inspector: (await core.getStartServices())[1].inspector, + uiSettingsClient: (await core.getStartServices())[0].uiSettings, + notifications: (await core.getStartServices())[0].notifications, + overlays: (await core.getStartServices())[0].overlays, + savedObject: (await core.getStartServices())[0].savedObjects, })) ); deps.embeddable.registerEmbeddableFactory( LIST_CONTAINER, - new ListContainerFactory(async () => ({ - getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory, - })) + new ListContainerFactory( + async (): Promise => ({ + getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory, + getAllEmbeddableFactories: (await core.getStartServices())[1].embeddable + .getEmbeddableFactories, + uiActionsApi: (await core.getStartServices())[1].uiActions, + inspector: (await core.getStartServices())[1].inspector, + uiSettingsClient: (await core.getStartServices())[0].uiSettings, + notifications: (await core.getStartServices())[0].notifications, + overlays: (await core.getStartServices())[0].overlays, + savedObject: (await core.getStartServices())[0].savedObjects, + }) + ) ); - deps.embeddable.registerEmbeddableFactory( - TODO_EMBEDDABLE, - new TodoEmbeddableFactory(async () => ({ + /** + * This embeddable is a version of the Todo embeddable, but one that is backed, optionally, + * off a saved object. + */ + deps.embeddable.registerEmbeddableFactory( + TODO_SO_EMBEDDABLE, + new TodoSoEmbeddableFactory(async () => ({ openModal: (await core.getStartServices())[0].overlays.openModal, + savedObjectsClient: (await core.getStartServices())[0].savedObjects.client, + getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory, })) ); + + const editTodoAction = createEditTodoAction(async () => ({ + openModal: (await core.getStartServices())[0].overlays.openModal, + savedObjectsClient: (await core.getStartServices())[0].savedObjects.client, + getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory, + })); + deps.uiActions.registerAction(editTodoAction); + deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, editTodoAction); } - public start(core: CoreStart, deps: EmbeddableExamplesStartDependencies) {} + public start(core: CoreStart, deps: EmbeddableExamplesStartDependencies) { + return { + createSampleData: (overwrite: boolean) => + createSampleData(core.savedObjects.client, overwrite), + }; + } public stop() {} } diff --git a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx index 06462937c768d..67f58f63e92a2 100644 --- a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx +++ b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx @@ -21,10 +21,10 @@ import ReactDOM from 'react-dom'; import { Container, ContainerInput, - EmbeddableStart, EmbeddableInput, } from '../../../../src/plugins/embeddable/public'; import { SearchableListContainerComponent } from './searchable_list_container_component'; +import { StartServices } from './searchable_list_container_factory'; export const SEARCHABLE_LIST_CONTAINER = 'SEARCHABLE_LIST_CONTAINER'; @@ -40,11 +40,8 @@ export class SearchableListContainer extends Container, node); + ReactDOM.render( + , + node + ); } public destroy() { diff --git a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx index b79f86e2a0192..0fc2b05aa61b4 100644 --- a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx +++ b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx @@ -37,11 +37,13 @@ import { } from '../../../../src/plugins/embeddable/public'; import { EmbeddableListItem } from '../list_container/embeddable_list_item'; import { SearchableListContainer, SearchableContainerInput } from './searchable_list_container'; +import { StartServices } from './searchable_list_container_factory'; interface Props { embeddable: SearchableListContainer; input: SearchableContainerInput; output: ContainerOutput; + services: StartServices; } interface State { @@ -160,7 +162,7 @@ export class SearchableListContainerComponentInner extends Component - + - + @@ -183,6 +185,9 @@ export class SearchableListContainerComponentInner extends Component(SearchableListContainerComponentInner); diff --git a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_factory.ts b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_factory.ts index 79859cc015a80..bf7ea3ef2932f 100644 --- a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_factory.ts +++ b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_factory.ts @@ -18,6 +18,9 @@ */ import { i18n } from '@kbn/i18n'; +import { UiActionsStart } from 'src/plugins/ui_actions/public'; +import { OverlayStart, CoreStart, SavedObjectsStart, IUiSettingsClient } from 'kibana/public'; +import { Start as InspectorStart } from 'src/plugins/inspector/public'; import { EmbeddableFactory, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { SEARCHABLE_LIST_CONTAINER, @@ -25,8 +28,15 @@ import { SearchableContainerInput, } from './searchable_list_container'; -interface StartServices { +export interface StartServices { + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + uiActionsApi: UiActionsStart; + overlays: OverlayStart; + notifications: CoreStart['notifications']; + inspector: InspectorStart; + savedObject: SavedObjectsStart; + uiSettingsClient: IUiSettingsClient; } export class SearchableListContainerFactory extends EmbeddableFactory { @@ -42,8 +52,8 @@ export class SearchableListContainerFactory extends EmbeddableFactory { } public async create(initialInput: SearchableContainerInput) { - const { getEmbeddableFactory } = await this.getStartServices(); - return new SearchableListContainer(initialInput, getEmbeddableFactory); + const services = await this.getStartServices(); + return new SearchableListContainer(initialInput, services); } public getDisplayName() { diff --git a/examples/embeddable_examples/public/todo/todo_component.tsx b/examples/embeddable_examples/public/todo/todo_component.tsx index fbebfc98627b5..fa84c317f6026 100644 --- a/examples/embeddable_examples/public/todo/todo_component.tsx +++ b/examples/embeddable_examples/public/todo/todo_component.tsx @@ -27,7 +27,7 @@ import { withEmbeddableSubscription, EmbeddableOutput, } from '../../../../src/plugins/embeddable/public'; -import { TodoEmbeddable, TodoInput } from './todo_embeddable'; +import { TodoEmbeddable, TodoInput, TodoOutput } from './todo_embeddable'; interface Props { embeddable: TodoEmbeddable; @@ -51,7 +51,7 @@ function wrapSearchTerms(task: string, search?: string) { export function TodoEmbeddableComponentInner({ input: { icon, title, task, search } }: Props) { return ( - + {icon ? : } @@ -71,4 +71,9 @@ export function TodoEmbeddableComponentInner({ input: { icon, title, task, searc ); } -export const TodoEmbeddableComponent = withEmbeddableSubscription(TodoEmbeddableComponentInner); +export const TodoEmbeddableComponent = withEmbeddableSubscription< + TodoInput, + TodoOutput, + TodoEmbeddable, + {} +>(TodoEmbeddableComponentInner); diff --git a/examples/embeddable_examples/public/todo_saved_object/create_edit_todo_component.tsx b/examples/embeddable_examples/public/todo_saved_object/create_edit_todo_component.tsx new file mode 100644 index 0000000000000..29839dc2f0f80 --- /dev/null +++ b/examples/embeddable_examples/public/todo_saved_object/create_edit_todo_component.tsx @@ -0,0 +1,83 @@ +/* + * 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 } from '@elastic/eui'; +import { EuiFieldText } from '@elastic/eui'; +import { EuiButton } from '@elastic/eui'; +import { EuiModalFooter } from '@elastic/eui'; +import { TodoSavedObjectAttributes } from 'examples/embeddable_examples/common'; +import { EuiModalHeader } from '@elastic/eui'; +import { EuiFormRow } from '@elastic/eui'; + +export function CreateEditTodoComponent({ + savedObjectId, + attributes, + onSave, +}: { + savedObjectId?: string; + attributes?: TodoSavedObjectAttributes; + onSave: (attributes: TodoSavedObjectAttributes, saveToLibrary: boolean) => void; +}) { + const [task, setTask] = useState(attributes?.task ?? ''); + const [title, setTitle] = useState(attributes?.title ?? ''); + return ( + + +

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

+
+ + + setTitle(e.target.value)} + /> + + + + setTask(e.target.value)} + /> + + + + {savedObjectId === undefined ? ( + onSave({ task, title }, false)} + > + Save + + ) : null} + onSave({ task, title }, true)} + > + {savedObjectId ? 'Update library item' : 'Save to library'} + + +
+ ); +} diff --git a/examples/embeddable_examples/public/todo_saved_object/edit_todo_action.tsx b/examples/embeddable_examples/public/todo_saved_object/edit_todo_action.tsx new file mode 100644 index 0000000000000..328440a7fe104 --- /dev/null +++ b/examples/embeddable_examples/public/todo_saved_object/edit_todo_action.tsx @@ -0,0 +1,83 @@ +/* + * 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, SavedObjectsClientContract } from 'kibana/public'; +import { TodoSavedObjectAttributes } from 'examples/embeddable_examples/common'; +import { i18n } from '@kbn/i18n'; +import { createAction } from '../../../../src/plugins/ui_actions/public'; +import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; +import { ViewMode } from '../../../../src/plugins/embeddable/public'; +import { CreateEditTodoComponent } from './create_edit_todo_component'; +import { TodoSoEmbeddable, TODO_SO_EMBEDDABLE } from './todo_so_embeddable'; + +interface StartServices { + openModal: OverlayStart['openModal']; + savedObjectsClient: SavedObjectsClientContract; +} + +interface ActionContext { + embeddable: TodoSoEmbeddable; +} + +export const EDIT_TODO_ACTION = 'EDIT_TODO_ACTION'; + +export const createEditTodoAction = (getStartServices: () => Promise) => + createAction({ + getDisplayName: () => + i18n.translate('embeddableExamples.todo.edit', { defaultMessage: 'Edit' }), + type: EDIT_TODO_ACTION, + isCompatible: async ({ embeddable }: ActionContext) => { + return ( + embeddable.type === TODO_SO_EMBEDDABLE && embeddable.getInput().viewMode === ViewMode.EDIT + ); + }, + execute: async ({ embeddable }: ActionContext) => { + const { openModal, savedObjectsClient } = await getStartServices(); + const onSave = async (attributes: TodoSavedObjectAttributes, includeInLibrary: boolean) => { + if (includeInLibrary) { + if (embeddable.getInput().savedObjectId) { + await savedObjectsClient.update( + 'todo', + embeddable.getInput().savedObjectId!, + attributes + ); + embeddable.updateInput({ attributes: undefined }); + embeddable.reload(); + } else { + const savedItem = await savedObjectsClient.create('todo', attributes); + embeddable.updateInput({ savedObjectId: savedItem.id }); + } + } else { + embeddable.updateInput({ attributes }); + } + }; + const overlay = openModal( + toMountPoint( + { + overlay.close(); + onSave(attributes, includeInLibrary); + }} + /> + ) + ); + }, + }); diff --git a/examples/embeddable_examples/public/todo_saved_object/index.ts b/examples/embeddable_examples/public/todo_saved_object/index.ts new file mode 100644 index 0000000000000..5911e92b8f857 --- /dev/null +++ b/examples/embeddable_examples/public/todo_saved_object/index.ts @@ -0,0 +1,22 @@ +/* + * 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 './todo_so_embeddable'; +export * from './todo_so_embeddable_factory'; +export * from './edit_todo_action'; diff --git a/examples/embeddable_examples/public/todo_saved_object/todo_so_component.tsx b/examples/embeddable_examples/public/todo_saved_object/todo_so_component.tsx new file mode 100644 index 0000000000000..18f75b630aab4 --- /dev/null +++ b/examples/embeddable_examples/public/todo_saved_object/todo_so_component.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 { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; + +import { EuiText } from '@elastic/eui'; +import { EuiAvatar } from '@elastic/eui'; +import { EuiIcon } from '@elastic/eui'; +import { EuiFlexGrid } from '@elastic/eui'; +import { + withEmbeddableSubscription, + EmbeddableOutput, +} from '../../../../src/plugins/embeddable/public'; +import { + TodoSoEmbeddable, + TodoSoEmbeddableInput, + getTask, + getTitle, + getIcon, + TodoSoEmbeddableOutput, +} from './todo_so_embeddable'; + +interface Props { + embeddable: TodoSoEmbeddable; + input: TodoSoEmbeddableInput; + output: EmbeddableOutput; +} + +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 TodoSoEmbeddableComponentInner({ input: { search }, embeddable }: Props) { + const task = getTask(embeddable); + const title = getTitle(embeddable); + const icon = getIcon(embeddable); + return ( + + + {icon ? ( + + ) : ( + + )} + + + + + +

{wrapSearchTerms(title || '', search)}

+
+
+ + {wrapSearchTerms(task, search)} + +
+
+
+ ); +} + +export const TodoSoEmbeddableComponent = withEmbeddableSubscription< + TodoSoEmbeddableInput, + TodoSoEmbeddableOutput, + TodoSoEmbeddable, + {} +>(TodoSoEmbeddableComponentInner); diff --git a/examples/embeddable_examples/public/todo_saved_object/todo_so_embeddable.tsx b/examples/embeddable_examples/public/todo_saved_object/todo_so_embeddable.tsx new file mode 100644 index 0000000000000..4db6a3d93b27a --- /dev/null +++ b/examples/embeddable_examples/public/todo_saved_object/todo_so_embeddable.tsx @@ -0,0 +1,145 @@ +/* + * 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 * as Rx from 'rxjs'; +import { SavedObjectsClientContract } from 'kibana/public'; +import { TodoSavedObjectAttributes } from '../../common'; +import { + Embeddable, + IContainer, + EmbeddableOutput, + SavedObjectEmbeddableInput, +} from '../../../../src/plugins/embeddable/public'; +import { TodoSoEmbeddableComponent } from './todo_so_component'; + +// Notice this is not the same value as the 'todo' saved object type. Many of our +// cases in prod today use the same value, but this is unnecessary. +export const TODO_SO_EMBEDDABLE = 'TODO_SO_EMBEDDABLE'; + +export function getTitle(note: TodoSoEmbeddable): string | undefined { + const title = note.getInput().attributes?.title; + const savedTitle = note.getOutput().savedAttributes?.title; + + return title || savedTitle; +} + +export function getTask(note: TodoSoEmbeddable): string | undefined { + const unsavedTask = note.getInput().attributes?.task; + const savedTask = note.getOutput().savedAttributes?.task; + + return unsavedTask || savedTask; +} + +export function getIcon(note: TodoSoEmbeddable): string | undefined { + const icon = note.getInput().attributes?.icon; + const savedIcon = note.getOutput().savedAttributes?.icon; + + return icon || savedIcon; +} + +export interface TodoSoEmbeddableInput extends SavedObjectEmbeddableInput { + search?: string; + attributes?: TodoSavedObjectAttributes; +} + +export interface TodoSoEmbeddableOutput extends EmbeddableOutput { + hasMatch: boolean; + savedAttributes?: TodoSavedObjectAttributes; +} + +function getHasMatch(todo: TodoSoEmbeddable): boolean { + const task = getTask(todo); + const title = getTitle(todo); + const { search } = todo.getInput(); + if (!search) return true; + if (!task) return false; + return todo.getInput().search ? Boolean(task?.match(search) || title?.match(search)) : true; +} + +/** + * This is an example of an embeddable that can optionally be backed by a saved object. + */ + +export class TodoSoEmbeddable extends Embeddable { + public readonly type = TODO_SO_EMBEDDABLE; + private subscription: Subscription; + private node?: HTMLElement; + private savedObjectsClient: SavedObjectsClientContract; + private savedObjectId?: string; + + constructor( + initialInput: TodoSoEmbeddableInput, + { + parent, + savedObjectsClient, + }: { + parent?: IContainer; + savedObjectsClient: SavedObjectsClientContract; + } + ) { + super(initialInput, { hasMatch: false, defaultTitle: initialInput.attributes?.title }, parent); + this.savedObjectsClient = savedObjectsClient; + + this.subscription = Rx.merge(this.getOutput$(), this.getInput$()).subscribe(async () => { + const { savedObjectId } = this.getInput(); + if (this.savedObjectId !== savedObjectId) { + this.savedObjectId = savedObjectId; + if (savedObjectId !== undefined) { + this.reload(); + } + } + }); + } + + public render(node: HTMLElement) { + this.node = node; + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + ReactDOM.render(, node); + } + + public async reload() { + if (this.savedObjectId !== undefined) { + const savedObject = await this.savedObjectsClient.get( + 'todo', + this.savedObjectId + ); + this.updateOutput({ + hasMatch: getHasMatch(this), + savedAttributes: savedObject.attributes, + defaultTitle: savedObject.attributes.title, + title: this.input.hidePanelTitles ? '' : savedObject.attributes.title, + }); + if (this.node) { + this.render(this.node); + } + } + } + + public destroy() { + super.destroy(); + this.subscription.unsubscribe(); + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + } +} diff --git a/examples/embeddable_examples/public/todo_saved_object/todo_so_embeddable_factory.tsx b/examples/embeddable_examples/public/todo_saved_object/todo_so_embeddable_factory.tsx new file mode 100644 index 0000000000000..1661d3eb6df4d --- /dev/null +++ b/examples/embeddable_examples/public/todo_saved_object/todo_so_embeddable_factory.tsx @@ -0,0 +1,157 @@ +/* + * 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 { SavedObjectsClientContract, OverlayStart } from 'kibana/public'; +import { TodoSavedObjectAttributes } from '../../common'; +import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; +import { + IContainer, + SavedObjectEmbeddableFactory, + EmbeddableStart, + ErrorEmbeddable, +} from '../../../../src/plugins/embeddable/public'; +import { + TodoSoEmbeddable, + TODO_SO_EMBEDDABLE, + TodoSoEmbeddableInput, + TodoSoEmbeddableOutput, +} from './todo_so_embeddable'; +import { CreateEditTodoComponent } from './create_edit_todo_component'; + +interface StartServices { + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + savedObjectsClient: SavedObjectsClientContract; + openModal: OverlayStart['openModal']; +} + +export class TodoSoEmbeddableFactory extends SavedObjectEmbeddableFactory< + TodoSoEmbeddableInput, + TodoSoEmbeddableOutput, + TodoSoEmbeddable, + TodoSavedObjectAttributes +> { + public readonly type = TODO_SO_EMBEDDABLE; + + constructor(private getStartServices: () => Promise) { + super({ + savedObjectMetaData: { + name: 'Todo', + includeFields: ['task'], + type: 'todo', + getIconForSavedObject: () => 'pencil', + }, + }); + } + + public async isEditable() { + return true; + } + + canSave = (todoEmbeddable: TodoSoEmbeddable) => { + const { attributes } = todoEmbeddable.getInput(); + const { savedAttributes } = todoEmbeddable.getOutput(); + + if (!savedAttributes && attributes && attributes.task) return true; + + if (savedAttributes && !_.isEqual(savedAttributes, attributes)) return true; + + return false; + }; + + save = async (todoEmbeddable: TodoSoEmbeddable) => { + const { savedObjectsClient } = await this.getStartServices(); + const { attributes, savedObjectId } = todoEmbeddable.getInput(); + if (!todoEmbeddable.getInput().savedObjectId) { + return savedObjectsClient.create(this.savedObjectMetaData.type, { + ...attributes, + }); + } else if (savedObjectId) { + return savedObjectsClient.update(this.savedObjectMetaData.type, savedObjectId, { + ...(todoEmbeddable.getOutput().savedAttributes ?? {}), + ...attributes, + }); + } + throw new Error('something went wrong'); + }; + + public createFromSavedObject = async ( + savedObjectId: string, + input: Partial & { id: string }, + parent?: IContainer + ): Promise => { + const { savedObjectsClient } = await this.getStartServices(); + const todoSavedObject = await savedObjectsClient.get( + 'todo', + savedObjectId + ); + return this.create({ ...input, savedObjectId, attributes: todoSavedObject.attributes }, parent); + }; + + public async create(initialInput: TodoSoEmbeddableInput, parent?: IContainer) { + const { savedObjectsClient } = await this.getStartServices(); + return new TodoSoEmbeddable(initialInput, { + parent, + savedObjectsClient, + }); + } + + public getDisplayName() { + return i18n.translate('embeddableExamples.todo.displayName', { + defaultMessage: 'Todo item (optionally backed by saved object)', + }); + } + + /** + * This function is used when dynamically creating a new embeddable to add to a + * container. Some input may be inherited from the container, but not all. This can be + * used to collect specific embeddable input that the container will not provide, like + * in this case, the task string. + */ + public async getExplicitInput(): Promise<{ + savedObjectId?: string; + attributes?: { task: string }; + }> { + const { openModal, savedObjectsClient } = await this.getStartServices(); + return new Promise<{ + savedObjectId?: string; + attributes?: { task: string }; + }>(resolve => { + const onSave = async (attributes: TodoSavedObjectAttributes, includeInLibrary: boolean) => { + if (includeInLibrary) { + const savedItem = await savedObjectsClient.create('todo', attributes); + resolve({ savedObjectId: savedItem.id }); + } else { + resolve({ attributes }); + } + }; + const overlay = openModal( + toMountPoint( + { + onSave(attributes, includeInLibrary); + overlay.close(); + }} + /> + ) + ); + }); + } +} diff --git a/examples/embeddable_examples/server/index.ts b/examples/embeddable_examples/server/index.ts new file mode 100644 index 0000000000000..9ddc3bc2cf715 --- /dev/null +++ b/examples/embeddable_examples/server/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { PluginInitializer } from 'kibana/server'; + +import { EmbeddableExamplesPlugin } from './plugin'; + +export const plugin: PluginInitializer = () => new EmbeddableExamplesPlugin(); diff --git a/examples/embeddable_examples/server/plugin.ts b/examples/embeddable_examples/server/plugin.ts new file mode 100644 index 0000000000000..d956b834d0d3c --- /dev/null +++ b/examples/embeddable_examples/server/plugin.ts @@ -0,0 +1,31 @@ +/* + * 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 { Plugin, CoreSetup, CoreStart } from 'kibana/server'; +import { todoSavedObject } from './todo_saved_object'; + +export class EmbeddableExamplesPlugin implements Plugin { + public setup(core: CoreSetup) { + core.savedObjects.registerType(todoSavedObject); + } + + public start(core: CoreStart) {} + + public stop() {} +} diff --git a/examples/embeddable_examples/server/todo_saved_object.ts b/examples/embeddable_examples/server/todo_saved_object.ts new file mode 100644 index 0000000000000..0f67c53cfa3e1 --- /dev/null +++ b/examples/embeddable_examples/server/todo_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 todoSavedObject: SavedObjectsType = { + name: 'todo', + hidden: false, + namespaceAgnostic: true, + mappings: { + properties: { + title: { + type: 'keyword', + }, + task: { + type: 'text', + }, + icon: { + type: 'keyword', + }, + }, + }, + migrations: {}, +}; diff --git a/examples/embeddable_examples/tsconfig.json b/examples/embeddable_examples/tsconfig.json index 091130487791b..7fa03739119b4 100644 --- a/examples/embeddable_examples/tsconfig.json +++ b/examples/embeddable_examples/tsconfig.json @@ -6,6 +6,7 @@ }, "include": [ "index.ts", + "common/**/*.ts", "public/**/*.ts", "public/**/*.tsx", "server/**/*.ts", diff --git a/examples/embeddable_explorer/public/app.tsx b/examples/embeddable_explorer/public/app.tsx index 9c8568454855d..9da3f441c49e4 100644 --- a/examples/embeddable_explorer/public/app.tsx +++ b/examples/embeddable_explorer/public/app.tsx @@ -23,6 +23,7 @@ import { BrowserRouter as Router, Route, withRouter, RouteComponentProps } from import { EuiPage, EuiPageSideBar, EuiSideNav } from '@elastic/eui'; +import { EmbeddableExamplesStart } from 'examples/embeddable_examples/public/plugin'; import { EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; import { Start as InspectorStartContract } from '../../../src/plugins/inspector/public'; @@ -38,6 +39,7 @@ import { HelloWorldEmbeddableExample } from './hello_world_embeddable_example'; import { TodoEmbeddableExample } from './todo_embeddable_example'; import { ListContainerExample } from './list_container_example'; import { EmbeddablePanelExample } from './embeddable_panel_example'; +import { SavedObjectEmbeddableExample } from './saved_object_embeddable_example'; interface PageDef { title: string; @@ -81,6 +83,7 @@ interface Props { inspector: InspectorStartContract; savedObject: SavedObjectsStart; uiSettingsClient: IUiSettingsClient; + createSampleData: EmbeddableExamplesStart['createSampleData']; } const EmbeddableExplorerApp = ({ @@ -93,6 +96,7 @@ const EmbeddableExplorerApp = ({ overlays, uiActionsApi, notifications, + createSampleData, }: Props) => { const pages: PageDef[] = [ { @@ -130,6 +134,23 @@ const EmbeddableExplorerApp = ({ /> ), }, + { + title: 'Embeddables backed by saved objects', + id: 'savedObjectSection', + component: ( + + ), + }, ]; const routes = pages.map((page, i) => ( diff --git a/examples/embeddable_explorer/public/embeddable_panel_example.tsx b/examples/embeddable_explorer/public/embeddable_panel_example.tsx index b26111bed7ff2..85bd597d5cee5 100644 --- a/examples/embeddable_explorer/public/embeddable_panel_example.tsx +++ b/examples/embeddable_explorer/public/embeddable_panel_example.tsx @@ -34,6 +34,7 @@ import { EmbeddablePanel, EmbeddableStart, IEmbeddable, + ViewMode, } from '../../../src/plugins/embeddable/public'; import { HELLO_WORLD_EMBEDDABLE, @@ -69,6 +70,7 @@ export function EmbeddablePanelExample({ const searchableInput = { id: '1', title: 'My searchable todo list', + viewMode: ViewMode.EDIT, panels: { '1': { type: HELLO_WORLD_EMBEDDABLE, diff --git a/examples/embeddable_explorer/public/plugin.tsx b/examples/embeddable_explorer/public/plugin.tsx index 7c75b108d9912..fe6387e9cb11e 100644 --- a/examples/embeddable_explorer/public/plugin.tsx +++ b/examples/embeddable_explorer/public/plugin.tsx @@ -18,6 +18,7 @@ */ import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; +import { EmbeddableExamplesStart } from 'examples/embeddable_examples/public/plugin'; import { UiActionsService } from '../../../src/plugins/ui_actions/public'; import { EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { Start as InspectorStart } from '../../../src/plugins/inspector/public'; @@ -26,6 +27,7 @@ interface StartDeps { uiActions: UiActionsService; embeddable: EmbeddableStart; inspector: InspectorStart; + embeddableExamples: EmbeddableExamplesStart; } export class EmbeddableExplorerPlugin implements Plugin { @@ -47,6 +49,7 @@ export class EmbeddableExplorerPlugin implements Plugin(undefined); + const [embeddable, setEmbeddable] = useState(undefined); + const [loading, setLoading] = useState(false); + + const ref = useRef(false); + + useEffect(() => { + ref.current = true; + if (!container) { + const factory = getEmbeddableFactory(SEARCHABLE_LIST_CONTAINER); + const promise = factory?.create(searchableInput); + if (promise) { + promise.then(e => { + if (ref.current) { + setContainer(e); + } + }); + } + } + + if (!embeddable) { + const factory = getEmbeddableFactory< + TodoSoEmbeddableInput, + TodoSoEmbeddableOutput, + TodoSoEmbeddable + >(TODO_SO_EMBEDDABLE); + const promise = factory?.create({ + savedObjectId: 'sample-todo-saved-object', + id: '123', + title: 'Garbage', + }); + if (promise) { + promise.then(e => { + if (ref.current) { + setEmbeddable(e); + } + }); + } + } + return () => { + ref.current = false; + }; + }); + + const onCreateSampleDataClick = async () => { + setLoading(true); + await createSampleData(true); + if (embeddable) embeddable.reload(); + if (container) container.reload(); + setLoading(false); + }; + + return ( + + + + +

Saved object embeddable example

+
+
+
+ + + + + {loading ? : 'Reset sample data'} + +

+ This example showcases an embeddable that is backed by a saved object. Click the + context meny and click Edit todo item to see how to edit and update the saved object. + Refreshing the page after editing this embeddable will preserve your edits. +

+
+ + + {embeddable ? ( + + ) : ( + Loading... + )} + + +

+ This next example showcases another embeddable that is backed by a saved object, and + one that has children that can optionally be backed by a saved object. The first child + is linked to a saved object. The second child has input that does not include a saved + object id, so it is by value. Click the context menu and you can see how to turn the + by value version into a saved object. +

+
+ + {container ? ( + + ) : ( + Loading... + )} +
+
+
+ ); +} diff --git a/examples/embeddable_explorer/public/todo_embeddable_example.tsx b/examples/embeddable_explorer/public/todo_embeddable_example.tsx index ce92301236c2b..f09c45e578bef 100644 --- a/examples/embeddable_explorer/public/todo_embeddable_example.tsx +++ b/examples/embeddable_explorer/public/todo_embeddable_example.tsx @@ -113,7 +113,8 @@ export class TodoEmbeddableExample extends React.Component { const { task, title, icon } = this.state;
- this.embeddable.updateInput({ task, title, icon }); + this.embeddable.updateInput({ attributes: { task, title, icon } + });

diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index a39266ecd8db3..02e8738672a62 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -58,6 +58,7 @@ import { isErrorEmbeddable, openAddPanelFlyout, ViewMode, + SavedObjectEmbeddableInput, } from '../../../../../../plugins/embeddable/public'; import { NavAction, SavedDashboardPanel } from './types'; @@ -374,7 +375,7 @@ export class DashboardAppController { if ($routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE]) { const type = $routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE]; const id = $routeParams[DashboardConstants.ADD_EMBEDDABLE_ID]; - container.addSavedObjectEmbeddable(type, id); + container.addNewEmbeddable(type, { savedObjectId: id }); removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_TYPE); removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_ID); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/embeddable_saved_object_converters.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/embeddable_saved_object_converters.test.ts index d3c3dc46c7057..1f8027f067307 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/embeddable_saved_object_converters.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/embeddable_saved_object_converters.test.ts @@ -24,12 +24,14 @@ import { } from './embeddable_saved_object_converters'; import { SavedDashboardPanel } from '../types'; import { DashboardPanelState } from 'src/plugins/dashboard/public'; -import { EmbeddableInput } from 'src/plugins/embeddable/public'; +import { EmbeddableInput, SavedObjectEmbeddableInput } from 'src/plugins/embeddable/public'; interface CustomInput extends EmbeddableInput { something: string; } +type CustomSavedObjectInput = CustomInput & SavedObjectEmbeddableInput; + test('convertSavedDashboardPanelToPanelState', () => { const savedDashboardPanel: SavedDashboardPanel = { type: 'search', @@ -59,8 +61,8 @@ test('convertSavedDashboardPanelToPanelState', () => { explicitInput: { something: 'hi!', id: '123', + savedObjectId: 'savedObjectId', }, - savedObjectId: 'savedObjectId', type: 'search', }); }); @@ -87,7 +89,7 @@ test('convertSavedDashboardPanelToPanelState does not include undefined id', () }); test('convertPanelStateToSavedDashboardPanel', () => { - const dashboardPanel: DashboardPanelState = { + const dashboardPanel: DashboardPanelState = { gridData: { x: 0, y: 0, @@ -95,10 +97,10 @@ test('convertPanelStateToSavedDashboardPanel', () => { w: 15, i: '123', }, - savedObjectId: 'savedObjectId', explicitInput: { something: 'hi!', id: '123', + savedObjectId: 'savedObjectId', }, type: 'search', }; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/embeddable_saved_object_converters.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/embeddable_saved_object_converters.ts index 500ee7e28daa6..7d9be9a830cdd 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/embeddable_saved_object_converters.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/embeddable_saved_object_converters.ts @@ -17,6 +17,7 @@ * under the License. */ import { omit } from 'lodash'; +import { SavedObjectEmbeddableInput } from '../../../../../../../plugins/embeddable/public'; import { DashboardPanelState } from '../../../../../../../plugins/dashboard/public'; import { SavedDashboardPanel } from '../types'; @@ -26,9 +27,9 @@ export function convertSavedDashboardPanelToPanelState( return { type: savedDashboardPanel.type, gridData: savedDashboardPanel.gridData, - ...(savedDashboardPanel.id !== undefined && { savedObjectId: savedDashboardPanel.id }), explicitInput: { id: savedDashboardPanel.panelIndex, + ...(savedDashboardPanel.id !== undefined && { savedObjectId: savedDashboardPanel.id }), ...(savedDashboardPanel.title !== undefined && { title: savedDashboardPanel.title }), ...savedDashboardPanel.embeddableConfig, }, @@ -42,13 +43,14 @@ export function convertPanelStateToSavedDashboardPanel( const customTitle: string | undefined = panelState.explicitInput.title ? (panelState.explicitInput.title as string) : undefined; + const savedObjectId = (panelState.explicitInput as SavedObjectEmbeddableInput).savedObjectId; return { version, type: panelState.type, gridData: panelState.gridData, panelIndex: panelState.explicitInput.id, - embeddableConfig: omit(panelState.explicitInput, 'id'), + embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId']), ...(customTitle && { title: customTitle }), - ...(panelState.savedObjectId !== undefined && { id: panelState.savedObjectId }), + ...(savedObjectId !== undefined && { id: savedObjectId }), }; } diff --git a/src/plugins/dashboard/public/actions/replace_panel_flyout.tsx b/src/plugins/dashboard/public/actions/replace_panel_flyout.tsx index a1cd865f771d4..1d3129018c9ff 100644 --- a/src/plugins/dashboard/public/actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard/public/actions/replace_panel_flyout.tsx @@ -20,7 +20,10 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; -import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { + EmbeddableStart, + SavedObjectEmbeddableInput, +} from '../../../../../src/plugins/embeddable/public'; import { DashboardPanelState } from '../embeddable'; import { NotificationsStart, Toast } from '../../../../core/public'; import { IContainer, IEmbeddable, EmbeddableInput, EmbeddableOutput } from '../embeddable_plugin'; @@ -61,7 +64,7 @@ export class ReplacePanelFlyout extends React.Component { }); }; - public onReplacePanel = async (id: string, type: string, name: string) => { + public onReplacePanel = async (savedObjectId: string, type: string, name: string) => { const originalPanels = this.props.container.getInput().panels; const filteredPanels = { ...originalPanels }; @@ -71,7 +74,9 @@ export class ReplacePanelFlyout extends React.Component { const nny = (filteredPanels[this.props.panelToRemove.id] as DashboardPanelState).gridData.y; // add the new view - const newObj = await this.props.container.addSavedObjectEmbeddable(type, id); + const newObj = await this.props.container.addNewEmbeddable(type, { + savedObjectId, + }); const finalPanels = _.cloneDeep(this.props.container.getInput().panels); (finalPanels[newObj.id] as DashboardPanelState).gridData.w = nnw; diff --git a/src/plugins/dashboard/public/bwc/types.ts b/src/plugins/dashboard/public/bwc/types.ts index e9b9d392e9b7d..a911723ea6bea 100644 --- a/src/plugins/dashboard/public/bwc/types.ts +++ b/src/plugins/dashboard/public/bwc/types.ts @@ -82,6 +82,11 @@ export type DashboardDocPre700 = DocPre700; // embedded in the panels. The reason this is stored at the top level is so the references can be uniformly // updated across all saved object types that have references. +/** + * This should always represent the latest dashboard panel shape, after all possible migrations. + */ +export type RawSavedDashboardPanel = RawSavedDashboardPanel730ToLatest; + // Starting in 7.3 we introduced the possibility of embeddables existing without an id // parameter. If there was no id, then type remains on the panel. So it either will have a name, // or a type property. diff --git a/src/plugins/dashboard/public/embeddable/types.ts b/src/plugins/dashboard/public/embeddable/types.ts index 480d03552ca68..520fb88a6039f 100644 --- a/src/plugins/dashboard/public/embeddable/types.ts +++ b/src/plugins/dashboard/public/embeddable/types.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { SavedObjectEmbeddableInput } from 'src/plugins/embeddable/public'; import { PanelState, EmbeddableInput } from '../embeddable_plugin'; export type PanelId = string; export type SavedObjectId = string; @@ -28,7 +29,8 @@ export interface GridData { i: string; } -export interface DashboardPanelState - extends PanelState { +export interface DashboardPanelState< + TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput +> extends PanelState { readonly gridData: GridData; } diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index df3c312c7ae1b..f86b4cc6b2149 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -152,6 +152,7 @@ export class DashboardEmbeddableContainerPublicPlugin indexPatterns, chrome: core.chrome, overlays: core.overlays, + embeddable: plugins.embeddable, }); return { getSavedDashboardLoader: () => savedDashboardLoader, diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts index 2a1e64fa88a02..8a952c20c09fb 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts @@ -18,21 +18,23 @@ */ import { SavedObjectsClientContract, ChromeStart, OverlayStart } from 'kibana/public'; +import { EmbeddableStart } from 'src/plugins/embeddable/public'; import { IndexPatternsContract } from '../../../../plugins/data/public'; import { SavedObjectLoader } from '../../../../plugins/saved_objects/public'; import { createSavedDashboardClass } from './saved_dashboard'; -interface Services { +export interface CreateSavedDashboardServices { savedObjectsClient: SavedObjectsClientContract; indexPatterns: IndexPatternsContract; chrome: ChromeStart; overlays: OverlayStart; + embeddable: EmbeddableStart; } /** * @param services */ -export function createSavedDashboardLoader(services: Services) { +export function createSavedDashboardLoader(services: CreateSavedDashboardServices) { const SavedDashboard = createSavedDashboardClass(services); return new SavedObjectLoader(SavedDashboard, services.savedObjectsClient, services.chrome); } diff --git a/src/plugins/dashboard/public/saved_dashboards/types.ts b/src/plugins/dashboard/public/saved_dashboards/types.ts new file mode 100644 index 0000000000000..9c37bb3a8431b --- /dev/null +++ b/src/plugins/dashboard/public/saved_dashboards/types.ts @@ -0,0 +1,131 @@ +/* + * 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 { ViewMode } from 'src/plugins/embeddable/public'; +import { + RawSavedDashboardPanelTo60, + RawSavedDashboardPanel610, + RawSavedDashboardPanel620, + RawSavedDashboardPanel630, + RawSavedDashboardPanel640To720, + RawSavedDashboardPanel730ToLatest, +} from '../bwc'; +import { Query, Filter } from '../../../../plugins/data/public'; + +export type NavAction = (anchorElement?: any) => void; + +/** + * This should always represent the latest dashboard panel shape, after all possible migrations. + */ +export type SavedDashboardPanel = SavedDashboardPanel730ToLatest; + +// id becomes optional starting in 7.3.0 +export type SavedDashboardPanel730ToLatest = Pick< + RawSavedDashboardPanel730ToLatest, + Exclude +> & { + readonly id?: string; + readonly type: string; +}; + +export type SavedDashboardPanel640To720 = Pick< + RawSavedDashboardPanel640To720, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanel630 = Pick< + RawSavedDashboardPanel630, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanel620 = Pick< + RawSavedDashboardPanel620, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanel610 = Pick< + RawSavedDashboardPanel610, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanelTo60 = Pick< + RawSavedDashboardPanelTo60, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export interface DashboardAppState { + panels: SavedDashboardPanel[]; + fullScreenMode: boolean; + title: string; + description: string; + timeRestore: boolean; + options: { + hidePanelTitles: boolean; + useMargins: boolean; + }; + query: Query | string; + filters: Filter[]; + viewMode: ViewMode; + savedQuery?: string; +} + +export type DashboardAppStateDefaults = DashboardAppState & { + description?: string; +}; + +export interface DashboardAppStateTransitions { + set: ( + state: DashboardAppState + ) => ( + prop: T, + value: DashboardAppState[T] + ) => DashboardAppState; + setOption: ( + state: DashboardAppState + ) => ( + prop: T, + value: DashboardAppState['options'][T] + ) => DashboardAppState; +} + +export interface SavedDashboardPanelMap { + [key: string]: SavedDashboardPanel; +} + +export interface StagedFilter { + field: string; + value: string; + operator: string; + index: string; +} diff --git a/src/plugins/embeddable/public/bootstrap.ts b/src/plugins/embeddable/public/bootstrap.ts index c8c4f0b95c458..1b55f63d14f16 100644 --- a/src/plugins/embeddable/public/bootstrap.ts +++ b/src/plugins/embeddable/public/bootstrap.ts @@ -32,6 +32,7 @@ import { FilterActionContext, ACTION_APPLY_FILTER, } from './lib'; +import { EmbeddableStart } from './plugin'; declare module '../../ui_actions/public' { export interface TriggerContextMapping { @@ -53,7 +54,10 @@ declare module '../../ui_actions/public' { * This method initializes Embeddable plugin with initial set of * triggers and actions. */ -export const bootstrap = (uiActions: UiActionsSetup) => { +export const bootstrap = ( + uiActions: UiActionsSetup, + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'] +) => { uiActions.registerTrigger(contextMenuTrigger); uiActions.registerTrigger(panelBadgeTrigger); diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index eca74af4ec253..192c3d290fab1 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -60,6 +60,9 @@ export { PropertySpec, ViewMode, withEmbeddableSubscription, + SavedObjectEmbeddableInput, + isSavedObjectEmbeddableFactory, + SavedObjectEmbeddableFactory, } from './lib'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts index 4ab74e1883917..c1d80655bf22c 100644 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ b/src/plugins/embeddable/public/lib/containers/container.ts @@ -30,6 +30,7 @@ import { import { IContainer, ContainerInput, ContainerOutput, PanelState } from './i_container'; import { PanelNotFoundError, EmbeddableFactoryNotFoundError } from '../errors'; import { EmbeddableStart } from '../../plugin'; +import { isSavedObjectEmbeddableInput } from '../embeddables/saved_object_embeddable'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -98,17 +99,6 @@ export abstract class Container< return this.createAndSaveEmbeddable(type, panelState); } - public async addSavedObjectEmbeddable< - TEmbeddableInput extends EmbeddableInput = EmbeddableInput, - TEmbeddable extends IEmbeddable = IEmbeddable - >(type: string, savedObjectId: string): Promise { - const factory = this.getFactory(type) as EmbeddableFactory; - const panelState = this.createNewPanelState(factory); - panelState.savedObjectId = savedObjectId; - - return this.createAndSaveEmbeddable(type, panelState); - } - public removeEmbeddable(embeddableId: string) { // Just a shortcut for removing the panel from input state, all internal state will get cleaned up naturally // by the listener. @@ -298,15 +288,19 @@ export abstract class Container< } as Partial); let embeddable: IEmbeddable | ErrorEmbeddable | undefined; const inputForChild = this.getInputForChild(panel.explicitInput.id); + try { const factory = this.getFactory(panel.type); if (!factory) { throw new EmbeddableFactoryNotFoundError(panel.type); } - embeddable = panel.savedObjectId - ? await factory.createFromSavedObject(panel.savedObjectId, inputForChild, this) - : await factory.create(inputForChild, this); + // TODO: lets get rid of this distinction with factories, I don't think it will be needed + // anymore after this change. + embeddable = + isSavedObjectEmbeddableInput(inputForChild) && inputForChild.savedObjectId + ? await factory.createFromSavedObject(inputForChild.savedObjectId, inputForChild, this) + : await factory.create(inputForChild, this); } catch (e) { embeddable = new ErrorEmbeddable(e, { id: panel.explicitInput.id }, this); } @@ -323,23 +317,6 @@ export abstract class Container< return; } - if (embeddable.getOutput().savedObjectId) { - this.updateInput({ - panels: { - ...this.input.panels, - [panel.explicitInput.id]: { - ...this.input.panels[panel.explicitInput.id], - ...(embeddable.getOutput().savedObjectId - ? { savedObjectId: embeddable.getOutput().savedObjectId } - : undefined), - explicitInput: { - ...this.input.panels[panel.explicitInput.id].explicitInput, - }, - }, - }, - } as Partial); - } - this.children[embeddable.id] = embeddable; this.updateOutput({ embeddableLoaded: { diff --git a/src/plugins/embeddable/public/lib/containers/i_container.ts b/src/plugins/embeddable/public/lib/containers/i_container.ts index 7da5f92ec92c1..7523d382953b1 100644 --- a/src/plugins/embeddable/public/lib/containers/i_container.ts +++ b/src/plugins/embeddable/public/lib/containers/i_container.ts @@ -26,8 +26,6 @@ import { } from '../embeddables'; export interface PanelState { - savedObjectId?: string; - // The type of embeddable in this panel. Will be used to find the factory in which to // load the embeddable. type: string; @@ -35,7 +33,7 @@ export interface PanelState { // Stores input for this embeddable that is specific to this embeddable. Other parts of embeddable input // will be derived from the container's input. **Any state in here will override any state derived from // the container.** - explicitInput: Partial & { id: string }; + explicitInput: Partial & { id: string } & { [key: string]: unknown }; } export interface ContainerOutput extends EmbeddableOutput { @@ -89,17 +87,6 @@ export interface IContainer< */ removeEmbeddable(embeddableId: string): void; - /** - * Adds a new embeddable that is backed off of a saved object. - */ - addSavedObjectEmbeddable< - EEI extends EmbeddableInput = EmbeddableInput, - E extends Embeddable = Embeddable - >( - type: string, - savedObjectId: string - ): Promise; - /** * Adds a new embeddable to the container. `explicitInput` may partially specify the required embeddable input, * but the remainder must come from inherited container state. diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index eb10c16806640..d10f147c80e6a 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -25,7 +25,7 @@ import { ViewMode } from '../types'; import { TriggerContextMapping } from '../ui_actions'; import { EmbeddableActionStorage } from './embeddable_action_storage'; -function getPanelTitle(input: EmbeddableInput, output: EmbeddableOutput) { +export function getPanelTitle(input: EmbeddableInput, output: EmbeddableOutput) { return input.hidePanelTitles ? '' : input.title === undefined ? output.defaultTitle : input.title; } @@ -136,6 +136,16 @@ export abstract class Embeddable< } } + /** + * Implement replaceReferences if your Embeddable contains any references to saved objects (be sure + * also to propagate this call down to any nested Embeddable references, or other implementations that may have + * nested references, like expressions). This supports functionality like "copy to space", when we want + * the copy to create copies of nested saved objects - we need to replace the ids. It also supports saved object + * id migrations although that should be very rare. + * @param references + */ + public replaceReferences(references: Array<{ oldId: string; type: string; newId: string }>) {} + public render(domNode: HTMLElement | Element): void { if (this.destoyed) { throw new Error('Embeddable has been destroyed'); diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts index 6345c34b0dda2..8e17122f5a200 100644 --- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts @@ -26,6 +26,11 @@ import { TriggerContextMapping } from '../../../../ui_actions/public'; export interface EmbeddableInput { viewMode?: ViewMode; title?: string; + /** + * Note this is not a saved object id. It is used to uniquely identify this + * Embeddable instance from others (e.g. inside a container). It's possible to + * have two Embeddables where everything else is the same but the id. + */ id: string; lastReloadRequestTime?: number; hidePanelTitles?: boolean; @@ -44,6 +49,8 @@ export interface EmbeddableInput { * Whether this embeddable should not execute triggers. */ disableTriggers?: boolean; + + [key: string]: unknown; } export interface EmbeddableOutput { @@ -52,6 +59,7 @@ export interface EmbeddableOutput { title?: string; editable?: boolean; savedObjectId?: string; + references?: Array<{ id: string; type: string }>; } export interface IEmbeddable< diff --git a/src/plugins/embeddable/public/lib/embeddables/index.ts b/src/plugins/embeddable/public/lib/embeddables/index.ts index 2175c3a59aa58..cd9257e2d1ecd 100644 --- a/src/plugins/embeddable/public/lib/embeddables/index.ts +++ b/src/plugins/embeddable/public/lib/embeddables/index.ts @@ -27,3 +27,5 @@ export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable'; export { withEmbeddableSubscription } from './with_subscription'; export { EmbeddableFactoryRenderer } from './embeddable_factory_renderer'; export { EmbeddableRoot } from './embeddable_root'; +export * from './saved_object_embeddable'; +export * from './saved_object_embeddable_factory'; diff --git a/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts new file mode 100644 index 0000000000000..bcdae711c8514 --- /dev/null +++ b/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts @@ -0,0 +1,35 @@ +/* + * 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, EmbeddableOutput } from '..'; + +export interface SavedObjectEmbeddableInput extends EmbeddableInput { + /** + * Optional if a saved object embeddable isn't saved yet. + */ + savedObjectId?: string; +} + +export type SavedObjectEmbeddableOutput = EmbeddableOutput; + +export function isSavedObjectEmbeddableInput( + input: EmbeddableInput | SavedObjectEmbeddableInput +): input is SavedObjectEmbeddableInput { + return (input as SavedObjectEmbeddableInput).savedObjectId !== undefined; +} diff --git a/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable_factory.ts new file mode 100644 index 0000000000000..d5b2376d14aed --- /dev/null +++ b/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable_factory.ts @@ -0,0 +1,52 @@ +/* + * 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, SimpleSavedObject } from 'kibana/public'; +import { SavedObjectMetaData } from 'src/plugins/saved_objects/public'; +import { SavedObjectEmbeddableInput, SavedObjectEmbeddableOutput } from './saved_object_embeddable'; +import { EmbeddableFactory } from './embeddable_factory'; +import { IEmbeddable } from './i_embeddable'; + +// interface StartServices { +// getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; +// savedObjectsClient: SavedObjectsClient; +// } + +export function isSavedObjectEmbeddableFactory( + factory: EmbeddableFactory | SavedObjectEmbeddableFactory +): factory is SavedObjectEmbeddableFactory { + return (factory as SavedObjectEmbeddableFactory).savedObjectMetaData !== undefined; +} + +export abstract class SavedObjectEmbeddableFactory< + I extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput, + O extends SavedObjectEmbeddableOutput = SavedObjectEmbeddableOutput, + E extends IEmbeddable = IEmbeddable, + SA extends SavedObjectAttributes = SavedObjectAttributes +> extends EmbeddableFactory { + public savedObjectMetaData: SavedObjectMetaData; + + constructor({ savedObjectMetaData }: { savedObjectMetaData: SavedObjectMetaData }) { + super({ savedObjectMetaData }); + this.savedObjectMetaData = savedObjectMetaData; + } + + abstract canSave(embeddable: E): boolean; + + abstract save(embeddable: E): Promise; +} diff --git a/src/plugins/embeddable/public/lib/embeddables/with_subscription.tsx b/src/plugins/embeddable/public/lib/embeddables/with_subscription.tsx index 47b8001961cf5..25bb51ef27060 100644 --- a/src/plugins/embeddable/public/lib/embeddables/with_subscription.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/with_subscription.tsx @@ -23,18 +23,19 @@ import { IEmbeddable, EmbeddableInput, EmbeddableOutput } from './i_embeddable'; export const withEmbeddableSubscription = < I extends EmbeddableInput, O extends EmbeddableOutput, - E extends IEmbeddable = IEmbeddable + E extends IEmbeddable = IEmbeddable, + P = {} >( - WrappedComponent: React.ComponentType<{ input: I; output: O; embeddable: E }> -): React.ComponentType<{ embeddable: E }> => + WrappedComponent: React.ComponentType<{ input: I; output: O; embeddable: E } & P> +): React.ComponentType<{ embeddable: E } & P> => class WithEmbeddableSubscription extends React.Component< - { embeddable: E }, + { embeddable: E } & P, { input: I; output: O } > { private subscription?: Rx.Subscription; private mounted: boolean = false; - constructor(props: { embeddable: E }) { + constructor(props: { embeddable: E } & P) { super(props); this.state = { input: this.props.embeddable.getInput(), @@ -71,6 +72,7 @@ export const withEmbeddableSubscription = < input={this.state.input} output={this.state.output} embeddable={this.props.embeddable} + {...this.props} /> ); } diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index b95060a73252f..f0be8d4c8a6da 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -19,7 +19,7 @@ import { EuiContextMenuPanelDescriptor, EuiPanel, htmlIdGenerator } from '@elastic/eui'; import classNames from 'classnames'; import React from 'react'; -import { Subscription } from 'rxjs'; +import { Subscription, merge } from 'rxjs'; import { buildContextMenuForActions, UiActionsService, Action } from '../ui_actions'; import { CoreStart, OverlayStart } from '../../../../../core/public'; import { toMountPoint } from '../../../../kibana_react/public'; @@ -57,6 +57,7 @@ interface State { hidePanelTitles: boolean; closeContextMenu: boolean; badges: Array>; + displayTitle?: string; } export class EmbeddablePanel extends React.Component { @@ -82,6 +83,7 @@ export class EmbeddablePanel extends React.Component { hidePanelTitles, closeContextMenu: false, badges: [], + displayTitle: embeddable.getTitle(), }; this.embeddableRoot = React.createRef(); @@ -108,15 +110,20 @@ export class EmbeddablePanel extends React.Component { const { embeddable } = this.props; const { parent } = embeddable; - this.subscription = embeddable.getInput$().subscribe(async () => { - if (this.mounted) { - this.setState({ - viewMode: embeddable.getInput().viewMode ? embeddable.getInput().viewMode : ViewMode.EDIT, - }); + this.subscription = merge(embeddable.getOutput$(), embeddable.getInput$()).subscribe( + async () => { + if (this.mounted) { + this.setState({ + viewMode: embeddable.getInput().viewMode + ? embeddable.getInput().viewMode + : ViewMode.EDIT, + displayTitle: embeddable.getTitle(), + }); - this.refreshBadges(); + this.refreshBadges(); + } } - }); + ); if (parent) { this.parentSubscription = parent.getInput$().subscribe(async () => { @@ -204,7 +211,6 @@ export class EmbeddablePanel extends React.Component { let actions = await this.props.getActions(CONTEXT_MENU_TRIGGER, { embeddable: this.props.embeddable, }); - const { disabledActions } = this.props.embeddable.getInput(); if (disabledActions) { actions = actions.filter(action => disabledActions.indexOf(action.id) === -1); diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx index 95eeb63710c32..ea615627f2b49 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx @@ -33,6 +33,7 @@ import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; import { IContainer } from '../../../../containers'; import { EmbeddableFactoryNotFoundError } from '../../../../errors'; import { SavedObjectFinderCreateNew } from './saved_object_finder_create_new'; +import { SavedObjectEmbeddableInput } from '../../../../embeddables'; interface Props { onClose: () => void; @@ -98,8 +99,18 @@ export class AddPanelFlyout extends React.Component { } }; - public onAddPanel = async (id: string, type: string, name: string) => { - this.props.container.addSavedObjectEmbeddable(type, id); + public onAddPanel = async (savedObjectId: string, savedObjectType: string, name: string) => { + const factoryForSavedObjectType = [...this.props.getAllFactories()].find( + factory => factory.savedObjectMetaData && factory.savedObjectMetaData.type === savedObjectType + ); + if (!factoryForSavedObjectType) { + throw new EmbeddableFactoryNotFoundError(savedObjectType); + } + + this.props.container.addNewEmbeddable( + factoryForSavedObjectType.type, + { savedObjectId } + ); this.showToast(name); }; @@ -121,15 +132,16 @@ export class AddPanelFlyout extends React.Component { public render() { const SavedObjectFinder = this.props.SavedObjectFinder; + const metaData = [...this.props.getAllFactories()] + .filter( + embeddableFactory => + Boolean(embeddableFactory.savedObjectMetaData) && !embeddableFactory.isContainerType + ) + .map(({ savedObjectMetaData }) => savedObjectMetaData as any); const savedObjectsFinder = ( - Boolean(embeddableFactory.savedObjectMetaData) && !embeddableFactory.isContainerType - ) - .map(({ savedObjectMetaData }) => savedObjectMetaData as any)} + savedObjectMetaData={metaData} showFilter={true} noItemsMessage={i18n.translate('embeddableApi.addPanel.noMatchingObjectsMessage', { defaultMessage: 'No matching objects found.', diff --git a/src/plugins/embeddable/public/plugin.ts b/src/plugins/embeddable/public/plugin.ts index 381665c359ffd..ad8f3b9cdf08c 100644 --- a/src/plugins/embeddable/public/plugin.ts +++ b/src/plugins/embeddable/public/plugin.ts @@ -20,7 +20,7 @@ import { UiActionsSetup } from 'src/plugins/ui_actions/public'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { EmbeddableFactoryRegistry } from './types'; import { bootstrap } from './bootstrap'; -import { EmbeddableFactory, EmbeddableInput, EmbeddableOutput } from './lib'; +import { EmbeddableFactory, EmbeddableInput, EmbeddableOutput, IEmbeddable } from './lib'; export interface EmbeddableSetupDependencies { uiActions: UiActionsSetup; @@ -35,10 +35,11 @@ export interface EmbeddableSetup { export interface EmbeddableStart { getEmbeddableFactory: < I extends EmbeddableInput = EmbeddableInput, - O extends EmbeddableOutput = EmbeddableOutput + O extends EmbeddableOutput = EmbeddableOutput, + E extends IEmbeddable = IEmbeddable >( embeddableFactoryId: string - ) => EmbeddableFactory | undefined; + ) => EmbeddableFactory | undefined; getEmbeddableFactories: () => IterableIterator; } @@ -48,7 +49,7 @@ export class EmbeddablePublicPlugin implements Plugin = IEmbeddable >( embeddableFactoryId: string ) => { @@ -88,6 +90,6 @@ export class EmbeddablePublicPlugin implements Plugin; + return factory as EmbeddableFactory; }; } diff --git a/src/plugins/embeddable/public/tests/container.test.ts b/src/plugins/embeddable/public/tests/container.test.ts index 1ee52f4749135..f64a5c26b67c6 100644 --- a/src/plugins/embeddable/public/tests/container.test.ts +++ b/src/plugins/embeddable/public/tests/container.test.ts @@ -640,8 +640,7 @@ test('container stores ErrorEmbeddables when a saved object cannot be found', as panels: { '123': { type: 'vis', - explicitInput: { id: '123' }, - savedObjectId: '456', + explicitInput: { id: '123', savedObjectId: '456' }, }, }, viewMode: ViewMode.EDIT, @@ -662,8 +661,7 @@ test('ErrorEmbeddables get updated when parent does', async done => { panels: { '123': { type: 'vis', - explicitInput: { id: '123' }, - savedObjectId: '456', + explicitInput: { id: '123', savedObjectId: '456' }, }, }, viewMode: ViewMode.EDIT, diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.ts b/src/plugins/ui_actions/public/service/ui_actions_service.ts index f7718e63773f5..ec74a7ad051e4 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts @@ -103,7 +103,7 @@ export class UiActionsService { } else { const registeredAction = this.actions.get(action.id); if (registeredAction !== action) { - throw new Error(`A different action instance with this id is already registered.`); + throw new Error(`An action with the id ${action.id} is already registered.`); } } diff --git a/test/examples/embeddables/adding_children.ts b/test/examples/embeddables/adding_children.ts index 110b8ce573332..ddb1a06eebe64 100644 --- a/test/examples/embeddables/adding_children.ts +++ b/test/examples/embeddables/adding_children.ts @@ -23,9 +23,12 @@ import { PluginFunctionalProviderContext } from 'test/plugin_functional/services // eslint-disable-next-line import/no-default-export export default function({ getService }: PluginFunctionalProviderContext) { const testSubjects = getService('testSubjects'); + const flyout = getService('flyout'); describe('creating and adding children', () => { before(async () => { + await testSubjects.click('savedObjectSection'); + await testSubjects.click('reset-sample-data'); await testSubjects.click('embeddablePanelExamplae'); }); @@ -39,5 +42,14 @@ export default function({ getService }: PluginFunctionalProviderContext) { const tasks = await testSubjects.getVisibleTextAll('todoEmbeddableTask'); expect(tasks).to.eql(['Goes out on Wednesdays!', 'new task']); }); + + it('Can add a child backed off a saved object', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelAction-ACTION_ADD_PANEL'); + await testSubjects.click('savedObjectTitleGarbage'); + await flyout.ensureClosed('dashboardAddPanel'); + const task = await testSubjects.getVisibleText('todoSoEmbeddableTask'); + expect(task).to.eql('Take the garbage out'); + }); }); } diff --git a/test/examples/embeddables/index.ts b/test/examples/embeddables/index.ts index 8ad0961fcc3b6..f3861cb0557ad 100644 --- a/test/examples/embeddables/index.ts +++ b/test/examples/embeddables/index.ts @@ -40,5 +40,6 @@ export default function({ loadTestFile(require.resolve('./todo_embeddable')); loadTestFile(require.resolve('./list_container')); loadTestFile(require.resolve('./adding_children')); + loadTestFile(require.resolve('./saved_object_embeddable')); }); } diff --git a/test/examples/embeddables/saved_object_embeddable.ts b/test/examples/embeddables/saved_object_embeddable.ts new file mode 100644 index 0000000000000..786c71eb55e64 --- /dev/null +++ b/test/examples/embeddables/saved_object_embeddable.ts @@ -0,0 +1,80 @@ +/* + * 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 expect from '@kbn/expect'; + +import { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: PluginFunctionalProviderContext) { + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + const retry = getService('retry'); + const dashboardPanelActions = getService('dashboardPanelActions'); + + describe('saved object embeddable', () => { + before(async () => { + await testSubjects.click('savedObjectSection'); + await testSubjects.click('reset-sample-data'); + }); + + it('renders', async () => { + await retry.try(async () => { + const texts = await testSubjects.getVisibleTextAll('todoSoEmbeddableTitle'); + expect(texts).to.eql(['Garbage', 'Garbage', 'Take out the trash (By value example)']); + }); + }); + + it('can be edited when backed by saved object ', async () => { + const header = await dashboardPanelActions.getPanelHeading('Garbage'); + await dashboardPanelActions.openContextMenu(header); + await testSubjects.click('embeddablePanelAction-EDIT_TODO_ACTION'); + await testSubjects.setValue('titleInputField', 'Trash'); + await testSubjects.click('saveTodoEmbeddableByRef'); + + await retry.try(async () => { + const texts = await testSubjects.getVisibleTextAll('todoSoEmbeddableTitle'); + expect(texts).to.eql(['Trash', 'Garbage', 'Take out the trash (By value example)']); + }); + }); + + it('can be edited when not backed by saved object', async () => { + const header = await dashboardPanelActions.getPanelHeading( + 'Take out the trash (By value example)' + ); + await dashboardPanelActions.openContextMenu(header); + await testSubjects.click('embeddablePanelAction-EDIT_TODO_ACTION'); + await testSubjects.setValue('titleInputField', 'Junk'); + await testSubjects.click('saveTodoEmbeddableByValue'); + + await retry.try(async () => { + const texts = await testSubjects.getVisibleTextAll('todoSoEmbeddableTitle'); + expect(texts).to.eql(['Trash', 'Garbage', 'Junk']); + }); + + const url = await browser.getCurrentUrl(); + await browser.get(url.toString(), true); + + await retry.try(async () => { + const texts2 = await testSubjects.getVisibleTextAll('todoSoEmbeddableTitle'); + expect(texts2).to.eql(['Trash', 'Trash', 'Take out the trash (By value example)']); + }); + }); + }); +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_input.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_input.ts index bb8951680be35..37ef8cad948cb 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_input.ts +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_input.ts @@ -47,7 +47,7 @@ export const dashboardInput: DashboardContainerInput = { explicitInput: { id: '2', firstName: 'Sue', - } as any, + }, }, '822cd0f0-ce7c-419d-aeaa-1171cf452745': { gridData: { @@ -60,8 +60,8 @@ export const dashboardInput: DashboardContainerInput = { type: 'visualization', explicitInput: { id: '822cd0f0-ce7c-419d-aeaa-1171cf452745', + savedObjectId: '3fe22200-3dcb-11e8-8660-4d65aa086b3c', }, - savedObjectId: '3fe22200-3dcb-11e8-8660-4d65aa086b3c', }, '66f0a265-7b06-4974-accd-d05f74f7aa82': { gridData: { @@ -74,8 +74,8 @@ export const dashboardInput: DashboardContainerInput = { type: 'visualization', explicitInput: { id: '66f0a265-7b06-4974-accd-d05f74f7aa82', + savedObjectId: '4c0f47e0-3dcd-11e8-8660-4d65aa086b3c', }, - savedObjectId: '4c0f47e0-3dcd-11e8-8660-4d65aa086b3c', }, 'b2861741-40b9-4dc8-b82b-080c6e29a551': { gridData: { @@ -88,8 +88,8 @@ export const dashboardInput: DashboardContainerInput = { type: 'search', explicitInput: { id: 'b2861741-40b9-4dc8-b82b-080c6e29a551', + savedObjectId: 'be5accf0-3dca-11e8-8660-4d65aa086b3c', }, - savedObjectId: 'be5accf0-3dca-11e8-8660-4d65aa086b3c', }, }, isFullScreenMode: false,