Embeddable add panel examples (#57319)

* Embeddable add panel examples

* add tests

* Fix type error after merge

* address code review comments

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Stacey Gammon 2020-02-19 15:16:58 -05:00 committed by GitHub
parent 5946729097
commit 63cfffbe11
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 428 additions and 79 deletions

View file

@ -23,9 +23,13 @@ import {
MultiTaskTodoEmbeddable,
MULTI_TASK_TODO_EMBEDDABLE,
MultiTaskTodoInput,
MultiTaskTodoOutput,
} from './multi_task_todo_embeddable';
export class MultiTaskTodoEmbeddableFactory extends EmbeddableFactory {
export class MultiTaskTodoEmbeddableFactory extends EmbeddableFactory<
MultiTaskTodoInput,
MultiTaskTodoOutput
> {
public readonly type = MULTI_TASK_TODO_EMBEDDABLE;
public isEditable() {
@ -36,6 +40,15 @@ export class MultiTaskTodoEmbeddableFactory extends EmbeddableFactory {
return new MultiTaskTodoEmbeddable(initialInput, parent);
}
/**
* Check out todo_embeddable_factory for a better example that asks for data from
* the user. This just returns default data. That's okay too though, if you want to
* start with default data and expose an "edit" action to modify it.
*/
public async getExplicitInput() {
return { title: 'default title', tasks: ['Im default data'] };
}
public getDisplayName() {
return i18n.translate('embeddableExamples.multiTaskTodo.displayName', {
defaultMessage: 'Multi-task todo item',

View file

@ -17,11 +17,20 @@
* under the License.
*/
import { IEmbeddableSetup, IEmbeddableStart } from '../../../src/plugins/embeddable/public';
import {
IEmbeddableSetup,
IEmbeddableStart,
EmbeddableFactory,
} 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 } from './todo';
import { MULTI_TASK_TODO_EMBEDDABLE, MultiTaskTodoEmbeddableFactory } from './multi_task_todo';
import { TODO_EMBEDDABLE, TodoEmbeddableFactory, TodoInput, TodoOutput } from './todo';
import {
MULTI_TASK_TODO_EMBEDDABLE,
MultiTaskTodoEmbeddableFactory,
MultiTaskTodoOutput,
MultiTaskTodoInput,
} from './multi_task_todo';
import {
SEARCHABLE_LIST_CONTAINER,
SearchableListContainerFactory,
@ -45,12 +54,9 @@ export class EmbeddableExamplesPlugin
new HelloWorldEmbeddableFactory()
);
deps.embeddable.registerEmbeddableFactory(TODO_EMBEDDABLE, new TodoEmbeddableFactory());
deps.embeddable.registerEmbeddableFactory(
MULTI_TASK_TODO_EMBEDDABLE,
new MultiTaskTodoEmbeddableFactory()
);
deps.embeddable.registerEmbeddableFactory<
EmbeddableFactory<MultiTaskTodoInput, MultiTaskTodoOutput>
>(MULTI_TASK_TODO_EMBEDDABLE, new MultiTaskTodoEmbeddableFactory());
}
public start(core: CoreStart, deps: EmbeddableExamplesStartDependencies) {
@ -66,6 +72,11 @@ export class EmbeddableExamplesPlugin
LIST_CONTAINER,
new ListContainerFactory(deps.embeddable.getEmbeddableFactory)
);
deps.embeddable.registerEmbeddableFactory<EmbeddableFactory<TodoInput, TodoOutput>>(
TODO_EMBEDDABLE,
new TodoEmbeddableFactory(core.overlays.openModal)
);
}
public stop() {}

View file

@ -1,40 +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 { i18n } from '@kbn/i18n';
import { IContainer, EmbeddableFactory } from '../../../../src/plugins/embeddable/public';
import { TodoEmbeddable, TODO_EMBEDDABLE, TodoInput } from './todo_embeddable';
export class TodoEmbeddableFactory extends EmbeddableFactory {
public readonly type = TODO_EMBEDDABLE;
public isEditable() {
return true;
}
public async create(initialInput: TodoInput, parent?: IContainer) {
return new TodoEmbeddable(initialInput, parent);
}
public getDisplayName() {
return i18n.translate('embeddableExamples.todo.displayName', {
defaultMessage: 'Todo item',
});
}
}

View file

@ -0,0 +1,92 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState } from 'react';
import { EuiModalBody } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
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, EmbeddableFactory } from '../../../../src/plugins/embeddable/public';
import { TodoEmbeddable, TODO_EMBEDDABLE, TodoInput, TodoOutput } from './todo_embeddable';
function TaskInput({ onSave }: { onSave: (task: string) => void }) {
const [task, setTask] = useState('');
return (
<EuiModalBody>
<EuiFieldText
data-test-subj="taskInputField"
value={task}
placeholder="Enter task here"
onChange={e => setTask(e.target.value)}
/>
<EuiButton data-test-subj="createTodoEmbeddable" onClick={() => onSave(task)}>
Save
</EuiButton>
</EuiModalBody>
);
}
export class TodoEmbeddableFactory extends EmbeddableFactory<
TodoInput,
TodoOutput,
TodoEmbeddable
> {
public readonly type = TODO_EMBEDDABLE;
constructor(private openModal: OverlayStart['openModal']) {
super();
}
public isEditable() {
return true;
}
public async create(initialInput: TodoInput, parent?: IContainer) {
return new TodoEmbeddable(initialInput, parent);
}
/**
* This function is used when dynamically creating a new embeddable to add to a
* container. Some input may be inherited from the container, but not all. This can be
* used to collect specific embeddable input that the container will not provide, like
* in this case, the task string.
*/
public async getExplicitInput() {
return new Promise<{ task: string }>(resolve => {
const onSave = (task: string) => resolve({ task });
const overlay = this.openModal(
toMountPoint(
<TaskInput
onSave={(task: string) => {
onSave(task);
overlay.close();
}}
/>
)
);
});
}
public getDisplayName() {
return i18n.translate('embeddableExamples.todo.displayName', {
defaultMessage: 'Todo item',
});
}
}

View file

@ -5,6 +5,6 @@
"configPath": ["embeddable_explorer"],
"server": false,
"ui": true,
"requiredPlugins": ["embeddable", "embeddableExamples"],
"requiredPlugins": ["uiActions", "inspector", "embeddable", "embeddableExamples"],
"optionalPlugins": []
}

View file

@ -23,11 +23,21 @@ import { BrowserRouter as Router, Route, withRouter, RouteComponentProps } from
import { EuiPage, EuiPageSideBar, EuiSideNav } from '@elastic/eui';
import { IEmbeddableStart } from 'src/plugins/embeddable/public';
import { AppMountContext, AppMountParameters, CoreStart } from '../../../src/core/public';
import { IEmbeddableStart } from '../../../src/plugins/embeddable/public';
import { UiActionsStart } from '../../../src/plugins/ui_actions/public';
import { Start as InspectorStartContract } from '../../../src/plugins/inspector/public';
import {
AppMountContext,
AppMountParameters,
CoreStart,
SavedObjectsStart,
IUiSettingsClient,
OverlayStart,
} from '../../../src/core/public';
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';
interface PageDef {
title: string;
@ -61,15 +71,29 @@ const Nav = withRouter(({ history, navigateToApp, pages }: NavProps) => {
);
});
interface Props {
basename: string;
navigateToApp: CoreStart['application']['navigateToApp'];
embeddableApi: IEmbeddableStart;
uiActionsApi: UiActionsStart;
overlays: OverlayStart;
notifications: CoreStart['notifications'];
inspector: InspectorStartContract;
savedObject: SavedObjectsStart;
uiSettingsClient: IUiSettingsClient;
}
const EmbeddableExplorerApp = ({
basename,
navigateToApp,
embeddableApi,
}: {
basename: string;
navigateToApp: CoreStart['application']['navigateToApp'];
embeddableApi: IEmbeddableStart;
}) => {
inspector,
uiSettingsClient,
savedObject,
overlays,
uiActionsApi,
notifications,
}: Props) => {
const pages: PageDef[] = [
{
title: 'Hello world embeddable',
@ -90,6 +114,22 @@ const EmbeddableExplorerApp = ({
id: 'listContainerSection',
component: <ListContainerExample getEmbeddableFactory={embeddableApi.getEmbeddableFactory} />,
},
{
title: 'Dynamically adding children to a container',
id: 'embeddablePanelExamplae',
component: (
<EmbeddablePanelExample
uiActionsApi={uiActionsApi}
getAllEmbeddableFactories={embeddableApi.getEmbeddableFactories}
getEmbeddableFactory={embeddableApi.getEmbeddableFactory}
overlays={overlays}
uiSettingsClient={uiSettingsClient}
savedObject={savedObject}
notifications={notifications}
inspector={inspector}
/>
),
},
];
const routes = pages.map((page, i) => (
@ -108,19 +148,8 @@ const EmbeddableExplorerApp = ({
);
};
export const renderApp = (
core: CoreStart,
embeddableApi: IEmbeddableStart,
{ appBasePath, element }: AppMountParameters
) => {
ReactDOM.render(
<EmbeddableExplorerApp
basename={appBasePath}
navigateToApp={core.application.navigateToApp}
embeddableApi={embeddableApi}
/>,
element
);
export const renderApp = (props: Props, element: AppMountParameters['element']) => {
ReactDOM.render(<EmbeddableExplorerApp {...props} />, element);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -0,0 +1,164 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState, useEffect, useRef } from 'react';
import {
EuiPanel,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageHeader,
EuiPageHeaderSection,
EuiTitle,
EuiText,
} from '@elastic/eui';
import { EuiSpacer } from '@elastic/eui';
import { OverlayStart, CoreStart, SavedObjectsStart, IUiSettingsClient } from 'kibana/public';
import {
GetEmbeddableFactory,
EmbeddablePanel,
IEmbeddableStart,
IEmbeddable,
} from '../../../src/plugins/embeddable/public';
import {
HELLO_WORLD_EMBEDDABLE,
TODO_EMBEDDABLE,
MULTI_TASK_TODO_EMBEDDABLE,
SEARCHABLE_LIST_CONTAINER,
} from '../../embeddable_examples/public';
import { UiActionsStart } from '../../../src/plugins/ui_actions/public';
import { Start as InspectorStartContract } from '../../../src/plugins/inspector/public';
import { getSavedObjectFinder } from '../../../src/plugins/saved_objects/public';
interface Props {
getAllEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories'];
getEmbeddableFactory: GetEmbeddableFactory;
uiActionsApi: UiActionsStart;
overlays: OverlayStart;
notifications: CoreStart['notifications'];
inspector: InspectorStartContract;
savedObject: SavedObjectsStart;
uiSettingsClient: IUiSettingsClient;
}
export function EmbeddablePanelExample({
inspector,
notifications,
overlays,
getAllEmbeddableFactories,
getEmbeddableFactory,
uiActionsApi,
savedObject,
uiSettingsClient,
}: Props) {
const searchableInput = {
id: '1',
title: 'My searchable todo list',
panels: {
'1': {
type: HELLO_WORLD_EMBEDDABLE,
explicitInput: {
id: '1',
title: 'Hello',
},
},
'2': {
type: TODO_EMBEDDABLE,
explicitInput: {
id: '2',
task: 'Goes out on Wednesdays!',
icon: 'broom',
title: 'Take out the trash',
},
},
'3': {
type: MULTI_TASK_TODO_EMBEDDABLE,
explicitInput: {
id: '3',
icon: 'searchProfilerApp',
title: 'Learn more',
tasks: ['Go to school', 'Watch planet earth', 'Read the encyclopedia'],
},
},
},
};
const [embeddable, setEmbeddable] = useState<IEmbeddable | undefined>(undefined);
const ref = useRef(false);
useEffect(() => {
ref.current = true;
if (!embeddable) {
const factory = getEmbeddableFactory(SEARCHABLE_LIST_CONTAINER);
const promise = factory?.create(searchableInput);
if (promise) {
promise.then(e => {
if (ref.current) {
setEmbeddable(e);
}
});
}
}
return () => {
ref.current = false;
};
});
return (
<EuiPageBody>
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>The embeddable panel component</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody>
<EuiText>
You can render your embeddable inside the EmbeddablePanel component. This adds some
extra rendering and offers a context menu with pluggable actions. Using EmbeddablePanel
to render your embeddable means you get access to the &quote;Add panel flyout&quote;.
Now you can see how to add embeddables to your container, and how
&quote;getExplicitInput&quote; is used to grab input not provided by the container.
</EuiText>
<EuiPanel data-test-subj="embeddedPanelExample" paddingSize="none" role="figure">
{embeddable ? (
<EmbeddablePanel
embeddable={embeddable}
getActions={uiActionsApi.getTriggerCompatibleActions}
getEmbeddableFactory={getEmbeddableFactory}
getAllEmbeddableFactories={getAllEmbeddableFactories}
overlays={overlays}
notifications={notifications}
inspector={inspector}
SavedObjectFinder={getSavedObjectFinder(savedObject, uiSettingsClient)}
/>
) : (
<EuiText>Loading...</EuiText>
)}
</EuiPanel>
<EuiSpacer />
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
);
}

View file

@ -60,7 +60,7 @@ export function ListContainerExample({ getEmbeddableFactory }: Props) {
type: TODO_EMBEDDABLE,
explicitInput: {
id: '2',
task: 'Goes out on Wenesdays!',
task: 'Goes out on Wednesdays!',
icon: 'broom',
title: 'Take out the trash',
},
@ -91,7 +91,7 @@ export function ListContainerExample({ getEmbeddableFactory }: Props) {
type: TODO_EMBEDDABLE,
explicitInput: {
id: '2',
task: 'Goes out on Wenesdays!',
task: 'Goes out on Wednesdays!',
icon: 'broom',
title: 'Take out the trash',
},
@ -102,7 +102,7 @@ export function ListContainerExample({ getEmbeddableFactory }: Props) {
id: '3',
icon: 'searchProfilerApp',
title: 'Learn more',
tasks: ['Go to school', 'Watch planet earth', 'Read the encylopedia'],
tasks: ['Go to school', 'Watch planet earth', 'Read the encyclopedia'],
},
},
},
@ -151,6 +151,11 @@ export function ListContainerExample({ getEmbeddableFactory }: Props) {
The first HelloWorldEmbeddable does not emit the hasMatch output variable, so the
container chooses to hide it.
</p>
<p>
Check out the &quote;Dynamically adding children&quote; section, to see how to add
children to this container, and see it rendered inside an `EmbeddablePanel` component.
</p>
</EuiText>
<EuiSpacer />

View file

@ -18,17 +18,38 @@
*/
import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public';
import { UiActionsService } from '../../../src/plugins/ui_actions/public';
import { IEmbeddableStart } from '../../../src/plugins/embeddable/public';
import { Start as InspectorStart } from '../../../src/plugins/inspector/public';
export class EmbeddableExplorerPlugin implements Plugin {
public setup(core: CoreSetup<{ embeddable: IEmbeddableStart }>) {
interface StartDeps {
uiActions: UiActionsService;
embeddable: IEmbeddableStart;
inspector: InspectorStart;
}
export class EmbeddableExplorerPlugin implements Plugin<void, void, {}, StartDeps> {
public setup(core: CoreSetup<StartDeps>) {
core.application.register({
id: 'embeddableExplorer',
title: 'Embeddable explorer',
async mount(params: AppMountParameters) {
const [coreStart, depsStart] = await core.getStartServices();
const { renderApp } = await import('./app');
return renderApp(coreStart, depsStart.embeddable, params);
return renderApp(
{
notifications: coreStart.notifications,
inspector: depsStart.inspector,
embeddableApi: depsStart.embeddable,
uiActionsApi: depsStart.uiActions,
basename: params.appBasePath,
uiSettingsClient: coreStart.uiSettings,
savedObject: coreStart.savedObjects,
overlays: coreStart.overlays,
navigateToApp: coreStart.application.navigateToApp,
},
params.element
);
},
});
}

View file

@ -24,7 +24,10 @@ export interface EmbeddableApi {
getEmbeddableFactory: (embeddableFactoryId: string) => EmbeddableFactory;
getEmbeddableFactories: GetEmbeddableFactories;
// TODO: Make `registerEmbeddableFactory` receive only `factory` argument.
registerEmbeddableFactory: (id: string, factory: EmbeddableFactory) => void;
registerEmbeddableFactory: <TEmbeddableFactory extends EmbeddableFactory>(
id: string,
factory: TEmbeddableFactory
) => void;
}
export interface EmbeddableDependencies {

View file

@ -33,6 +33,13 @@ export default async function({ readConfigFile }) {
...functionalConfig.get('services'),
...services,
},
uiSettings: {
defaults: {
'accessibility:disableAnimations': true,
'dateFormat:tz': 'UTC',
'telemetry:optIn': false,
},
},
pageObjects: functionalConfig.get('pageObjects'),
servers: functionalConfig.get('servers'),
esTestCluster: functionalConfig.get('esTestCluster'),

View file

@ -0,0 +1,43 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import expect from '@kbn/expect';
import { PluginFunctionalProviderContext } from 'test/plugin_functional/services';
// eslint-disable-next-line import/no-default-export
export default function({ getService }: PluginFunctionalProviderContext) {
const testSubjects = getService('testSubjects');
describe('creating and adding children', () => {
before(async () => {
await testSubjects.click('embeddablePanelExamplae');
});
it('Can create a new child', async () => {
await testSubjects.click('embeddablePanelToggleMenuIcon');
await testSubjects.click('embeddablePanelAction-ADD_PANEL_ACTION_ID');
await testSubjects.click('createNew');
await testSubjects.click('createNew-TODO_EMBEDDABLE');
await testSubjects.setValue('taskInputField', 'new task');
await testSubjects.click('createTodoEmbeddable');
const tasks = await testSubjects.getVisibleTextAll('todoEmbeddableTask');
expect(tasks).to.eql(['Goes out on Wednesdays!', 'new task']);
});
});
}

View file

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

View file

@ -45,7 +45,7 @@ export default function({ getService }: PluginFunctionalProviderContext) {
expect(text).to.eql(['HELLO WORLD!', 'HELLO WORLD!']);
const tasks = await testSubjects.getVisibleTextAll('multiTaskTodoTask');
expect(tasks).to.eql(['Go to school', 'Watch planet earth', 'Read the encylopedia']);
expect(tasks).to.eql(['Go to school', 'Watch planet earth', 'Read the encyclopedia']);
});
});