mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
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:
parent
80ab0d9792
commit
3d0552e03c
65 changed files with 1031 additions and 863 deletions
1
examples/dashboard_embeddable_examples/README.md
Normal file
1
examples/dashboard_embeddable_examples/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
Example of using dashboard container embeddable outside of dashboard app
|
9
examples/dashboard_embeddable_examples/kibana.json
Normal file
9
examples/dashboard_embeddable_examples/kibana.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "dashboardEmbeddableExamples",
|
||||
"version": "0.0.1",
|
||||
"kibanaVersion": "kibana",
|
||||
"server": false,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["embeddable", "embeddableExamples", "dashboard", "developerExamples"],
|
||||
"optionalPlugins": []
|
||||
}
|
112
examples/dashboard_embeddable_examples/public/app.tsx
Normal file
112
examples/dashboard_embeddable_examples/public/app.tsx
Normal 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);
|
||||
};
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -17,4 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { App } from './app';
|
||||
import { DashboardEmbeddableExamples } from './plugin';
|
||||
|
||||
export const plugin = () => new DashboardEmbeddableExamples();
|
64
examples/dashboard_embeddable_examples/public/plugin.tsx
Normal file
64
examples/dashboard_embeddable_examples/public/plugin.tsx
Normal 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() {}
|
||||
}
|
15
examples/dashboard_embeddable_examples/tsconfig.json
Normal file
15
examples/dashboard_embeddable_examples/tsconfig.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"public/**/*.ts",
|
||||
"public/**/*.tsx",
|
||||
"server/**/*.ts",
|
||||
"../../typings/**/*",
|
||||
],
|
||||
"exclude": []
|
||||
}
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -18,4 +18,4 @@
|
|||
*/
|
||||
|
||||
export { ListContainer, LIST_CONTAINER } from './list_container';
|
||||
export { ListContainerFactory } from './list_container_factory';
|
||||
export { ListContainerFactoryDefinition, ListContainerFactory } from './list_container_factory';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'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'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>
|
||||
|
|
|
@ -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't be difficult. In the mean time, it's good
|
||||
to understand that chilren may ignore input they don't care about. Likewise the
|
||||
to understand that children may ignore input they don't care about. Likewise the
|
||||
container will have to choose what to do when it encounters children that are missing
|
||||
certain output variables.
|
||||
</p>
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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 { task, title, icon } = this.state;
|
||||
<br />
|
||||
this.embeddable.updateInput({ task, title, icon });
|
||||
</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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 }>;
|
||||
|
|
|
@ -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} />
|
||||
);
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
5
src/plugins/embeddable/docs/README.md
Normal file
5
src/plugins/embeddable/docs/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Embeddable Docs, Guides & Caveats
|
||||
|
||||
## Reference
|
||||
|
||||
- [Embeddable containers and inherited input state](./containers_and_inherited_state.md)
|
|
@ -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' });
|
||||
```
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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} />;
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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!
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
```
|
|
@ -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} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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();
|
|
@ -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() {}
|
||||
}
|
|
@ -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": []
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
|
@ -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} を編集",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue