Embed dashboard by value example & some embeddable clean up (#67783)

Added example for using dashboard container by value
1.1 Refactored embeddable explorer e2e test to use new example, removed not needed kbn_tp_embeddable_explorer plugin.
For embeddable explorer examples went away from using getFactoryById() to improve type checks
There is new component a replacement for EmbeddableFactoryRenderer with slightly more flexible api: EmbeddableRenderer.
3.1 We can improve it going forward to support more use case
This commit is contained in:
Anton Dosov 2020-06-15 17:13:31 +02:00 committed by GitHub
parent 80ab0d9792
commit 3d0552e03c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 1031 additions and 863 deletions

View file

@ -0,0 +1 @@
Example of using dashboard container embeddable outside of dashboard app

View file

@ -0,0 +1,9 @@
{
"id": "dashboardEmbeddableExamples",
"version": "0.0.1",
"kibanaVersion": "kibana",
"server": false,
"ui": true,
"requiredPlugins": ["embeddable", "embeddableExamples", "dashboard", "developerExamples"],
"optionalPlugins": []
}

View file

@ -0,0 +1,112 @@
/*
* 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 { BrowserRouter as Router, Route, RouteComponentProps, withRouter } from 'react-router-dom';
import {
EuiPage,
EuiPageContent,
EuiPageContentBody,
EuiPageSideBar,
EuiSideNav,
} from '@elastic/eui';
import 'brace/mode/json';
import { AppMountParameters } from '../../../src/core/public';
import { DashboardEmbeddableByValue } from './by_value/embeddable';
import { DashboardStart } from '../../../src/plugins/dashboard/public';
interface PageDef {
title: string;
id: string;
component: React.ReactNode;
}
type NavProps = RouteComponentProps & {
pages: PageDef[];
};
const Nav = withRouter(({ history, pages }: NavProps) => {
const navItems = pages.map((page) => ({
id: page.id,
name: page.title,
onClick: () => history.push(`/${page.id}`),
'data-test-subj': page.id,
}));
return (
<EuiSideNav
items={[
{
name: 'Embeddable explorer',
id: 'home',
items: [...navItems],
},
]}
/>
);
});
interface Props {
basename: string;
DashboardContainerByValueRenderer: DashboardStart['DashboardContainerByValueRenderer'];
}
const DashboardEmbeddableExplorerApp = ({ basename, DashboardContainerByValueRenderer }: Props) => {
const pages: PageDef[] = [
{
title: 'By value dashboard embeddable',
id: 'dashboardEmbeddableByValue',
component: (
<DashboardEmbeddableByValue
DashboardContainerByValueRenderer={DashboardContainerByValueRenderer}
/>
),
},
{
title: 'By ref dashboard embeddable',
id: 'dashboardEmbeddableByRef',
component: <div>TODO: Not implemented, but coming soon...</div>,
},
];
const routes = pages.map((page, i) => (
<Route key={i} path={`/${page.id}`} render={(props) => page.component} />
));
return (
<Router basename={basename}>
<EuiPage>
<EuiPageSideBar>
<Nav pages={pages} />
</EuiPageSideBar>
<EuiPageContent>
<EuiPageContentBody>{routes}</EuiPageContentBody>
</EuiPageContent>
</EuiPage>
</Router>
);
};
export const renderApp = (props: Props, element: AppMountParameters['element']) => {
ReactDOM.render(<DashboardEmbeddableExplorerApp {...props} />, element);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -0,0 +1,120 @@
/*
* 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 { ViewMode } from '../../../../src/plugins/embeddable/public';
import { DashboardContainerInput, DashboardStart } from '../../../../src/plugins/dashboard/public';
import { HELLO_WORLD_EMBEDDABLE } from '../../../embeddable_examples/public/hello_world';
import { InputEditor } from './input_editor';
import { TODO_EMBEDDABLE } from '../../../embeddable_examples/public/todo';
import { TODO_REF_EMBEDDABLE } from '../../../embeddable_examples/public/todo/todo_ref_embeddable';
const initialInput: DashboardContainerInput = {
viewMode: ViewMode.VIEW,
panels: {
'1': {
gridData: {
w: 10,
h: 10,
x: 0,
y: 0,
i: '1',
},
type: HELLO_WORLD_EMBEDDABLE,
explicitInput: {
id: '1',
},
},
'2': {
gridData: {
w: 10,
h: 10,
x: 10,
y: 0,
i: '2',
},
type: HELLO_WORLD_EMBEDDABLE,
explicitInput: {
id: '2',
},
},
'3': {
gridData: {
w: 10,
h: 10,
x: 0,
y: 10,
i: '3',
},
type: TODO_EMBEDDABLE,
explicitInput: {
id: '3',
title: 'Clean up',
task: 'Clean up the code',
icon: 'trash',
},
},
'4': {
gridData: {
w: 10,
h: 10,
x: 10,
y: 10,
i: '4',
},
type: TODO_REF_EMBEDDABLE,
explicitInput: {
id: '4',
savedObjectId: 'sample-todo-saved-object',
},
},
},
isFullScreenMode: false,
filters: [],
useMargins: false,
id: 'random-id',
timeRange: {
to: 'now',
from: 'now-1d',
},
title: 'test',
query: {
query: '',
language: 'lucene',
},
refreshConfig: {
pause: true,
value: 15,
},
};
export const DashboardEmbeddableByValue = ({
DashboardContainerByValueRenderer,
}: {
DashboardContainerByValueRenderer: DashboardStart['DashboardContainerByValueRenderer'];
}) => {
const [input, setInput] = useState(initialInput);
return (
<>
<InputEditor input={input} onSubmit={setInput} />
<DashboardContainerByValueRenderer input={input} onInputUpdated={setInput} />
</>
);
};

View file

@ -0,0 +1,56 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { EuiButton } from '@elastic/eui';
import { JsonEditor } from '../../../../src/plugins/es_ui_shared/public';
export const InputEditor = <T,>(props: { input: T; onSubmit: (value: T) => void }) => {
const input = JSON.stringify(props.input, null, 4);
const [value, setValue] = React.useState(input);
const isValid = (() => {
try {
JSON.parse(value);
return true;
} catch (e) {
return false;
}
})();
React.useEffect(() => {
setValue(input);
}, [input]);
return (
<>
<JsonEditor
value={value}
onUpdate={(v) => setValue(v.data.raw)}
euiCodeEditorProps={{
'data-test-subj': 'dashboardEmbeddableByValueInputEditor',
}}
/>
<EuiButton
onClick={() => props.onSubmit(JSON.parse(value))}
disabled={!isValid}
data-test-subj={'dashboardEmbeddableByValueInputSubmit'}
>
Update Input
</EuiButton>
</>
);
};

View file

@ -17,4 +17,6 @@
* under the License.
*/
export { App } from './app';
import { DashboardEmbeddableExamples } from './plugin';
export const plugin = () => new DashboardEmbeddableExamples();

View file

@ -0,0 +1,64 @@
/*
* 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 { AppMountParameters, AppNavLinkStatus, CoreSetup, Plugin } from '../../../src/core/public';
import { DashboardStart } from '../../../src/plugins/dashboard/public';
import { DeveloperExamplesSetup } from '../../developer_examples/public';
import { EmbeddableExamplesStart } from '../../embeddable_examples/public/plugin';
interface SetupDeps {
developerExamples: DeveloperExamplesSetup;
}
interface StartDeps {
dashboard: DashboardStart;
embeddableExamples: EmbeddableExamplesStart;
}
export class DashboardEmbeddableExamples implements Plugin<void, void, {}, StartDeps> {
public setup(core: CoreSetup<StartDeps>, { developerExamples }: SetupDeps) {
core.application.register({
id: 'dashboardEmbeddableExamples',
title: 'Dashboard embeddable examples',
navLinkStatus: AppNavLinkStatus.hidden,
async mount(params: AppMountParameters) {
const [, depsStart] = await core.getStartServices();
const { renderApp } = await import('./app');
await depsStart.embeddableExamples.createSampleData();
return renderApp(
{
basename: params.appBasePath,
DashboardContainerByValueRenderer:
depsStart.dashboard.DashboardContainerByValueRenderer,
},
params.element
);
},
});
developerExamples.register({
appId: 'dashboardEmbeddableExamples',
title: 'Dashboard Container',
description: `Showcase different ways how to embed dashboard container into your app`,
});
}
public start() {}
public stop() {}
}

View file

@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"../../typings/**/*",
],
"exclude": []
}

View file

@ -22,10 +22,12 @@ import {
IContainer,
EmbeddableInput,
EmbeddableFactoryDefinition,
EmbeddableFactory,
} from '../../../../src/plugins/embeddable/public';
import { HelloWorldEmbeddable, HELLO_WORLD_EMBEDDABLE } from './hello_world_embeddable';
export class HelloWorldEmbeddableFactory implements EmbeddableFactoryDefinition {
export type HelloWorldEmbeddableFactory = EmbeddableFactory;
export class HelloWorldEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition {
public readonly type = HELLO_WORLD_EMBEDDABLE;
/**

View file

@ -20,13 +20,18 @@
export {
HELLO_WORLD_EMBEDDABLE,
HelloWorldEmbeddable,
HelloWorldEmbeddableFactoryDefinition,
HelloWorldEmbeddableFactory,
} from './hello_world';
export { ListContainer, LIST_CONTAINER } from './list_container';
export { TODO_EMBEDDABLE } from './todo';
export { ListContainer, LIST_CONTAINER, ListContainerFactory } from './list_container';
export { TODO_EMBEDDABLE, TodoEmbeddableFactory } from './todo';
import { EmbeddableExamplesPlugin } from './plugin';
export { SearchableListContainer, SEARCHABLE_LIST_CONTAINER } from './searchable_list_container';
export { MULTI_TASK_TODO_EMBEDDABLE } from './multi_task_todo';
export {
SearchableListContainer,
SEARCHABLE_LIST_CONTAINER,
SearchableListContainerFactory,
} from './searchable_list_container';
export { MULTI_TASK_TODO_EMBEDDABLE, MultiTaskTodoEmbeddableFactory } from './multi_task_todo';
export const plugin = () => new EmbeddableExamplesPlugin();

View file

@ -18,4 +18,4 @@
*/
export { ListContainer, LIST_CONTAINER } from './list_container';
export { ListContainerFactory } from './list_container_factory';
export { ListContainerFactoryDefinition, ListContainerFactory } from './list_container_factory';

View file

@ -42,10 +42,10 @@ export class ListContainer extends Container<{}, ContainerInput> {
}
public render(node: HTMLElement) {
this.node = node;
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
this.node = node;
ReactDOM.render(
<ListContainerComponent embeddable={this} embeddableServices={this.embeddableServices} />,
node

View file

@ -22,6 +22,8 @@ import {
EmbeddableFactoryDefinition,
ContainerInput,
EmbeddableStart,
EmbeddableFactory,
ContainerOutput,
} from '../../../../src/plugins/embeddable/public';
import { LIST_CONTAINER, ListContainer } from './list_container';
@ -29,7 +31,9 @@ interface StartServices {
embeddableServices: EmbeddableStart;
}
export class ListContainerFactory implements EmbeddableFactoryDefinition {
export type ListContainerFactory = EmbeddableFactory<ContainerInput, ContainerOutput>;
export class ListContainerFactoryDefinition
implements EmbeddableFactoryDefinition<ContainerInput, ContainerOutput> {
public readonly type = LIST_CONTAINER;
public readonly isContainerType = true;

View file

@ -18,7 +18,11 @@
*/
import { i18n } from '@kbn/i18n';
import { IContainer, EmbeddableFactoryDefinition } from '../../../../src/plugins/embeddable/public';
import {
IContainer,
EmbeddableFactoryDefinition,
EmbeddableFactory,
} from '../../../../src/plugins/embeddable/public';
import {
MultiTaskTodoEmbeddable,
MULTI_TASK_TODO_EMBEDDABLE,
@ -26,8 +30,15 @@ import {
MultiTaskTodoOutput,
} from './multi_task_todo_embeddable';
export class MultiTaskTodoEmbeddableFactory
implements EmbeddableFactoryDefinition<MultiTaskTodoInput, MultiTaskTodoOutput> {
export type MultiTaskTodoEmbeddableFactory = EmbeddableFactory<
MultiTaskTodoInput,
MultiTaskTodoOutput,
MultiTaskTodoEmbeddable
>;
export class MultiTaskTodoEmbeddableFactoryDefinition
implements
EmbeddableFactoryDefinition<MultiTaskTodoInput, MultiTaskTodoOutput, MultiTaskTodoEmbeddable> {
public readonly type = MULTI_TASK_TODO_EMBEDDABLE;
public async isEditable() {

View file

@ -18,23 +18,34 @@
*/
import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public';
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 { CoreSetup, CoreStart, Plugin } from '../../../src/core/public';
import {
HELLO_WORLD_EMBEDDABLE,
HelloWorldEmbeddableFactoryDefinition,
HelloWorldEmbeddableFactory,
} from './hello_world';
import { TODO_EMBEDDABLE, TodoEmbeddableFactory, TodoEmbeddableFactoryDefinition } from './todo';
import {
MULTI_TASK_TODO_EMBEDDABLE,
MultiTaskTodoEmbeddableFactory,
MultiTaskTodoInput,
MultiTaskTodoOutput,
MultiTaskTodoEmbeddableFactoryDefinition,
} from './multi_task_todo';
import {
SEARCHABLE_LIST_CONTAINER,
SearchableListContainerFactoryDefinition,
SearchableListContainerFactory,
} from './searchable_list_container';
import { LIST_CONTAINER, ListContainerFactory } from './list_container';
import {
LIST_CONTAINER,
ListContainerFactoryDefinition,
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';
import { TODO_REF_EMBEDDABLE } from './todo/todo_ref_embeddable';
import {
TodoRefEmbeddableFactory,
TodoRefEmbeddableFactoryDefinition,
} from './todo/todo_ref_embeddable_factory';
export interface EmbeddableExamplesSetupDependencies {
embeddable: EmbeddableSetup;
@ -44,8 +55,18 @@ export interface EmbeddableExamplesStartDependencies {
embeddable: EmbeddableStart;
}
interface ExampleEmbeddableFactories {
getHelloWorldEmbeddableFactory: () => HelloWorldEmbeddableFactory;
getMultiTaskTodoEmbeddableFactory: () => MultiTaskTodoEmbeddableFactory;
getSearchableListContainerEmbeddableFactory: () => SearchableListContainerFactory;
getListContainerEmbeddableFactory: () => ListContainerFactory;
getTodoEmbeddableFactory: () => TodoEmbeddableFactory;
getTodoRefEmbeddableFactory: () => TodoRefEmbeddableFactory;
}
export interface EmbeddableExamplesStart {
createSampleData: () => Promise<void>;
factories: ExampleEmbeddableFactories;
}
export class EmbeddableExamplesPlugin
@ -56,53 +77,59 @@ export class EmbeddableExamplesPlugin
EmbeddableExamplesSetupDependencies,
EmbeddableExamplesStartDependencies
> {
private exampleEmbeddableFactories: Partial<ExampleEmbeddableFactories> = {};
public setup(
core: CoreSetup<EmbeddableExamplesStartDependencies>,
deps: EmbeddableExamplesSetupDependencies
) {
deps.embeddable.registerEmbeddableFactory(
this.exampleEmbeddableFactories.getHelloWorldEmbeddableFactory = deps.embeddable.registerEmbeddableFactory(
HELLO_WORLD_EMBEDDABLE,
new HelloWorldEmbeddableFactory()
new HelloWorldEmbeddableFactoryDefinition()
);
deps.embeddable.registerEmbeddableFactory<MultiTaskTodoInput, MultiTaskTodoOutput>(
this.exampleEmbeddableFactories.getMultiTaskTodoEmbeddableFactory = deps.embeddable.registerEmbeddableFactory(
MULTI_TASK_TODO_EMBEDDABLE,
new MultiTaskTodoEmbeddableFactory()
new MultiTaskTodoEmbeddableFactoryDefinition()
);
deps.embeddable.registerEmbeddableFactory(
this.exampleEmbeddableFactories.getSearchableListContainerEmbeddableFactory = deps.embeddable.registerEmbeddableFactory(
SEARCHABLE_LIST_CONTAINER,
new SearchableListContainerFactory(async () => ({
new SearchableListContainerFactoryDefinition(async () => ({
embeddableServices: (await core.getStartServices())[1].embeddable,
}))
);
deps.embeddable.registerEmbeddableFactory(
this.exampleEmbeddableFactories.getListContainerEmbeddableFactory = deps.embeddable.registerEmbeddableFactory(
LIST_CONTAINER,
new ListContainerFactory(async () => ({
new ListContainerFactoryDefinition(async () => ({
embeddableServices: (await core.getStartServices())[1].embeddable,
}))
);
deps.embeddable.registerEmbeddableFactory<TodoInput, TodoOutput>(
this.exampleEmbeddableFactories.getTodoEmbeddableFactory = deps.embeddable.registerEmbeddableFactory(
TODO_EMBEDDABLE,
new TodoEmbeddableFactory(async () => ({
new TodoEmbeddableFactoryDefinition(async () => ({
openModal: (await core.getStartServices())[0].overlays.openModal,
}))
);
deps.embeddable.registerEmbeddableFactory<TodoRefInput, TodoRefOutput>(
this.exampleEmbeddableFactories.getTodoRefEmbeddableFactory = deps.embeddable.registerEmbeddableFactory(
TODO_REF_EMBEDDABLE,
new TodoRefEmbeddableFactory(async () => ({
new TodoRefEmbeddableFactoryDefinition(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
): EmbeddableExamplesStart {
return {
createSampleData: () => createSampleData(core.savedObjects.client),
factories: this.exampleEmbeddableFactories as ExampleEmbeddableFactories,
};
}

View file

@ -18,4 +18,7 @@
*/
export { SearchableListContainer, SEARCHABLE_LIST_CONTAINER } from './searchable_list_container';
export { SearchableListContainerFactory } from './searchable_list_container_factory';
export {
SearchableListContainerFactoryDefinition,
SearchableListContainerFactory,
} from './searchable_list_container_factory';

View file

@ -19,6 +19,8 @@
import { i18n } from '@kbn/i18n';
import {
ContainerOutput,
EmbeddableFactory,
EmbeddableFactoryDefinition,
EmbeddableStart,
} from '../../../../src/plugins/embeddable/public';
@ -32,7 +34,12 @@ interface StartServices {
embeddableServices: EmbeddableStart;
}
export class SearchableListContainerFactory implements EmbeddableFactoryDefinition {
export type SearchableListContainerFactory = EmbeddableFactory<
SearchableContainerInput,
ContainerOutput
>;
export class SearchableListContainerFactoryDefinition
implements EmbeddableFactoryDefinition<SearchableContainerInput, ContainerOutput> {
public readonly type = SEARCHABLE_LIST_CONTAINER;
public readonly isContainerType = true;

View file

@ -23,7 +23,11 @@ import { OverlayStart } from 'kibana/public';
import { EuiFieldText } from '@elastic/eui';
import { EuiButton } from '@elastic/eui';
import { toMountPoint } from '../../../../src/plugins/kibana_react/public';
import { IContainer, EmbeddableFactoryDefinition } from '../../../../src/plugins/embeddable/public';
import {
IContainer,
EmbeddableFactoryDefinition,
EmbeddableFactory,
} from '../../../../src/plugins/embeddable/public';
import { TodoEmbeddable, TODO_EMBEDDABLE, TodoInput, TodoOutput } from './todo_embeddable';
function TaskInput({ onSave }: { onSave: (task: string) => void }) {
@ -47,7 +51,9 @@ interface StartServices {
openModal: OverlayStart['openModal'];
}
export class TodoEmbeddableFactory
export type TodoEmbeddableFactory = EmbeddableFactory<TodoInput, TodoOutput, TodoEmbeddable>;
export class TodoEmbeddableFactoryDefinition
implements EmbeddableFactoryDefinition<TodoInput, TodoOutput, TodoEmbeddable> {
public readonly type = TODO_EMBEDDABLE;

View file

@ -24,6 +24,7 @@ import {
EmbeddableStart,
ErrorEmbeddable,
EmbeddableFactoryDefinition,
EmbeddableFactory,
} from '../../../../src/plugins/embeddable/public';
import {
TodoRefEmbeddable,
@ -37,7 +38,14 @@ interface StartServices {
savedObjectsClient: SavedObjectsClientContract;
}
export class TodoRefEmbeddableFactory
export type TodoRefEmbeddableFactory = EmbeddableFactory<
TodoRefInput,
TodoRefOutput,
TodoRefEmbeddable,
TodoSavedObjectAttributes
>;
export class TodoRefEmbeddableFactoryDefinition
implements
EmbeddableFactoryDefinition<
TodoRefInput,

View file

@ -38,6 +38,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 { EmbeddableExamplesStart } from '../../embeddable_examples/public/plugin';
interface PageDef {
title: string;
@ -81,43 +82,53 @@ interface Props {
inspector: InspectorStartContract;
savedObject: SavedObjectsStart;
uiSettingsClient: IUiSettingsClient;
embeddableExamples: EmbeddableExamplesStart;
}
const EmbeddableExplorerApp = ({
basename,
navigateToApp,
embeddableApi,
inspector,
uiSettingsClient,
savedObject,
overlays,
uiActionsApi,
notifications,
embeddableExamples,
}: Props) => {
const pages: PageDef[] = [
{
title: 'Hello world embeddable',
id: 'helloWorldEmbeddableSection',
component: (
<HelloWorldEmbeddableExample getEmbeddableFactory={embeddableApi.getEmbeddableFactory} />
<HelloWorldEmbeddableExample
helloWorldEmbeddableFactory={embeddableExamples.factories.getHelloWorldEmbeddableFactory()}
/>
),
},
{
title: 'Todo embeddable',
id: 'todoEmbeddableSection',
component: (
<TodoEmbeddableExample getEmbeddableFactory={embeddableApi.getEmbeddableFactory} />
<TodoEmbeddableExample
todoEmbeddableFactory={embeddableExamples.factories.getTodoEmbeddableFactory()}
/>
),
},
{
title: 'List container embeddable',
id: 'listContainerSection',
component: <ListContainerExample getEmbeddableFactory={embeddableApi.getEmbeddableFactory} />,
component: (
<ListContainerExample
listContainerEmbeddableFactory={embeddableExamples.factories.getListContainerEmbeddableFactory()}
searchableListContainerEmbeddableFactory={embeddableExamples.factories.getSearchableListContainerEmbeddableFactory()}
/>
),
},
{
title: 'Dynamically adding children to a container',
id: 'embeddablePanelExamplae',
component: <EmbeddablePanelExample embeddableServices={embeddableApi} />,
id: 'embeddablePanelExample',
component: (
<EmbeddablePanelExample
embeddableServices={embeddableApi}
searchListContainerFactory={embeddableExamples.factories.getSearchableListContainerEmbeddableFactory()}
/>
),
},
];

View file

@ -34,14 +34,15 @@ import {
HELLO_WORLD_EMBEDDABLE,
TODO_EMBEDDABLE,
MULTI_TASK_TODO_EMBEDDABLE,
SEARCHABLE_LIST_CONTAINER,
SearchableListContainerFactory,
} from '../../embeddable_examples/public';
interface Props {
embeddableServices: EmbeddableStart;
searchListContainerFactory: SearchableListContainerFactory;
}
export function EmbeddablePanelExample({ embeddableServices }: Props) {
export function EmbeddablePanelExample({ embeddableServices, searchListContainerFactory }: Props) {
const searchableInput = {
id: '1',
title: 'My searchable todo list',
@ -81,8 +82,7 @@ export function EmbeddablePanelExample({ embeddableServices }: Props) {
useEffect(() => {
ref.current = true;
if (!embeddable) {
const factory = embeddableServices.getEmbeddableFactory(SEARCHABLE_LIST_CONTAINER);
const promise = factory?.create(searchableInput);
const promise = searchListContainerFactory.create(searchableInput);
if (promise) {
promise.then((e) => {
if (ref.current) {

View file

@ -19,27 +19,26 @@
import React from 'react';
import {
EuiPanel,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageHeader,
EuiPageHeaderSection,
EuiTitle,
EuiPanel,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { EmbeddableRenderer } from '../../../src/plugins/embeddable/public';
import {
EmbeddableStart,
EmbeddableFactoryRenderer,
EmbeddableRoot,
} from '../../../src/plugins/embeddable/public';
import { HelloWorldEmbeddable, HELLO_WORLD_EMBEDDABLE } from '../../embeddable_examples/public';
HelloWorldEmbeddable,
HelloWorldEmbeddableFactory,
} from '../../embeddable_examples/public';
interface Props {
getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
helloWorldEmbeddableFactory: HelloWorldEmbeddableFactory;
}
export function HelloWorldEmbeddableExample({ getEmbeddableFactory }: Props) {
export function HelloWorldEmbeddableExample({ helloWorldEmbeddableFactory }: Props) {
return (
<EuiPageBody>
<EuiPageHeader>
@ -53,28 +52,25 @@ export function HelloWorldEmbeddableExample({ getEmbeddableFactory }: Props) {
<EuiPageContentBody>
<EuiText>
Here the embeddable is rendered without the factory. A developer may use this method if
they want to statically embed a single embeddable into their application or page.
they want to statically embed a single embeddable into their application or page. Also
`input` prop may be used to declaratively update current embeddable input
</EuiText>
<EuiPanel data-test-subj="helloWorldEmbeddablePanel" paddingSize="none" role="figure">
<EmbeddableRoot embeddable={new HelloWorldEmbeddable({ id: 'hello' })} />
<EmbeddableRenderer embeddable={new HelloWorldEmbeddable({ id: 'hello' })} />
</EuiPanel>
<EuiText>
Here the embeddable is rendered using the factory.create method. This method is used
programatically when a container embeddable attempts to initialize it&#39;s children
embeddables. This method can be used when you only have a string representing the type
of embeddable, and input data.
Here the embeddable is rendered using the factory. Internally it creates embeddable
using factory.create(). This method is used programatically when a container embeddable
attempts to initialize it&#39;s children embeddables. This method can be used when you
only have a access to a factory.
</EuiText>
<EuiPanel
data-test-subj="helloWorldEmbeddableFromFactory"
paddingSize="none"
role="figure"
>
<EmbeddableFactoryRenderer
getEmbeddableFactory={getEmbeddableFactory}
type={HELLO_WORLD_EMBEDDABLE}
input={{ id: '1234' }}
/>
<EmbeddableRenderer factory={helloWorldEmbeddableFactory} input={{ id: '1234' }} />
</EuiPanel>
</EuiPageContentBody>
</EuiPageContent>

View file

@ -19,35 +19,39 @@
import React from 'react';
import {
EuiPanel,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageHeader,
EuiPageHeaderSection,
EuiTitle,
EuiPanel,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { EuiSpacer } from '@elastic/eui';
import {
EmbeddableFactoryRenderer,
EmbeddableStart,
EmbeddableInput,
EmbeddableRenderer,
ViewMode,
} from '../../../src/plugins/embeddable/public';
import {
HELLO_WORLD_EMBEDDABLE,
TODO_EMBEDDABLE,
MULTI_TASK_TODO_EMBEDDABLE,
SEARCHABLE_LIST_CONTAINER,
LIST_CONTAINER,
TODO_EMBEDDABLE,
ListContainerFactory,
SearchableListContainerFactory,
} from '../../embeddable_examples/public';
interface Props {
getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
listContainerEmbeddableFactory: ListContainerFactory;
searchableListContainerEmbeddableFactory: SearchableListContainerFactory;
}
export function ListContainerExample({ getEmbeddableFactory }: Props) {
const listInput = {
export function ListContainerExample({
listContainerEmbeddableFactory,
searchableListContainerEmbeddableFactory,
}: Props) {
const listInput: EmbeddableInput = {
id: 'hello',
title: 'My todo list',
viewMode: ViewMode.VIEW,
@ -78,7 +82,7 @@ export function ListContainerExample({ getEmbeddableFactory }: Props) {
},
};
const searchableInput = {
const searchableInput: EmbeddableInput = {
id: '1',
title: 'My searchable todo list',
viewMode: ViewMode.VIEW,
@ -127,11 +131,7 @@ export function ListContainerExample({ getEmbeddableFactory }: Props) {
list.
</EuiText>
<EuiPanel data-test-subj="listContainerEmbeddablePanel" paddingSize="none" role="figure">
<EmbeddableFactoryRenderer
getEmbeddableFactory={getEmbeddableFactory}
type={LIST_CONTAINER}
input={listInput}
/>
<EmbeddableRenderer input={listInput} factory={listContainerEmbeddableFactory} />
</EuiPanel>
<EuiSpacer />
@ -167,10 +167,9 @@ export function ListContainerExample({ getEmbeddableFactory }: Props) {
paddingSize="none"
role="figure"
>
<EmbeddableFactoryRenderer
getEmbeddableFactory={getEmbeddableFactory}
type={SEARCHABLE_LIST_CONTAINER}
<EmbeddableRenderer
input={searchableInput}
factory={searchableListContainerEmbeddableFactory}
/>{' '}
</EuiPanel>
<EuiSpacer />
@ -178,7 +177,7 @@ export function ListContainerExample({ getEmbeddableFactory }: Props) {
<p>
There currently is no formal way to limit what children can be added to a container.
If the use case arose, it wouldn&#39;t be difficult. In the mean time, it&#39;s good
to understand that chilren may ignore input they don&#39;t care about. Likewise the
to understand that children may ignore input they don&#39;t care about. Likewise the
container will have to choose what to do when it encounters children that are missing
certain output variables.
</p>

View file

@ -56,6 +56,7 @@ export class EmbeddableExplorerPlugin implements Plugin<void, void, {}, StartDep
savedObject: coreStart.savedObjects,
overlays: coreStart.overlays,
navigateToApp: coreStart.application.navigateToApp,
embeddableExamples: depsStart.embeddableExamples,
},
params.element
);

View file

@ -19,35 +19,27 @@
import React from 'react';
import {
EuiFormRow,
EuiButton,
EuiFieldText,
EuiPanel,
EuiTextArea,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageHeader,
EuiPageHeaderSection,
EuiTitle,
EuiPanel,
EuiText,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiTextArea,
EuiTitle,
} from '@elastic/eui';
import {
TodoEmbeddable,
TODO_EMBEDDABLE,
TodoInput,
} from '../../../examples/embeddable_examples/public/todo';
import {
EmbeddableStart,
EmbeddableRoot,
EmbeddableOutput,
ErrorEmbeddable,
} from '../../../src/plugins/embeddable/public';
import { TodoInput } from '../../../examples/embeddable_examples/public/todo';
import { TodoEmbeddableFactory } from '../../../examples/embeddable_examples/public';
import { EmbeddableRenderer } from '../../../src/plugins/embeddable/public';
interface Props {
getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
todoEmbeddableFactory: TodoEmbeddableFactory;
}
interface State {
@ -55,50 +47,27 @@ interface State {
title?: string;
icon?: string;
loading: boolean;
input: TodoInput;
}
export class TodoEmbeddableExample extends React.Component<Props, State> {
private embeddable?: TodoEmbeddable | ErrorEmbeddable;
constructor(props: Props) {
super(props);
this.state = { loading: true };
}
public componentDidMount() {
const factory = this.props.getEmbeddableFactory<TodoInput, EmbeddableOutput, TodoEmbeddable>(
TODO_EMBEDDABLE
);
if (factory === undefined) {
throw new Error('Embeddable factory is undefined!');
}
factory
.create({
this.state = {
loading: true,
input: {
id: '1',
task: 'Take out the trash',
icon: 'broom',
title: 'Trash',
})
.then((embeddable) => {
this.embeddable = embeddable;
this.setState({ loading: false });
});
}
public componentWillUnmount() {
if (this.embeddable) {
this.embeddable.destroy();
}
},
};
}
private onUpdateEmbeddableInput = () => {
if (this.embeddable) {
const { task, title, icon } = this.state;
this.embeddable.updateInput({ task, title, icon });
}
const { task, title, icon, input } = this.state;
this.setState({ input: { ...input, task: task ?? '', title, icon } });
};
public render() {
@ -115,20 +84,7 @@ export class TodoEmbeddableExample extends React.Component<Props, State> {
<EuiPageContentBody>
<EuiText>
This embeddable takes input parameters, task, title and icon. You can update them
using this form. In the code, pressing update will call
<pre>
<code>
const &#123; task, title, icon &#125; = this.state;
<br />
this.embeddable.updateInput(&#123; task, title, icon &#125;);
</code>
</pre>
<p>
You may also notice this example uses `EmbeddableRoot` instead of the
`EmbeddableFactoryRenderer` helper component. This is because it needs a reference
to the embeddable to update it, and `EmbeddableFactoryRenderer` creates and holds
the embeddable instance internally.
</p>
using this form. Input changes will be passed inside `EmbeddableRenderer` as a prop
</EuiText>
<EuiFlexGroup>
<EuiFlexItem grow={true}>
@ -169,7 +125,10 @@ export class TodoEmbeddableExample extends React.Component<Props, State> {
</EuiFlexItem>
</EuiFlexGroup>
<EuiPanel data-test-subj="todoEmbeddable" paddingSize="none" role="figure">
<EmbeddableRoot embeddable={this.embeddable} loading={this.state.loading} />
<EmbeddableRenderer
factory={this.props.todoEmbeddableFactory}
input={this.state.input}
/>
</EuiPanel>
</EuiPageContentBody>
</EuiPageContent>

View file

@ -391,6 +391,7 @@
"@types/supertest-as-promised": "^2.0.38",
"@types/testing-library__react": "^9.1.2",
"@types/testing-library__react-hooks": "^3.1.0",
"@types/testing-library__dom": "^6.10.0",
"@types/type-detect": "^4.0.1",
"@types/uuid": "^3.4.4",
"@types/vinyl-fs": "^2.4.11",

View file

@ -66,6 +66,14 @@
'@types/reach__router',
],
},
{
groupSlug: '@testing-library/dom',
groupName: '@testing-library/dom related packages',
packageNames: [
'@testing-library/dom',
'@types/testing-library__dom',
],
},
{
groupSlug: 'angular',
groupName: 'angular related packages',

View file

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

View file

@ -17,26 +17,21 @@
* under the License.
*/
export default function ({ getService }) {
const testSubjects = getService('testSubjects');
const pieChart = getService('pieChart');
const dashboardExpect = getService('dashboardExpect');
import * as React from 'react';
import { DashboardContainerInput } from './dashboard_container';
import { DashboardContainerFactory } from './dashboard_container_factory';
import { EmbeddableRenderer } from '../../../../embeddable/public';
describe('dashboard container', () => {
before(async () => {
await testSubjects.click('embedExplorerTab-dashboardContainer');
});
it('pie charts', async () => {
await pieChart.expectPieSliceCount(5);
});
it('markdown', async () => {
await dashboardExpect.markdownWithValuesExists(["I'm a markdown!"]);
});
it('saved search', async () => {
await dashboardExpect.savedSearchRowCount(50);
});
});
interface Props {
input: DashboardContainerInput;
onInputUpdated?: (newInput: DashboardContainerInput) => void;
// TODO: add other props as needed
}
export const createDashboardContainerByValueRenderer = ({
factory,
}: {
factory: DashboardContainerFactory;
}): React.FC<Props> => (props: Props) => (
<EmbeddableRenderer input={props.input} onInputUpdated={props.onInputUpdated} factory={factory} />
);

View file

@ -19,9 +19,9 @@
import { i18n } from '@kbn/i18n';
import { UiActionsStart } from 'src/plugins/ui_actions/public';
import { EmbeddableStart } from 'src/plugins/embeddable/public';
import { CoreStart } from 'src/core/public';
import { Start as InspectorStartContract } from 'src/plugins/inspector/public';
import { EmbeddableFactory, EmbeddableStart } from '../../../../embeddable/public';
import {
ContainerOutput,
EmbeddableFactoryDefinition,
@ -43,7 +43,12 @@ interface StartServices {
uiActions: UiActionsStart;
}
export class DashboardContainerFactory
export type DashboardContainerFactory = EmbeddableFactory<
DashboardContainerInput,
ContainerOutput,
DashboardContainer
>;
export class DashboardContainerFactoryDefinition
implements
EmbeddableFactoryDefinition<DashboardContainerInput, ContainerOutput, DashboardContainer> {
public readonly isContainerType = true;

View file

@ -17,7 +17,10 @@
* under the License.
*/
export { DashboardContainerFactory } from './dashboard_container_factory';
export {
DashboardContainerFactoryDefinition,
DashboardContainerFactory,
} from './dashboard_container_factory';
export { DashboardContainer, DashboardContainerInput } from './dashboard_container';
export { createPanelState } from './panel';
@ -29,3 +32,5 @@ export {
DEFAULT_PANEL_WIDTH,
DASHBOARD_CONTAINER_TYPE,
} from './dashboard_constants';
export { createDashboardContainerByValueRenderer } from './dashboard_container_by_value_renderer';

View file

@ -23,7 +23,7 @@ import { DashboardPlugin } from './plugin';
export {
DashboardContainer,
DashboardContainerInput,
DashboardContainerFactory,
DashboardContainerFactoryDefinition,
DASHBOARD_CONTAINER_TYPE,
// Types below here can likely be made private when dashboard app moved into this NP plugin.
DEFAULT_PANEL_WIDTH,

View file

@ -25,17 +25,17 @@ import { i18n } from '@kbn/i18n';
import {
App,
AppMountParameters,
AppUpdater,
CoreSetup,
CoreStart,
PluginInitializerContext,
Plugin,
PluginInitializerContext,
SavedObjectsClientContract,
AppUpdater,
ScopedHistory,
} from 'src/core/public';
import { UsageCollectionSetup } from '../../usage_collection/public';
import { CONTEXT_MENU_TRIGGER, EmbeddableSetup, EmbeddableStart } from '../../embeddable/public';
import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public';
import { DataPublicPluginSetup, DataPublicPluginStart, esFilters } from '../../data/public';
import { SharePluginSetup, SharePluginStart, UrlGeneratorContract } from '../../share/public';
import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public';
@ -52,35 +52,38 @@ import {
} from '../../kibana_react/public';
import { createKbnUrlTracker, Storage } from '../../kibana_utils/public';
import {
initAngularBootstrap,
KibanaLegacySetup,
KibanaLegacyStart,
initAngularBootstrap,
} from '../../kibana_legacy/public';
import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../../plugins/home/public';
import { DEFAULT_APP_CATEGORIES } from '../../../core/public';
import {
DashboardContainerFactory,
ExpandPanelAction,
ExpandPanelActionContext,
ReplacePanelAction,
ReplacePanelActionContext,
ClonePanelAction,
ClonePanelActionContext,
ACTION_CLONE_PANEL,
ACTION_EXPAND_PANEL,
ACTION_REPLACE_PANEL,
ClonePanelAction,
ClonePanelActionContext,
DASHBOARD_CONTAINER_TYPE,
DashboardContainerFactory,
DashboardContainerFactoryDefinition,
ExpandPanelAction,
ExpandPanelActionContext,
RenderDeps,
ACTION_CLONE_PANEL,
ReplacePanelAction,
ReplacePanelActionContext,
} from './application';
import {
DashboardAppLinkGeneratorState,
DASHBOARD_APP_URL_GENERATOR,
createDashboardUrlGenerator,
DASHBOARD_APP_URL_GENERATOR,
DashboardAppLinkGeneratorState,
} from './url_generator';
import { createSavedDashboardLoader } from './saved_dashboards';
import { DashboardConstants } from './dashboard_constants';
import { addEmbeddableToDashboardUrl } from './url_utils/url_helper';
import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder';
import { createDashboardContainerByValueRenderer } from './application';
declare module '../../share/public' {
export interface UrlGeneratorStateMapping {
@ -121,6 +124,7 @@ export interface DashboardStart {
embeddableType: string;
}) => void | undefined;
dashboardUrlGenerator?: DashboardUrlGenerator;
DashboardContainerByValueRenderer: ReturnType<typeof createDashboardContainerByValueRenderer>;
}
declare module '../../../plugins/ui_actions/public' {
@ -202,7 +206,7 @@ export class DashboardPlugin
};
};
const factory = new DashboardContainerFactory(getStartServices);
const factory = new DashboardContainerFactoryDefinition(getStartServices);
embeddable.registerEmbeddableFactory(factory.type, factory);
const placeholderFactory = new PlaceholderEmbeddableFactory();
@ -388,10 +392,17 @@ export class DashboardPlugin
chrome: core.chrome,
overlays: core.overlays,
});
const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory(
DASHBOARD_CONTAINER_TYPE
)! as DashboardContainerFactory;
return {
getSavedDashboardLoader: () => savedDashboardLoader,
addEmbeddableToDashboard: this.addEmbeddableToDashboard.bind(this, core),
dashboardUrlGenerator: this.dashboardUrlGenerator,
DashboardContainerByValueRenderer: createDashboardContainerByValueRenderer({
factory: dashboardContainerFactory,
}),
};
}

View file

@ -1,6 +1,6 @@
# Embeddables
Embeddables are re-usable widgets that can be rendered in any environment or plugin. Developers can embed them directly in their plugin. End users can dynamically add them to any embeddable _containers_.
Embeddables are re-usable widgets that can be rendered in any environment or plugin. Developers can embed them directly in their plugin. End users can dynamically add them to any embeddable _containers_.
## Embeddable containers
@ -16,6 +16,12 @@ yarn start --run-examples
and navigate to the Embeddable explorer app.
There is also an example of rendering dashboard container outside of dashboard app [here](https://github.com/elastic/kibana/tree/master/examples/dashboard_embeddable_examples).
## Docs
[Embeddable docs, guides & caveats](./docs/README.md)
## Testing
Run unit tests

View file

@ -0,0 +1,5 @@
# Embeddable Docs, Guides & Caveats
## Reference
- [Embeddable containers and inherited input state](./containers_and_inherited_state.md)

View file

@ -0,0 +1,33 @@
## Embeddable containers and inherited input state
`updateInput` is typed as `updateInput(input: Partial<EmbeddableInput>)`. Notice it's _partial_. This is to support the use case of inherited state when an embeddable is inside a container.
If you are simply rendering an embeddable, it's no problem to do something like:
```ts
// Notice this isn't a partial so it'll be the entire state.
const input: EmbeddableInput = this.state.input
embeddable.updateInput(input);
```
However when you are dealing with _containers_, you want to be sure to **only pass into `updateInput` the actual state that changed**. This is because calling `child.updateInput({ foo })` will make `foo` _explicit_ state. It cannot be inherited from it's parent.
For example, on a dashboard, the time range is _inherited_ by all children, _unless_ they had their time range set explicitly. This is how "per panel time range" works. That action calls `embeddable.updateInput({ timeRange })`, and the time range will no longer be inherited from the container.
### Why is this important?
A common mistake is always passing in the full state. If you do this, all of a sudden you will lose the inheritance of the container state.
**Don't do**
```ts
// Doing this will make it so this embeddable inherits _nothing_ from its container. No more time range updates
// when the user updates the dashboard time range!
embeddable.updateInput({ ...embeddable.getInput(), foo: 'bar' });
```
**Do**
```ts
embeddable.updateInput({ foo: 'bar' });
```

View file

@ -43,7 +43,6 @@ export {
EmbeddableFactory,
EmbeddableFactoryDefinition,
EmbeddableFactoryNotFoundError,
EmbeddableFactoryRenderer,
EmbeddableInput,
EmbeddableInstanceConfiguration,
EmbeddableOutput,
@ -70,6 +69,8 @@ export {
isSavedObjectEmbeddableInput,
isRangeSelectTriggerContext,
isValueClickTriggerContext,
EmbeddableRenderer,
EmbeddableRendererProps,
} from './lib';
export function plugin(initializerContext: PluginInitializerContext) {

View file

@ -1,71 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import {
HELLO_WORLD_EMBEDDABLE,
HelloWorldEmbeddableFactory,
} from '../../../../../../examples/embeddable_examples/public';
import { EmbeddableFactoryRenderer } from './embeddable_factory_renderer';
import { mount } from 'enzyme';
import { nextTick } from 'test_utils/enzyme_helpers';
// @ts-ignore
import { findTestSubject } from '@elastic/eui/lib/test';
import { embeddablePluginMock } from '../../mocks';
test('EmbeddableFactoryRenderer renders an embeddable', async () => {
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory());
const getEmbeddableFactory = doStart().getEmbeddableFactory;
const component = mount(
<EmbeddableFactoryRenderer
getEmbeddableFactory={getEmbeddableFactory}
type={HELLO_WORLD_EMBEDDABLE}
input={{ id: '123' }}
/>
);
await nextTick();
component.update();
// Due to the way embeddables mount themselves on the dom node, they are not forced to be
// react components, and hence, we can't use the usual
// findTestSubject(component, 'subjIdHere');
expect(
component.getDOMNode().querySelectorAll('[data-test-subj="helloWorldEmbeddable"]').length
).toBe(1);
});
test('EmbeddableRoot renders an error if the type does not exist', async () => {
const getEmbeddableFactory = (id: string) => undefined;
const component = mount(
<EmbeddableFactoryRenderer
getEmbeddableFactory={getEmbeddableFactory}
type={HELLO_WORLD_EMBEDDABLE}
input={{ id: '123' }}
/>
);
await nextTick();
component.update();
expect(findTestSubject(component, 'embedSpinner').length).toBe(0);
expect(findTestSubject(component, 'embedError').length).toBe(1);
});

View file

@ -1,80 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { IEmbeddable, EmbeddableInput } from './i_embeddable';
import { EmbeddableRoot } from './embeddable_root';
import { EmbeddableStart } from '../../plugin';
interface Props {
type: string;
getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
input: EmbeddableInput;
}
interface State {
loading: boolean;
error?: string;
}
export class EmbeddableFactoryRenderer extends React.Component<Props, State> {
private embeddable?: IEmbeddable;
constructor(props: Props) {
super(props);
this.state = {
loading: true,
error: undefined,
};
}
public componentDidMount() {
const factory = this.props.getEmbeddableFactory(this.props.type);
if (factory === undefined) {
this.setState({
loading: false,
error: i18n.translate('embeddableApi.errors.factoryDoesNotExist', {
defaultMessage:
'Embeddable factory of {type} does not exist. Ensure all neccessary plugins are installed and enabled.',
values: {
type: this.props.type,
},
}),
});
return;
}
factory.create(this.props.input).then((embeddable) => {
this.embeddable = embeddable;
this.setState({ loading: false });
});
}
public render() {
return (
<EmbeddableRoot
embeddable={this.embeddable}
loading={this.state.loading}
error={this.state.error}
/>
);
}
}

View file

@ -0,0 +1,56 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { wait } from '@testing-library/dom';
import { cleanup, render } from '@testing-library/react/pure';
import '@testing-library/jest-dom/extend-expect';
import {
HelloWorldEmbeddable,
HelloWorldEmbeddableFactoryDefinition,
HELLO_WORLD_EMBEDDABLE,
} from '../../../../../../examples/embeddable_examples/public/hello_world';
import { EmbeddableRenderer } from './embeddable_renderer';
import { embeddablePluginMock } from '../../mocks';
describe('<EmbeddableRenderer/>', () => {
afterEach(cleanup);
test('Render embeddable', () => {
const embeddable = new HelloWorldEmbeddable({ id: 'hello' });
const { getByTestId } = render(<EmbeddableRenderer embeddable={embeddable} />);
expect(getByTestId('helloWorldEmbeddable')).toBeInTheDocument();
});
test('Render factory', async () => {
const { setup, doStart } = embeddablePluginMock.createInstance();
const getFactory = setup.registerEmbeddableFactory(
HELLO_WORLD_EMBEDDABLE,
new HelloWorldEmbeddableFactoryDefinition()
);
doStart();
const { getByTestId, queryByTestId } = render(
<EmbeddableRenderer factory={getFactory()} input={{ id: 'hello' }} />
);
expect(getByTestId('embedSpinner')).toBeInTheDocument();
await wait(() => !queryByTestId('embedSpinner')); // wait until spinner disappears
expect(getByTestId('helloWorldEmbeddable')).toBeInTheDocument();
});
});

View file

@ -0,0 +1,164 @@
/*
* 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, { useEffect, useState } from 'react';
import { EmbeddableInput, IEmbeddable } from './i_embeddable';
import { EmbeddableRoot } from './embeddable_root';
import { EmbeddableFactory } from './embeddable_factory';
import { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable';
/**
* This type is a publicly exposed props of {@link EmbeddableRenderer}
* Union is used to validate that or factory or embeddable is passed in, but it can't be both simultaneously
* In case when embeddable is passed in, input is optional, because there is already an input inside of embeddable object
* In case when factory is used, then input is required, because it will be used as initial input to create an embeddable object
*/
export type EmbeddableRendererProps<I extends EmbeddableInput> =
| EmbeddableRendererPropsWithEmbeddable<I>
| EmbeddableRendererWithFactory<I>;
interface EmbeddableRendererPropsWithEmbeddable<I extends EmbeddableInput> {
input?: I;
onInputUpdated?: (newInput: I) => void;
embeddable: IEmbeddable<I>;
}
function isWithEmbeddable<I extends EmbeddableInput>(
props: EmbeddableRendererProps<I>
): props is EmbeddableRendererPropsWithEmbeddable<I> {
return 'embeddable' in props;
}
interface EmbeddableRendererWithFactory<I extends EmbeddableInput> {
input: I;
onInputUpdated?: (newInput: I) => void;
factory: EmbeddableFactory<I>;
}
function isWithFactory<I extends EmbeddableInput>(
props: EmbeddableRendererProps<I>
): props is EmbeddableRendererWithFactory<I> {
return 'factory' in props;
}
/**
* Helper react component to render an embeddable
* Can be used if you have an embeddable object or an embeddable factory
* Supports updating input by passing `input` prop
*
* @remarks
* This component shouldn't be used inside an embeddable container to render embeddable children
* because children may lose inherited input, here is why:
*
* When passing `input` inside a prop, internally there is a call:
*
* ```ts
* embeddable.updateInput(input);
* ```
* If you are simply rendering an embeddable, it's no problem.
*
* However when you are dealing with containers,
* you want to be sure to only pass into updateInput the actual state that changed.
* This is because calling child.updateInput({ foo }) will make foo explicit state.
* It cannot be inherited from it's parent.
*
* For example, on a dashboard, the time range is inherited by all children,
* unless they had their time range set explicitly.
* This is how "per panel time range" works.
* That action calls embeddable.updateInput({ timeRange }),
* and the time range will no longer be inherited from the container.
*
* see: https://github.com/elastic/kibana/pull/67783#discussion_r435447657 for more details.
* refer to: examples/embeddable_explorer for examples with correct usage of this component.
*
* @public
* @param props - {@link EmbeddableRendererProps}
*/
export const EmbeddableRenderer = <I extends EmbeddableInput>(
props: EmbeddableRendererProps<I>
) => {
const { input, onInputUpdated } = props;
const [embeddable, setEmbeddable] = useState<IEmbeddable<I> | ErrorEmbeddable | undefined>(
isWithEmbeddable(props) ? props.embeddable : undefined
);
const [loading, setLoading] = useState<boolean>(!isWithEmbeddable(props));
const [error, setError] = useState<string | undefined>();
const latestInput = React.useRef(props.input);
useEffect(() => {
latestInput.current = input;
}, [input]);
const factoryFromProps = isWithFactory(props) ? props.factory : undefined;
const embeddableFromProps = isWithEmbeddable(props) ? props.embeddable : undefined;
useEffect(() => {
let canceled = false;
if (embeddableFromProps) {
setEmbeddable(embeddableFromProps);
return;
}
// keeping track of embeddables created by this component to be able to destroy them
let createdEmbeddableRef: IEmbeddable | ErrorEmbeddable | undefined;
if (factoryFromProps) {
setEmbeddable(undefined);
setLoading(true);
factoryFromProps
.create(latestInput.current!)
.then((createdEmbeddable) => {
if (canceled) {
if (createdEmbeddable) {
createdEmbeddable.destroy();
}
} else {
createdEmbeddableRef = createdEmbeddable;
setEmbeddable(createdEmbeddable);
}
})
.catch((err) => {
if (canceled) return;
setError(err?.message);
})
.finally(() => {
if (canceled) return;
setLoading(false);
});
}
return () => {
canceled = true;
if (createdEmbeddableRef) {
createdEmbeddableRef.destroy();
}
};
}, [factoryFromProps, embeddableFromProps]);
useEffect(() => {
if (!embeddable) return;
if (isErrorEmbeddable(embeddable)) return;
if (!onInputUpdated) return;
const sub = embeddable.getInput$().subscribe((newInput) => {
onInputUpdated(newInput);
});
return () => {
sub.unsubscribe();
};
}, [embeddable, onInputUpdated]);
return <EmbeddableRoot embeddable={embeddable} loading={loading} error={error} input={input} />;
};

View file

@ -36,6 +36,15 @@ test('EmbeddableRoot renders an embeddable', async () => {
expect(findTestSubject(component, 'embedError').length).toBe(0);
});
test('EmbeddableRoot updates input', async () => {
const embeddable = new HelloWorldEmbeddable({ id: 'hello' });
const component = mount(<EmbeddableRoot embeddable={embeddable} />);
const spy = jest.spyOn(embeddable, 'updateInput');
const newInput = { id: 'hello', something: 'new' };
component.setProps({ embeddable, input: newInput });
expect(spy).toHaveBeenCalledWith(newInput);
});
test('EmbeddableRoot renders a spinner if loading an no embeddable given', async () => {
const component = mount(<EmbeddableRoot loading={true} />);
// Due to the way embeddables mount themselves on the dom node, they are not forced to be

View file

@ -20,12 +20,13 @@
import React from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { EuiText } from '@elastic/eui';
import { IEmbeddable } from './i_embeddable';
import { EmbeddableInput, IEmbeddable } from './i_embeddable';
interface Props {
embeddable?: IEmbeddable;
loading?: boolean;
error?: string;
input?: EmbeddableInput;
}
export class EmbeddableRoot extends React.Component<Props> {
@ -45,10 +46,24 @@ export class EmbeddableRoot extends React.Component<Props> {
}
}
public componentDidUpdate() {
public componentDidUpdate(prevProps?: Props) {
let justRendered = false;
if (this.root && this.root.current && this.props.embeddable && !this.alreadyMounted) {
this.alreadyMounted = true;
this.props.embeddable.render(this.root.current);
justRendered = true;
}
if (
!justRendered &&
this.root &&
this.root.current &&
this.props.embeddable &&
this.alreadyMounted &&
this.props.input &&
prevProps?.input !== this.props.input
) {
this.props.embeddable.updateInput(this.props.input);
}
}
@ -57,7 +72,8 @@ export class EmbeddableRoot extends React.Component<Props> {
newProps.error !== this.props.error ||
newProps.loading !== this.props.loading ||
newProps.embeddable !== this.props.embeddable ||
(this.root && this.root.current && newProps.embeddable && !this.alreadyMounted)
(this.root && this.root.current && newProps.embeddable && !this.alreadyMounted) ||
newProps.input !== this.props.input
);
}

View file

@ -23,6 +23,6 @@ export * from './embeddable_factory_definition';
export * from './default_embeddable_factory_provider';
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 { EmbeddableRenderer, EmbeddableRendererProps } from './embeddable_renderer';

View file

@ -43,10 +43,14 @@ export interface EmbeddableStartDependencies {
}
export interface EmbeddableSetup {
registerEmbeddableFactory: <I extends EmbeddableInput, O extends EmbeddableOutput>(
registerEmbeddableFactory: <
I extends EmbeddableInput,
O extends EmbeddableOutput,
E extends IEmbeddable<I, O> = IEmbeddable<I, O>
>(
id: string,
factory: EmbeddableFactoryDefinition<I, O>
) => void;
factory: EmbeddableFactoryDefinition<I, O, E>
) => () => EmbeddableFactory<I, O, E>;
setCustomEmbeddableFactoryProvider: (customProvider: EmbeddableFactoryProvider) => void;
}
@ -69,6 +73,7 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
> = new Map();
private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map();
private customEmbeddableFactoryProvider?: EmbeddableFactoryProvider;
private isRegistryReady = false;
constructor(initializerContext: PluginInitializerContext) {}
@ -100,6 +105,8 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
: defaultEmbeddableFactoryProvider(def)
);
});
this.isRegistryReady = true;
return {
getEmbeddableFactory: this.getEmbeddableFactory,
getEmbeddableFactories: this.getEmbeddableFactories,
@ -133,16 +140,24 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
return this.embeddableFactories.values();
};
private registerEmbeddableFactory = (
private registerEmbeddableFactory = <
I extends EmbeddableInput = EmbeddableInput,
O extends EmbeddableOutput = EmbeddableOutput,
E extends IEmbeddable<I, O> = IEmbeddable<I, O>
>(
embeddableFactoryId: string,
factory: EmbeddableFactoryDefinition
) => {
factory: EmbeddableFactoryDefinition<I, O, E>
): (() => EmbeddableFactory<I, O, E>) => {
if (this.embeddableFactoryDefinitions.has(embeddableFactoryId)) {
throw new Error(
`Embeddable factory [embeddableFactoryId = ${embeddableFactoryId}] already registered in Embeddables API.`
);
}
this.embeddableFactoryDefinitions.set(embeddableFactoryId, factory);
return () => {
return this.getEmbeddableFactory(embeddableFactoryId);
};
};
private getEmbeddableFactory = <
@ -152,6 +167,9 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
>(
embeddableFactoryId: string
): EmbeddableFactory<I, O, E> => {
if (!this.isRegistryReady) {
throw new Error('Embeddable factories can only be retrieved after setup lifecycle.');
}
this.ensureFactoryExists(embeddableFactoryId);
const factory = this.embeddableFactories.get(embeddableFactoryId);

View file

@ -31,7 +31,7 @@ import { CONTACT_CARD_EMBEDDABLE } from '../lib/test_samples/embeddables/contact
import { SlowContactCardEmbeddableFactory } from '../lib/test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory';
import {
HELLO_WORLD_EMBEDDABLE,
HelloWorldEmbeddableFactory,
HelloWorldEmbeddableFactoryDefinition,
} from '../../../../../examples/embeddable_examples/public';
import { HelloWorldContainer } from '../lib/test_samples/embeddables/hello_world_container';
import {
@ -61,7 +61,7 @@ async function creatHelloWorldContainerAndEmbeddable(
const slowContactCardFactory = new SlowContactCardEmbeddableFactory({
execAction: uiActions.executeTriggerActions,
});
const helloWorldFactory = new HelloWorldEmbeddableFactory();
const helloWorldFactory = new HelloWorldEmbeddableFactoryDefinition();
setup.registerEmbeddableFactory(filterableFactory.type, filterableFactory);
setup.registerEmbeddableFactory(slowContactCardFactory.type, slowContactCardFactory);
@ -733,7 +733,7 @@ test('untilEmbeddableLoaded() resolves if child is loaded in the container', asy
coreMock.createSetup(),
coreMock.createStart()
);
const factory = new HelloWorldEmbeddableFactory();
const factory = new HelloWorldEmbeddableFactoryDefinition();
setup.registerEmbeddableFactory(factory.type, factory);
const start = doStart();
const testPanel = createEmbeddablePanelMock({

View file

@ -28,7 +28,7 @@ import { CONTACT_CARD_EMBEDDABLE } from '../lib/test_samples/embeddables/contact
import { SlowContactCardEmbeddableFactory } from '../lib/test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory';
import {
HELLO_WORLD_EMBEDDABLE,
HelloWorldEmbeddableFactory,
HelloWorldEmbeddableFactoryDefinition,
} from '../../../../../examples/embeddable_examples/public';
import { FilterableContainer } from '../lib/test_samples/embeddables/filterable_container';
import { isErrorEmbeddable } from '../lib';
@ -49,7 +49,10 @@ const factory = new SlowContactCardEmbeddableFactory({
execAction: uiActions.executeTriggerActions,
});
setup.registerEmbeddableFactory(CONTACT_CARD_EMBEDDABLE, factory);
setup.registerEmbeddableFactory(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory());
setup.registerEmbeddableFactory(
HELLO_WORLD_EMBEDDABLE,
new HelloWorldEmbeddableFactoryDefinition()
);
const start = doStart();

View file

@ -27,7 +27,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
describe('creating and adding children', () => {
before(async () => {
await testSubjects.click('embeddablePanelExamplae');
await testSubjects.click('embeddablePanelExample');
});
it('Can create a new child', async () => {

View file

@ -17,10 +17,9 @@
* under the License.
*/
import { ViewMode, CONTACT_CARD_EMBEDDABLE, HELLO_WORLD_EMBEDDABLE } from '../embeddable_api';
import { DashboardContainerInput } from '../../../../../../src/plugins/dashboard/public';
import { PluginFunctionalProviderContext } from 'test/plugin_functional/services';
export const dashboardInput: DashboardContainerInput = {
export const testDashboardInput = {
panels: {
'1': {
gridData: {
@ -30,25 +29,11 @@ export const dashboardInput: DashboardContainerInput = {
y: 15,
i: '1',
},
type: HELLO_WORLD_EMBEDDABLE,
type: 'HELLO_WORLD_EMBEDDABLE',
explicitInput: {
id: '1',
},
},
'2': {
gridData: {
w: 24,
h: 15,
x: 24,
y: 15,
i: '2',
},
type: CONTACT_CARD_EMBEDDABLE,
explicitInput: {
id: '2',
firstName: 'Sue',
},
},
'822cd0f0-ce7c-419d-aeaa-1171cf452745': {
gridData: {
w: 24,
@ -110,8 +95,55 @@ export const dashboardInput: DashboardContainerInput = {
value: 0,
pause: true,
},
viewMode: ViewMode.EDIT,
viewMode: 'edit',
lastReloadRequestTime: 1556569306103,
title: 'New Dashboard',
description: '',
};
// eslint-disable-next-line import/no-default-export
export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) {
const esArchiver = getService('esArchiver');
const testSubjects = getService('testSubjects');
const pieChart = getService('pieChart');
const browser = getService('browser');
const dashboardExpect = getService('dashboardExpect');
const PageObjects = getPageObjects(['common']);
describe('dashboard container', () => {
before(async () => {
await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/dashboard/current/data');
await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/dashboard/current/kibana');
await PageObjects.common.navigateToApp('dashboardEmbeddableExamples');
await testSubjects.click('dashboardEmbeddableByValue');
await updateInput(JSON.stringify(testDashboardInput, null, 4));
});
it('pie charts', async () => {
await pieChart.expectPieSliceCount(5);
});
it('markdown', async () => {
await dashboardExpect.markdownWithValuesExists(["I'm a markdown!"]);
});
it('saved search', async () => {
await dashboardExpect.savedSearchRowCount(50);
});
});
async function updateInput(input: string) {
const editorWrapper = await (
await testSubjects.find('dashboardEmbeddableByValueInputEditor')
).findByClassName('ace_editor');
const editorId = await editorWrapper.getAttribute('id');
await browser.execute(
(_editorId: string, _input: string) => {
return (window as any).ace.edit(_editorId).setValue(_input);
},
editorId,
input
);
await testSubjects.click('dashboardEmbeddableByValueInputSubmit');
}
}

View file

@ -38,5 +38,6 @@ export default function ({
loadTestFile(require.resolve('./todo_embeddable'));
loadTestFile(require.resolve('./list_container'));
loadTestFile(require.resolve('./adding_children'));
loadTestFile(require.resolve('./dashboard'));
});
}

View file

@ -34,7 +34,6 @@ export default async function ({ readConfigFile }) {
testFiles: [
require.resolve('./test_suites/custom_visualizations'),
require.resolve('./test_suites/panel_actions'),
require.resolve('./test_suites/embeddable_explorer'),
require.resolve('./test_suites/core_plugins'),
require.resolve('./test_suites/management'),
require.resolve('./test_suites/doc_views'),

View file

@ -1,17 +0,0 @@
## Embeddable Explorer
This is a functionally tested set of Embeddable API examples.
To get started, run
```
> yarn es snapshot
```
in one terminal, and in another:
```
> yarn start --plugin-path test/plugin_functional/plugins/kbn_tp_embeddable_explorer/
```
Then open up Kibana, navigate to the embeddable explorer app, and have fun!

View file

@ -1,16 +0,0 @@
{
"id": "kbn_tp_embeddable_explorer",
"version": "0.0.1",
"kibanaVersion": "kibana",
"requiredPlugins": [
"visTypeMarkdown",
"visTypeVislib",
"data",
"embeddable",
"uiActions",
"inspector",
"discover"
],
"server": false,
"ui": true
}

View file

@ -1,22 +0,0 @@
{
"name": "kbn_tp_embeddable_explorer",
"version": "1.0.0",
"main": "target/test/plugin_functional/plugins/kbn_tp_embeddable_explorer",
"kibana": {
"version": "kibana",
"templateVersion": "1.0.0"
},
"license": "Apache-2.0",
"dependencies": {
"@elastic/eui": "24.1.0",
"react": "^16.12.0"
},
"scripts": {
"kbn": "node ../../../../scripts/kbn.js",
"build": "rm -rf './target' && tsc"
},
"devDependencies": {
"@kbn/plugin-helpers": "9.0.2",
"typescript": "3.9.5"
}
}

View file

@ -1,21 +0,0 @@
## Embeddable Explorer
These are functionally tested examples for how to build embeddables. In order
to see them render, start the functional test server in one terminal:
```
node scripts/functional_tests_server --config test/plugin_functional/config
```
and be sure to load the data
```
node scripts/es_archiver.js load dashboard/current/data
node scripts/es_archiver.js load dashboard/current/kibana
```
alternatively you can run them via
```
yarn start --plugin-path test/plugin_functional/plugins/kbn_tp_embeddable_explorer
```

View file

@ -1,79 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { EuiTab } from '@elastic/eui';
import React, { Component } from 'react';
import { EmbeddableStart } from 'src/plugins/embeddable/public';
import { DashboardContainerExample } from './dashboard_container_example';
export interface AppProps {
embeddableServices: EmbeddableStart;
}
export class App extends Component<AppProps, { selectedTabId: string }> {
private tabs: Array<{ id: string; name: string }>;
constructor(props: AppProps) {
super(props);
this.tabs = [
{
id: 'dashboardContainer',
name: 'Dashboard Container',
},
];
this.state = {
selectedTabId: 'helloWorldContainer',
};
}
public onSelectedTabChanged = (id: string) => {
this.setState({
selectedTabId: id,
});
};
public renderTabs() {
return this.tabs.map((tab: { id: string; name: string }, index: number) => (
<EuiTab
onClick={() => this.onSelectedTabChanged(tab.id)}
isSelected={tab.id === this.state.selectedTabId}
key={index}
data-test-subj={`embedExplorerTab-${tab.id}`}
>
{tab.name}
</EuiTab>
));
}
public render() {
return (
<div id="dashboardViewport" style={{ flex: '1', display: 'flex', flexDirection: 'column' }}>
<div>{this.renderTabs()}</div>
{this.getContentsForTab()}
</div>
);
}
private getContentsForTab() {
switch (this.state.selectedTabId) {
case 'dashboardContainer': {
return <DashboardContainerExample embeddableServices={this.props.embeddableServices} />;
}
}
}
}

View file

@ -1,102 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { EuiButton, EuiLoadingChart } from '@elastic/eui';
import { ContainerOutput } from 'src/plugins/embeddable/public';
import { ErrorEmbeddable, ViewMode, isErrorEmbeddable, EmbeddableStart } from '../embeddable_api';
import {
DASHBOARD_CONTAINER_TYPE,
DashboardContainer,
DashboardContainerInput,
} from '../../../../../../src/plugins/dashboard/public';
import { dashboardInput } from './dashboard_input';
interface Props {
embeddableServices: EmbeddableStart;
}
interface State {
loaded: boolean;
viewMode: ViewMode;
}
export class DashboardContainerExample extends React.Component<Props, State> {
private mounted = false;
private container: DashboardContainer | ErrorEmbeddable | undefined;
constructor(props: Props) {
super(props);
this.state = {
viewMode: ViewMode.VIEW,
loaded: false,
};
}
public async componentDidMount() {
this.mounted = true;
const dashboardFactory = this.props.embeddableServices.getEmbeddableFactory<
DashboardContainerInput,
ContainerOutput,
DashboardContainer
>(DASHBOARD_CONTAINER_TYPE);
if (dashboardFactory) {
this.container = await dashboardFactory.create(dashboardInput);
if (this.mounted) {
this.setState({ loaded: true });
}
}
}
public componentWillUnmount() {
this.mounted = false;
if (this.container) {
this.container.destroy();
}
}
public switchViewMode = () => {
this.setState<'viewMode'>((prevState: State) => {
if (!this.container || isErrorEmbeddable<DashboardContainer>(this.container)) {
return prevState;
}
const newMode = prevState.viewMode === ViewMode.VIEW ? ViewMode.EDIT : ViewMode.VIEW;
this.container.updateInput({ viewMode: newMode });
return { viewMode: newMode };
});
};
public render() {
const { embeddableServices } = this.props;
return (
<div className="app-container dshAppContainer">
<h1>Dashboard Container</h1>
<EuiButton onClick={this.switchViewMode}>
{this.state.viewMode === ViewMode.VIEW ? 'Edit' : 'View'}
</EuiButton>
{!this.state.loaded || !this.container ? (
<EuiLoadingChart size="l" mono />
) : (
<embeddableServices.EmbeddablePanel embeddable={this.container} />
)}
</div>
);
}
}

View file

@ -1,25 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export * from '../../../../../src/plugins/embeddable/public';
export * from '../../../../../src/plugins/embeddable/public/lib/test_samples';
export {
HELLO_WORLD_EMBEDDABLE,
HelloWorldEmbeddableFactory,
} from '../../../../../examples/embeddable_examples/public';

View file

@ -1,30 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { PluginInitializer } from 'kibana/public';
import {
EmbeddableExplorerPublicPlugin,
EmbeddableExplorerSetup,
EmbeddableExplorerStart,
} from './plugin';
export { EmbeddableExplorerPublicPlugin as Plugin };
export const plugin: PluginInitializer<EmbeddableExplorerSetup, EmbeddableExplorerStart> = () =>
new EmbeddableExplorerPublicPlugin();

View file

@ -1,98 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { CoreSetup, Plugin, AppMountParameters } from 'kibana/public';
import { UiActionsStart, UiActionsSetup } from '../../../../../src/plugins/ui_actions/public';
import { createHelloWorldAction } from '../../../../../src/plugins/ui_actions/public/tests/test_samples';
import {
Start as InspectorStartContract,
Setup as InspectorSetupContract,
} from '../../../../../src/plugins/inspector/public';
import { App } from './app';
import {
CONTEXT_MENU_TRIGGER,
CONTACT_CARD_EMBEDDABLE,
HELLO_WORLD_EMBEDDABLE,
HelloWorldEmbeddableFactory,
ContactCardEmbeddableFactory,
SayHelloAction,
createSendMessageAction,
} from './embeddable_api';
import {
EmbeddableStart,
EmbeddableSetup,
} from '.../../../../../../../src/plugins/embeddable/public';
export interface SetupDependencies {
embeddable: EmbeddableSetup;
inspector: InspectorSetupContract;
uiActions: UiActionsSetup;
}
interface StartDependencies {
embeddable: EmbeddableStart;
uiActions: UiActionsStart;
inspector: InspectorStartContract;
}
export type EmbeddableExplorerSetup = void;
export type EmbeddableExplorerStart = void;
export class EmbeddableExplorerPublicPlugin
implements
Plugin<EmbeddableExplorerSetup, EmbeddableExplorerStart, SetupDependencies, StartDependencies> {
public setup(core: CoreSetup, setupDeps: SetupDependencies): EmbeddableExplorerSetup {
const helloWorldAction = createHelloWorldAction({} as any);
const sayHelloAction = new SayHelloAction(alert);
const sendMessageAction = createSendMessageAction({} as any);
setupDeps.uiActions.registerAction(helloWorldAction);
setupDeps.uiActions.registerAction(sayHelloAction);
setupDeps.uiActions.registerAction(sendMessageAction);
setupDeps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, helloWorldAction.id);
setupDeps.embeddable.registerEmbeddableFactory(
HELLO_WORLD_EMBEDDABLE,
new HelloWorldEmbeddableFactory()
);
setupDeps.embeddable.registerEmbeddableFactory(
CONTACT_CARD_EMBEDDABLE,
new ContactCardEmbeddableFactory((() => null) as any, {} as any)
);
core.application.register({
id: 'EmbeddableExplorer',
title: 'Embeddable Explorer',
async mount(params: AppMountParameters) {
const startPlugins = (await core.getStartServices())[1] as StartDependencies;
render(<App embeddableServices={startPlugins.embeddable} />, params.element);
return () => unmountComponentAtNode(params.element);
},
});
}
public start() {}
public stop() {}
}

View file

@ -1,20 +0,0 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true,
"types": [
"node",
"jest",
"react",
"flot"
]
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"../../../../typings/**/*",
],
"exclude": []
}

View file

@ -1,42 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export default function ({ getService, getPageObjects, loadTestFile }) {
const browser = getService('browser');
const appsMenu = getService('appsMenu');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const PageObjects = getPageObjects(['common', 'header']);
describe('embeddable explorer', function () {
before(async () => {
await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/dashboard/current/data');
await esArchiver.load('../functional/fixtures/es_archiver/dashboard/current/kibana');
await kibanaServer.uiSettings.replace({
'dateFormat:tz': 'Australia/North',
defaultIndex: 'logstash-*',
});
await browser.setWindowSize(1300, 900);
await PageObjects.common.navigateToApp('settings');
await appsMenu.clickLink('Embeddable Explorer');
});
loadTestFile(require.resolve('./dashboard_container'));
});
}

View file

@ -1200,7 +1200,6 @@
"embeddableApi.customizeTitle.optionsMenuForm.panelTitleInputAriaLabel": "このインプットへの変更は直ちに適用されます。Enter を押して閉じます。",
"embeddableApi.customizeTitle.optionsMenuForm.resetCustomDashboardButtonLabel": "タイトルをリセット",
"embeddableApi.errors.embeddableFactoryNotFound": "{type} を読み込めません。Elasticsearch と Kibana のデフォルトのディストリビューションを適切なライセンスでアップグレードしてください。",
"embeddableApi.errors.factoryDoesNotExist": "{type} の埋め込み可能なファクトリーは存在しません。必要なプラグインが全てインストールおよび有効化済みであることを確かめてください。",
"embeddableApi.errors.paneldoesNotExist": "パネルが見つかりません",
"embeddableApi.panel.dashboardPanelAriaLabel": "ダッシュボードパネル",
"embeddableApi.panel.editPanel.displayName": "{value} を編集",

View file

@ -1203,7 +1203,6 @@
"embeddableApi.customizeTitle.optionsMenuForm.panelTitleInputAriaLabel": "对此输入的更改将立即应用。按 enter 键可退出。",
"embeddableApi.customizeTitle.optionsMenuForm.resetCustomDashboardButtonLabel": "重置标题",
"embeddableApi.errors.embeddableFactoryNotFound": "{type} 无法加载。请升级到具有适当许可的默认 Elasticsearch 和 Kibana 分发。",
"embeddableApi.errors.factoryDoesNotExist": "{type} 的 Embeddable 工厂不存在。确保所有必需插件已安装并启用。",
"embeddableApi.errors.paneldoesNotExist": "未找到面板",
"embeddableApi.panel.dashboardPanelAriaLabel": "仪表板面板",
"embeddableApi.panel.editPanel.displayName": "编辑 {value}",

View file

@ -5835,6 +5835,13 @@
dependencies:
pretty-format "^24.3.0"
"@types/testing-library__dom@^6.10.0":
version "6.14.0"
resolved "https://registry.yarnpkg.com/@types/testing-library__dom/-/testing-library__dom-6.14.0.tgz#1aede831cb4ed4a398448df5a2c54b54a365644e"
integrity sha512-sMl7OSv0AvMOqn1UJ6j1unPMIHRXen0Ita1ujnMX912rrOcawe4f7wu0Zt9GIQhBhJvH2BaibqFgQ3lP+Pj2hA==
dependencies:
pretty-format "^24.3.0"
"@types/testing-library__react-hooks@^3.0.0", "@types/testing-library__react-hooks@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.1.0.tgz#04d174ce767fbcce3ccb5021d7f156e1b06008a9"