Add embeddable via saved object example (#61692)

* Add embeddable via saved object example

* give todoRefEmbed a different name from the by value one

* fix types

* fix order of unmounting

Co-authored-by: Christos Nasikas <christos.nasikas@elastic.co>
This commit is contained in:
Stacey Gammon 2020-04-15 11:10:20 -04:00 committed by GitHub
parent 00a1144ae2
commit d0404487f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 679 additions and 90 deletions

View file

@ -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 './todo_saved_object_attributes';

View file

@ -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 '../../../src/core/types';
export interface TodoSavedObjectAttributes extends SavedObjectAttributes {
task: string;
icon?: string;
title?: string;
}

View file

@ -3,7 +3,7 @@
"version": "0.0.1",
"kibanaVersion": "kibana",
"configPath": ["embeddable_examples"],
"server": false,
"server": true,
"ui": true,
"requiredPlugins": ["embeddable"],
"optionalPlugins": []

View file

@ -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) {
await client.create<TodoSavedObjectAttributes>(
'todo',
{
task: 'Take the garbage out',
title: 'Garbage',
icon: 'trash',
},
{
id: 'sample-todo-saved-object',
overwrite: true,
}
);
}

View file

@ -17,7 +17,6 @@
* under the License.
*/
import { PluginInitializer } from 'kibana/public';
export {
HELLO_WORLD_EMBEDDABLE,
HelloWorldEmbeddable,
@ -26,18 +25,8 @@ 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 const plugin: PluginInitializer<
void,
void,
EmbeddableExamplesSetupDependencies,
EmbeddableExamplesStartDependencies
> = () => new EmbeddableExamplesPlugin();
export const plugin = () => new EmbeddableExamplesPlugin();

View file

@ -21,12 +21,20 @@ import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddabl
import { Plugin, CoreSetup, CoreStart } from '../../../src/core/public';
import { HelloWorldEmbeddableFactory, HELLO_WORLD_EMBEDDABLE } from './hello_world';
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 { TodoRefInput, TodoRefOutput, TODO_REF_EMBEDDABLE } from './todo/todo_ref_embeddable';
import { TodoRefEmbeddableFactory } from './todo/todo_ref_embeddable_factory';
export interface EmbeddableExamplesSetupDependencies {
embeddable: EmbeddableSetup;
@ -36,9 +44,18 @@ export interface EmbeddableExamplesStartDependencies {
embeddable: EmbeddableStart;
}
export interface EmbeddableExamplesStart {
createSampleData: () => Promise<void>;
}
export class EmbeddableExamplesPlugin
implements
Plugin<void, void, EmbeddableExamplesSetupDependencies, EmbeddableExamplesStartDependencies> {
Plugin<
void,
EmbeddableExamplesStart,
EmbeddableExamplesSetupDependencies,
EmbeddableExamplesStartDependencies
> {
public setup(
core: CoreSetup<EmbeddableExamplesStartDependencies>,
deps: EmbeddableExamplesSetupDependencies
@ -48,7 +65,7 @@ export class EmbeddableExamplesPlugin
new HelloWorldEmbeddableFactory()
);
deps.embeddable.registerEmbeddableFactory(
deps.embeddable.registerEmbeddableFactory<MultiTaskTodoInput, MultiTaskTodoOutput>(
MULTI_TASK_TODO_EMBEDDABLE,
new MultiTaskTodoEmbeddableFactory()
);
@ -73,9 +90,21 @@ export class EmbeddableExamplesPlugin
openModal: (await core.getStartServices())[0].overlays.openModal,
}))
);
deps.embeddable.registerEmbeddableFactory<TodoRefInput, TodoRefOutput>(
TODO_REF_EMBEDDABLE,
new TodoRefEmbeddableFactory(async () => ({
savedObjectsClient: (await core.getStartServices())[0].savedObjects.client,
getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory,
}))
);
}
public start(core: CoreStart, deps: EmbeddableExamplesStartDependencies) {}
public start(core: CoreStart, deps: EmbeddableExamplesStartDependencies) {
return {
createSampleData: () => createSampleData(core.savedObjects.client),
};
}
public stop() {}
}

View file

@ -0,0 +1,43 @@
There are two examples in here:
- TodoEmbeddable
- TodoRefEmbeddable
# TodoEmbeddable
The first example you should review is the HelloWorldEmbeddable. That is as basic an embeddable as you can get.
This embeddable is the next step up - an embeddable that renders dynamic input data. The data is simple:
- a required task string
- an optional title
- an optional icon string
- an optional search string
It also has output data, which is `hasMatch` - whether or not the search string has matched any input data.
`hasMatch` is a better fit for output data than input data, because it's state that is _derived_ from input data.
For example, if it was input data, you could create a TodoEmbeddable with input like this:
```ts
todoEmbeddableFactory.create({ task: 'take out the garabage', search: 'garbage', hasMatch: false });
```
That's wrong because there is actually a match from the search string inside the task.
The TodoEmbeddable component itself doesn't do anything with the `hasMatch` variable other than set it, but
if you check out `SearchableListContainer`, you can see an example where this output data is being used.
## TodoRefEmbeddable
This is an example of an embeddable based off of a saved object. The input is just the `savedObjectId` and
the `search` string. It has even more output parameters, and this time, it does read it's own output parameters in
order to calculate `hasMatch`.
Output:
```ts
{
hasMatch: boolean,
savedAttributes?: TodoSavedAttributes
}
```
`savedAttributes` is optional because it's possible a TodoSavedObject could not be found with the given savedObjectId.

View file

@ -0,0 +1,86 @@
/*
* 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 } from '../../../../src/plugins/embeddable/public';
import { TodoRefInput, TodoRefOutput, TodoRefEmbeddable } from './todo_ref_embeddable';
interface Props {
embeddable: TodoRefEmbeddable;
input: TodoRefInput;
output: TodoRefOutput;
}
function wrapSearchTerms(task?: string, search?: string) {
if (!search) return task;
if (!task) return task;
const parts = task.split(new RegExp(`(${search})`, 'g'));
return parts.map((part, i) =>
part === search ? (
<span key={i} style={{ backgroundColor: 'yellow' }}>
{part}
</span>
) : (
part
)
);
}
export function TodoRefEmbeddableComponentInner({
input: { search },
output: { savedAttributes },
}: Props) {
const icon = savedAttributes?.icon;
const title = savedAttributes?.title;
const task = savedAttributes?.task;
return (
<EuiFlexGroup>
<EuiFlexItem grow={false}>
{icon ? (
<EuiIcon type={icon} size="l" />
) : (
<EuiAvatar name={title || task || ''} size="l" />
)}
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGrid columns={1}>
<EuiFlexItem>
<EuiText data-test-subj="todoEmbeddableTitle">
<h3>{wrapSearchTerms(title || '', search)}</h3>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiText data-test-subj="todoEmbeddableTask">{wrapSearchTerms(task, search)}</EuiText>
</EuiFlexItem>
</EuiFlexGrid>
</EuiFlexItem>
</EuiFlexGroup>
);
}
export const TodoRefEmbeddableComponent = withEmbeddableSubscription<
TodoRefInput,
TodoRefOutput,
TodoRefEmbeddable
>(TodoRefEmbeddableComponentInner);

View file

@ -0,0 +1,153 @@
/*
* 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 { TodoSavedObjectAttributes } from 'examples/embeddable_examples/common';
import { SavedObjectsClientContract } from 'kibana/public';
import {
Embeddable,
IContainer,
EmbeddableOutput,
SavedObjectEmbeddableInput,
} from '../../../../src/plugins/embeddable/public';
import { TodoRefEmbeddableComponent } from './todo_ref_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_REF_EMBEDDABLE = 'TODO_REF_EMBEDDABLE';
export interface TodoRefInput extends SavedObjectEmbeddableInput {
/**
* Optional search string which will be used to highlight search terms as
* well as calculate `output.hasMatch`.
*/
search?: string;
}
export interface TodoRefOutput extends EmbeddableOutput {
/**
* Should be true if input.search is defined and the task or title contain
* search as a substring.
*/
hasMatch: boolean;
/**
* Will contain the saved object attributes of the Todo Saved Object that matches
* `input.savedObjectId`. If the id is invalid, this may be undefined.
*/
savedAttributes?: TodoSavedObjectAttributes;
}
/**
* Returns whether any attributes contain the search string. If search is empty, true is returned. If
* there are no savedAttributes, false is returned.
* @param search - the search string
* @param savedAttributes - the saved object attributes for the saved object with id `input.savedObjectId`
*/
function getHasMatch(search?: string, savedAttributes?: TodoSavedObjectAttributes): boolean {
if (!search) return true;
if (!savedAttributes) return false;
return Boolean(
(savedAttributes.task && savedAttributes.task.match(search)) ||
(savedAttributes.title && savedAttributes.title.match(search))
);
}
/**
* This is an example of an embeddable that is backed by a saved object. It's essentially the
* same as `TodoEmbeddable` but that is "by value", while this is "by reference".
*/
export class TodoRefEmbeddable extends Embeddable<TodoRefInput, TodoRefOutput> {
public readonly type = TODO_REF_EMBEDDABLE;
private subscription: Subscription;
private node?: HTMLElement;
private savedObjectsClient: SavedObjectsClientContract;
private savedObjectId?: string;
constructor(
initialInput: TodoRefInput,
{
parent,
savedObjectsClient,
}: {
parent?: IContainer;
savedObjectsClient: SavedObjectsClientContract;
}
) {
super(initialInput, { hasMatch: false }, parent);
this.savedObjectsClient = savedObjectsClient;
this.subscription = this.getInput$().subscribe(async () => {
// There is a little more work today for this embeddable because it has
// more output it needs to update in response to input state changes.
let savedAttributes: TodoSavedObjectAttributes | undefined;
// Since this is an expensive task, we save a local copy of the previous
// savedObjectId locally and only retrieve the new saved object if the id
// actually changed.
if (this.savedObjectId !== this.input.savedObjectId) {
this.savedObjectId = this.input.savedObjectId;
const todoSavedObject = await this.savedObjectsClient.get<TodoSavedObjectAttributes>(
'todo',
this.input.savedObjectId
);
savedAttributes = todoSavedObject?.attributes;
}
// The search string might have changed as well so we need to make sure we recalculate
// hasMatch.
this.updateOutput({
hasMatch: getHasMatch(this.input.search, savedAttributes),
savedAttributes,
});
});
}
public render(node: HTMLElement) {
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
this.node = node;
ReactDOM.render(<TodoRefEmbeddableComponent embeddable={this} />, node);
}
/**
* Lets re-sync our saved object to make sure it's up to date!
*/
public async reload() {
this.savedObjectId = this.input.savedObjectId;
const todoSavedObject = await this.savedObjectsClient.get<TodoSavedObjectAttributes>(
'todo',
this.input.savedObjectId
);
const savedAttributes = todoSavedObject?.attributes;
this.updateOutput({
hasMatch: getHasMatch(this.input.search, savedAttributes),
savedAttributes,
});
}
public destroy() {
super.destroy();
this.subscription.unsubscribe();
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
}
}

View file

@ -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 { i18n } from '@kbn/i18n';
import { SavedObjectsClientContract } from 'kibana/public';
import { TodoSavedObjectAttributes } from 'examples/embeddable_examples/common';
import {
IContainer,
EmbeddableStart,
ErrorEmbeddable,
EmbeddableFactoryDefinition,
} from '../../../../src/plugins/embeddable/public';
import {
TodoRefEmbeddable,
TODO_REF_EMBEDDABLE,
TodoRefInput,
TodoRefOutput,
} from './todo_ref_embeddable';
interface StartServices {
getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
savedObjectsClient: SavedObjectsClientContract;
}
export class TodoRefEmbeddableFactory
implements
EmbeddableFactoryDefinition<
TodoRefInput,
TodoRefOutput,
TodoRefEmbeddable,
TodoSavedObjectAttributes
> {
public readonly type = TODO_REF_EMBEDDABLE;
public readonly savedObjectMetaData = {
name: 'Todo',
includeFields: ['task', 'icon', 'title'],
type: 'todo',
getIconForSavedObject: () => 'pencil',
};
constructor(private getStartServices: () => Promise<StartServices>) {}
public async isEditable() {
return true;
}
public createFromSavedObject = (
savedObjectId: string,
input: Partial<TodoRefInput> & { id: string },
parent?: IContainer
): Promise<TodoRefEmbeddable | ErrorEmbeddable> => {
return this.create({ ...input, savedObjectId }, parent);
};
public async create(input: TodoRefInput, parent?: IContainer) {
const { savedObjectsClient } = await this.getStartServices();
return new TodoRefEmbeddable(input, {
parent,
savedObjectsClient,
});
}
public getDisplayName() {
return i18n.translate('embeddableExamples.todo.displayName', {
defaultMessage: 'Todo (by reference)',
});
}
}

View file

@ -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<void, void> = () => new EmbeddableExamplesPlugin();

View file

@ -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() {}
}

View file

@ -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: {},
};

View file

@ -6,6 +6,7 @@
},
"include": [
"index.ts",
"common/**/*.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",

View file

@ -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<void, void, {}, StartDeps> {
@ -36,6 +38,7 @@ export class EmbeddableExplorerPlugin implements Plugin<void, void, {}, StartDep
async mount(params: AppMountParameters) {
const [coreStart, depsStart] = await core.getStartServices();
const { renderApp } = await import('./app');
await depsStart.embeddableExamples.createSampleData();
return renderApp(
{
notifications: coreStart.notifications,

View file

@ -28,6 +28,7 @@ import {
EmbeddableInput,
EmbeddableOutput,
EmbeddableStart,
SavedObjectEmbeddableInput,
} from '../../embeddable_plugin';
interface Props {
@ -66,7 +67,7 @@ export class ReplacePanelFlyout extends React.Component<Props> {
});
};
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 };
@ -76,7 +77,9 @@ export class ReplacePanelFlyout extends React.Component<Props> {
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<SavedObjectEmbeddableInput>(type, {
savedObjectId,
});
const finalPanels = _.cloneDeep(this.props.container.getInput().panels);
(finalPanels[newObj.id] as DashboardPanelState).gridData.w = nnw;

View file

@ -53,6 +53,7 @@ import {
isErrorEmbeddable,
openAddPanelFlyout,
ViewMode,
SavedObjectEmbeddableInput,
ContainerOutput,
} from '../../../embeddable/public';
import { NavAction, SavedDashboardPanel } from '../types';
@ -394,7 +395,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<SavedObjectEmbeddableInput>(type, { savedObjectId: id });
removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_TYPE);
removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_ID);
}

View file

@ -55,7 +55,7 @@ export interface DashboardContainerInput extends ContainerInput {
description?: string;
isFullScreenMode: boolean;
panels: {
[panelId: string]: DashboardPanelState;
[panelId: string]: DashboardPanelState<EmbeddableInput & { [k: string]: unknown }>;
};
isEmptyState?: boolean;
}

View file

@ -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<TEmbeddableInput extends EmbeddableInput = EmbeddableInput>
extends PanelState<TEmbeddableInput> {
export interface DashboardPanelState<
TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput
> extends PanelState<TEmbeddableInput> {
readonly gridData: GridData;
}

View file

@ -23,11 +23,6 @@ import {
} from './embeddable_saved_object_converters';
import { SavedDashboardPanel } from '../../types';
import { DashboardPanelState } from '../embeddable';
import { EmbeddableInput } from 'src/plugins/embeddable/public';
interface CustomInput extends EmbeddableInput {
something: string;
}
test('convertSavedDashboardPanelToPanelState', () => {
const savedDashboardPanel: SavedDashboardPanel = {
@ -58,8 +53,8 @@ test('convertSavedDashboardPanelToPanelState', () => {
explicitInput: {
something: 'hi!',
id: '123',
savedObjectId: 'savedObjectId',
},
savedObjectId: 'savedObjectId',
type: 'search',
});
});
@ -86,7 +81,7 @@ test('convertSavedDashboardPanelToPanelState does not include undefined id', ()
});
test('convertPanelStateToSavedDashboardPanel', () => {
const dashboardPanel: DashboardPanelState<CustomInput> = {
const dashboardPanel: DashboardPanelState = {
gridData: {
x: 0,
y: 0,
@ -94,10 +89,10 @@ test('convertPanelStateToSavedDashboardPanel', () => {
w: 15,
i: '123',
},
savedObjectId: 'savedObjectId',
explicitInput: {
something: 'hi!',
id: '123',
savedObjectId: 'savedObjectId',
},
type: 'search',
};
@ -121,7 +116,7 @@ test('convertPanelStateToSavedDashboardPanel', () => {
});
test('convertPanelStateToSavedDashboardPanel will not add an undefined id when not needed', () => {
const dashboardPanel: DashboardPanelState<CustomInput> = {
const dashboardPanel: DashboardPanelState = {
gridData: {
x: 0,
y: 0,

View file

@ -19,6 +19,7 @@
import { omit } from 'lodash';
import { SavedDashboardPanel } from '../../types';
import { DashboardPanelState } from '../embeddable';
import { SavedObjectEmbeddableInput } from '../../embeddable_plugin';
export function convertSavedDashboardPanelToPanelState(
savedDashboardPanel: SavedDashboardPanel
@ -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 }),
};
}

View file

@ -61,6 +61,8 @@ export {
PropertySpec,
ViewMode,
withEmbeddableSubscription,
SavedObjectEmbeddableInput,
isSavedObjectEmbeddableInput,
} from './lib';
export function plugin(initializerContext: PluginInitializerContext) {

View file

@ -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 = <T extends {}>(o: T): Array<keyof T> => Object.keys(o) as Array<keyof T>;
@ -98,17 +99,6 @@ export abstract class Container<
return this.createAndSaveEmbeddable(type, panelState);
}
public async addSavedObjectEmbeddable<
TEmbeddableInput extends EmbeddableInput = EmbeddableInput,
TEmbeddable extends IEmbeddable<TEmbeddableInput> = IEmbeddable<TEmbeddableInput>
>(type: string, savedObjectId: string): Promise<TEmbeddable | ErrorEmbeddable> {
const factory = this.getFactory(type) as EmbeddableFactory<TEmbeddableInput, any, TEmbeddable>;
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.
@ -304,8 +294,10 @@ export abstract class Container<
throw new EmbeddableFactoryNotFoundError(panel.type);
}
embeddable = panel.savedObjectId
? await factory.createFromSavedObject(panel.savedObjectId, 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)
? 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 +315,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<TContainerInput>);
}
this.children[embeddable.id] = embeddable;
this.updateOutput({
embeddableLoaded: {

View file

@ -25,9 +25,7 @@ import {
IEmbeddable,
} from '../embeddables';
export interface PanelState<E extends { id: string } = { id: string }> {
savedObjectId?: string;
export interface PanelState<E extends { id: string; [key: string]: unknown } = { id: string }> {
// The type of embeddable in this panel. Will be used to find the factory in which to
// load the embeddable.
type: string;
@ -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<EEI> = Embeddable<EEI>
>(
type: string,
savedObjectId: string
): Promise<E | ErrorEmbeddable>;
/**
* Adds a new embeddable to the container. `explicitInput` may partially specify the required embeddable input,
* but the remainder must come from inherited container state.

View file

@ -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 {

View file

@ -25,3 +25,4 @@ 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';

View file

@ -0,0 +1,30 @@
/*
* 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 } from '..';
export interface SavedObjectEmbeddableInput extends EmbeddableInput {
savedObjectId: string;
}
export function isSavedObjectEmbeddableInput(
input: EmbeddableInput | SavedObjectEmbeddableInput
): input is SavedObjectEmbeddableInput {
return (input as SavedObjectEmbeddableInput).savedObjectId !== undefined;
}

View file

@ -33,6 +33,7 @@ import { EmbeddableStart } from 'src/plugins/embeddable/public';
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<Props, State> {
}
};
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<SavedObjectEmbeddableInput>(
factoryForSavedObjectType.type,
{ savedObjectId }
);
this.showToast(name);
};

View file

@ -641,8 +641,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,
@ -663,8 +662,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,

View file

@ -23,6 +23,7 @@ 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 () => {
@ -39,5 +40,15 @@ 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 testSubjects.moveMouseTo('euiFlyoutCloseButton');
await flyout.ensureClosed('dashboardAddPanel');
const tasks = await testSubjects.getVisibleTextAll('todoEmbeddableTask');
expect(tasks).to.eql(['Goes out on Wednesdays!', 'new task', 'Take the garbage out']);
});
});
}

View file

@ -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,