mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Portable Dashboards] Add portable dashboard example plugin (#148997)
Closes https://github.com/elastic/kibana/issues/145427 ## Summary This PR adds an example plugin that demonstrates a few uses of the new portable dashboards. It includes the following examples: 1. A by-value dashboard with controls  2. A by-value empty dashboard that allows panels (both by-value and by-reference) to be added where the state can be saved to local storage  3. Two side-by-side by-value empty dashboards with independent redux states  4. A static, by-reference dashboard  5. A static, by-value dashboard  As part of this, I created a new demo embeddable type - the `FilterDebuggerEmbeddable` which, when added to a dashboard, will display the filters + query that it is receiving as an input. You can see how this embeddable works in the GIF for the first example above. ### Checklist - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
6ad4a44917
commit
27dda79627
45 changed files with 1349 additions and 256 deletions
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
@ -287,7 +287,6 @@ packages/kbn-crypto-browser @elastic/kibana-core
|
|||
x-pack/plugins/custom_branding @elastic/appex-sharedux
|
||||
src/plugins/custom_integrations @elastic/fleet
|
||||
packages/kbn-cypress-config @elastic/kibana-operations
|
||||
examples/dashboard_embeddable_examples @elastic/kibana-presentation
|
||||
x-pack/plugins/dashboard_enhanced @elastic/kibana-presentation
|
||||
src/plugins/dashboard @elastic/kibana-presentation
|
||||
src/plugins/data @elastic/kibana-visualizations @elastic/kibana-data-discovery
|
||||
|
@ -477,6 +476,7 @@ packages/kbn-performance-testing-dataset-extractor @elastic/kibana-performance-t
|
|||
packages/kbn-picomatcher @elastic/kibana-operations
|
||||
packages/kbn-plugin-generator @elastic/kibana-operations
|
||||
packages/kbn-plugin-helpers @elastic/kibana-operations
|
||||
examples/portable_dashboards_example @elastic/kibana-presentation
|
||||
examples/preboot_example @elastic/kibana-security @elastic/kibana-core
|
||||
src/plugins/presentation_util @elastic/kibana-presentation
|
||||
x-pack/plugins/profiling @elastic/profiling-ui
|
||||
|
|
|
@ -11,9 +11,7 @@
|
|||
"server/**/*.ts",
|
||||
"../../typings/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
],
|
||||
"exclude": ["target/**/*"],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/developer-examples-plugin",
|
||||
|
@ -24,6 +22,6 @@
|
|||
"@kbn/shared-ux-page-kibana-template",
|
||||
"@kbn/embeddable-plugin",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/es-query",
|
||||
"@kbn/es-query"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Example of using dashboard container embeddable outside of dashboard app
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"type": "plugin",
|
||||
"id": "@kbn/dashboard-embeddable-examples-plugin",
|
||||
"owner": "@elastic/kibana-presentation",
|
||||
"description": "Example app that shows how to embed a dashboard in an application",
|
||||
"plugin": {
|
||||
"id": "dashboardEmbeddableExamples",
|
||||
"server": false,
|
||||
"browser": true,
|
||||
"requiredPlugins": [
|
||||
"embeddable",
|
||||
"embeddableExamples",
|
||||
"dashboard",
|
||||
"developerExamples",
|
||||
"kibanaReact"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter as Router, Route, RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
EuiPage,
|
||||
EuiPageContent_Deprecated as EuiPageContent,
|
||||
EuiPageContentBody_Deprecated as EuiPageContentBody,
|
||||
EuiPageSideBar_Deprecated as EuiPageSideBar,
|
||||
EuiSideNav,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import 'brace/mode/json';
|
||||
import { AppMountParameters, IUiSettingsClient } from '@kbn/core/public';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/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;
|
||||
uiSettings: IUiSettingsClient;
|
||||
}
|
||||
|
||||
const DashboardEmbeddableExplorerApp = ({ basename, uiSettings }: Props) => {
|
||||
const pages: PageDef[] = [
|
||||
{
|
||||
title: 'Portable Dashboard basic embeddable example',
|
||||
id: 'portableDashboardEmbeddableBasicExample',
|
||||
component: (
|
||||
<EuiTitle>
|
||||
<EuiText>Portable Dashboard embeddable examples coming soon!</EuiText>
|
||||
</EuiTitle>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const routes = pages.map((page, i) => (
|
||||
<Route key={i} path={`/${page.id}`} render={(props) => page.component} />
|
||||
));
|
||||
|
||||
return (
|
||||
<KibanaContextProvider services={{ uiSettings }}>
|
||||
<Router basename={basename}>
|
||||
<EuiPage>
|
||||
<EuiPageSideBar>
|
||||
<Nav pages={pages} />
|
||||
</EuiPageSideBar>
|
||||
<EuiPageContent>
|
||||
<EuiPageContentBody>{routes}</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiPage>
|
||||
</Router>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const renderApp = (props: Props, element: AppMountParameters['element']) => {
|
||||
ReactDOM.render(<DashboardEmbeddableExplorerApp {...props} />, element);
|
||||
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { AppMountParameters, AppNavLinkStatus, CoreSetup, Plugin } from '@kbn/core/public';
|
||||
import { DashboardStart } from '@kbn/dashboard-plugin/public';
|
||||
import { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
|
||||
import { EmbeddableExamplesStart } from '@kbn/embeddable-examples-plugin/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 [coreStart, depsStart] = await core.getStartServices();
|
||||
const { renderApp } = await import('./app');
|
||||
await depsStart.embeddableExamples.createSampleData();
|
||||
return renderApp(
|
||||
{
|
||||
basename: params.appBasePath,
|
||||
uiSettings: coreStart.uiSettings,
|
||||
},
|
||||
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() {}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"public/**/*.ts",
|
||||
"public/**/*.tsx",
|
||||
"server/**/*.ts",
|
||||
"../../typings/**/*",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/dashboard-plugin",
|
||||
"@kbn/kibana-react-plugin",
|
||||
"@kbn/embeddable-examples-plugin",
|
||||
"@kbn/developer-examples-plugin",
|
||||
]
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import {
|
||||
FilterDebuggerEmbeddableInput,
|
||||
FILTER_DEBUGGER_EMBEDDABLE,
|
||||
} from './filter_debugger_embeddable_factory';
|
||||
import { FilterDebuggerEmbeddableComponent } from './filter_debugger_embeddable_component';
|
||||
|
||||
export class FilterDebuggerEmbeddable extends Embeddable<FilterDebuggerEmbeddableInput> {
|
||||
public readonly type = FILTER_DEBUGGER_EMBEDDABLE;
|
||||
|
||||
private domNode?: HTMLElement;
|
||||
|
||||
constructor(initialInput: FilterDebuggerEmbeddableInput, parent?: IContainer) {
|
||||
super(initialInput, {}, parent);
|
||||
}
|
||||
|
||||
public render(node: HTMLElement) {
|
||||
if (this.domNode) {
|
||||
ReactDOM.unmountComponentAtNode(this.domNode);
|
||||
}
|
||||
this.domNode = node;
|
||||
ReactDOM.render(<FilterDebuggerEmbeddableComponent embeddable={this} />, node);
|
||||
}
|
||||
|
||||
public reload() {}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
if (this.domNode) ReactDOM.unmountComponentAtNode(this.domNode);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { distinctUntilChanged } from 'rxjs';
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
import { EuiPanel, EuiTitle, EuiCodeBlock, EuiSpacer } from '@elastic/eui';
|
||||
import { compareFilters, COMPARE_ALL_OPTIONS, type Filter, type Query } from '@kbn/es-query';
|
||||
|
||||
import { FilterDebuggerEmbeddable } from './filter_debugger_embeddable';
|
||||
|
||||
interface Props {
|
||||
embeddable: FilterDebuggerEmbeddable;
|
||||
}
|
||||
|
||||
export function FilterDebuggerEmbeddableComponent({ embeddable }: Props) {
|
||||
const [filters, setFilters] = useState<Filter[]>();
|
||||
const [query, setQuery] = useState<Query>();
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = embeddable
|
||||
.getInput$()
|
||||
.pipe(
|
||||
distinctUntilChanged(
|
||||
({ filters: filtersA, query: queryA }, { filters: filtersB, query: queryB }) => {
|
||||
return (
|
||||
JSON.stringify(queryA) === JSON.stringify(queryB) &&
|
||||
compareFilters(filtersA ?? [], filtersB ?? [], COMPARE_ALL_OPTIONS)
|
||||
);
|
||||
}
|
||||
)
|
||||
)
|
||||
.subscribe(({ filters: newFilters, query: newQuery }) => {
|
||||
setFilters(newFilters);
|
||||
setQuery(newQuery);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [embeddable]);
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
css={css`
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
`}
|
||||
className="eui-yScrollWithShadows"
|
||||
hasShadow={false}
|
||||
>
|
||||
<EuiTitle>
|
||||
<h2>Filters</h2>
|
||||
</EuiTitle>
|
||||
<EuiCodeBlock language="JSON">{JSON.stringify(filters, undefined, 1)}</EuiCodeBlock>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiTitle>
|
||||
<h2>Query</h2>
|
||||
</EuiTitle>
|
||||
<EuiCodeBlock language="JSON">{JSON.stringify(query, undefined, 1)}</EuiCodeBlock>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
IContainer,
|
||||
EmbeddableInput,
|
||||
EmbeddableFactoryDefinition,
|
||||
EmbeddableFactory,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { type Filter, type Query } from '@kbn/es-query';
|
||||
import { FilterDebuggerEmbeddable } from './filter_debugger_embeddable';
|
||||
|
||||
export const FILTER_DEBUGGER_EMBEDDABLE = 'filterDebuggerEmbeddable';
|
||||
export interface FilterDebuggerEmbeddableInput extends EmbeddableInput {
|
||||
filters?: Filter[];
|
||||
query?: Query;
|
||||
}
|
||||
|
||||
export type FilterDebuggerEmbeddableFactory = EmbeddableFactory;
|
||||
export class FilterDebuggerEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition {
|
||||
public readonly type = FILTER_DEBUGGER_EMBEDDABLE;
|
||||
|
||||
public async isEditable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public async create(initialInput: FilterDebuggerEmbeddableInput, parent?: IContainer) {
|
||||
return new FilterDebuggerEmbeddable(initialInput, parent);
|
||||
}
|
||||
|
||||
public canCreateNew() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public getDisplayName() {
|
||||
return 'Filter debugger';
|
||||
}
|
||||
}
|
|
@ -6,6 +6,5 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { DashboardEmbeddableExamples } from './plugin';
|
||||
|
||||
export const plugin = () => new DashboardEmbeddableExamples();
|
||||
export * from './filter_debugger_embeddable';
|
||||
export * from './filter_debugger_embeddable_factory';
|
|
@ -20,6 +20,10 @@ export { TODO_EMBEDDABLE } from './todo';
|
|||
export { BOOK_EMBEDDABLE } from './book';
|
||||
|
||||
export { SIMPLE_EMBEDDABLE } from './migrations';
|
||||
export {
|
||||
FILTER_DEBUGGER_EMBEDDABLE,
|
||||
FilterDebuggerEmbeddableFactoryDefinition,
|
||||
} from './filter_debugger';
|
||||
|
||||
import { EmbeddableExamplesPlugin } from './plugin';
|
||||
|
||||
|
@ -27,4 +31,5 @@ export type { SearchableListContainerFactory } from './searchable_list_container
|
|||
export { SearchableListContainer, SEARCHABLE_LIST_CONTAINER } from './searchable_list_container';
|
||||
export type { MultiTaskTodoEmbeddableFactory } from './multi_task_todo';
|
||||
export { MULTI_TASK_TODO_EMBEDDABLE } from './multi_task_todo';
|
||||
|
||||
export const plugin = () => new EmbeddableExamplesPlugin();
|
||||
|
|
|
@ -54,6 +54,11 @@ import {
|
|||
SimpleEmbeddableFactory,
|
||||
SimpleEmbeddableFactoryDefinition,
|
||||
} from './migrations';
|
||||
import {
|
||||
FILTER_DEBUGGER_EMBEDDABLE,
|
||||
FilterDebuggerEmbeddableFactory,
|
||||
FilterDebuggerEmbeddableFactoryDefinition,
|
||||
} from './filter_debugger';
|
||||
|
||||
export interface EmbeddableExamplesSetupDependencies {
|
||||
embeddable: EmbeddableSetup;
|
||||
|
@ -74,6 +79,7 @@ interface ExampleEmbeddableFactories {
|
|||
getTodoRefEmbeddableFactory: () => TodoRefEmbeddableFactory;
|
||||
getBookEmbeddableFactory: () => BookEmbeddableFactory;
|
||||
getMigrationsEmbeddableFactory: () => SimpleEmbeddableFactory;
|
||||
getFilterDebuggerEmbeddableFactory: () => FilterDebuggerEmbeddableFactory;
|
||||
}
|
||||
|
||||
export interface EmbeddableExamplesStart {
|
||||
|
@ -157,6 +163,12 @@ export class EmbeddableExamplesPlugin
|
|||
}))
|
||||
);
|
||||
|
||||
this.exampleEmbeddableFactories.getFilterDebuggerEmbeddableFactory =
|
||||
deps.embeddable.registerEmbeddableFactory(
|
||||
FILTER_DEBUGGER_EMBEDDABLE,
|
||||
new FilterDebuggerEmbeddableFactoryDefinition()
|
||||
);
|
||||
|
||||
const editBookAction = createEditBookActionDefinition(async () => ({
|
||||
getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService,
|
||||
openModal: (await core.getStartServices())[0].overlays.openModal,
|
||||
|
|
|
@ -24,5 +24,6 @@
|
|||
"@kbn/saved-objects-plugin",
|
||||
"@kbn/i18n",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/es-query",
|
||||
]
|
||||
}
|
||||
|
|
26
examples/portable_dashboards_example/kibana.jsonc
Normal file
26
examples/portable_dashboards_example/kibana.jsonc
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"type": "plugin",
|
||||
"id": "@kbn/portable-dashboards-example",
|
||||
"owner": "@elastic/kibana-presentation",
|
||||
"description": "Example plugin for portable dashboards",
|
||||
"plugin": {
|
||||
"id": "portableDashboardExamples",
|
||||
"server": false,
|
||||
"browser": true,
|
||||
"version": "1.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"ui": true,
|
||||
"requiredPlugins": [
|
||||
"data",
|
||||
"controls",
|
||||
"dashboard",
|
||||
"embeddable",
|
||||
"navigation",
|
||||
"savedObjects",
|
||||
"unifiedSearch",
|
||||
"developerExamples",
|
||||
"embeddableExamples"
|
||||
],
|
||||
"requiredBundles": ["presentationUtil"]
|
||||
}
|
||||
}
|
57
examples/portable_dashboards_example/public/app.tsx
Normal file
57
examples/portable_dashboards_example/public/app.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { AppMountParameters } from '@kbn/core/public';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
|
||||
import { DualReduxExample } from './dual_redux_example';
|
||||
import { PortableDashboardsExampleStartDeps } from './plugin';
|
||||
import { StaticByValueExample } from './static_by_value_example';
|
||||
import { StaticByReferenceExample } from './static_by_reference_example';
|
||||
import { DynamicByReferenceExample } from './dynamically_add_panels_example';
|
||||
import { DashboardWithControlsExample } from './dashboard_with_controls_example';
|
||||
|
||||
export const renderApp = async (
|
||||
{ data, dashboard }: PortableDashboardsExampleStartDeps,
|
||||
{ element }: AppMountParameters
|
||||
) => {
|
||||
const dataViews = await data.dataViews.find('kibana_sample_data_logs');
|
||||
const findDashboardsService = await dashboard.findDashboardsService();
|
||||
const logsSampleDashboardId = (await findDashboardsService?.findByTitle('[Logs] Web Traffic'))
|
||||
?.id;
|
||||
|
||||
const examples =
|
||||
dataViews.length > 0 ? (
|
||||
<>
|
||||
<DashboardWithControlsExample dataView={dataViews[0]} />
|
||||
<EuiSpacer size="xl" />
|
||||
<DynamicByReferenceExample />
|
||||
<EuiSpacer size="xl" />
|
||||
<DualReduxExample />
|
||||
<EuiSpacer size="xl" />
|
||||
<StaticByReferenceExample dashboardId={logsSampleDashboardId} dataView={dataViews[0]} />
|
||||
<EuiSpacer size="xl" />
|
||||
<StaticByValueExample />
|
||||
</>
|
||||
) : (
|
||||
<div>{'Install web logs sample data to run the embeddable dashboard examples.'}</div>
|
||||
);
|
||||
|
||||
ReactDOM.render(
|
||||
<KibanaPageTemplate>
|
||||
<KibanaPageTemplate.Header pageTitle="Portable Dashboards" />
|
||||
<KibanaPageTemplate.Section>{examples}</KibanaPageTemplate.Section>
|
||||
</KibanaPageTemplate>,
|
||||
element
|
||||
);
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
9
examples/portable_dashboards_example/public/constants.ts
Normal file
9
examples/portable_dashboards_example/public/constants.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const PLUGIN_ID = 'portableDashboardExamples';
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { withSuspense } from '@kbn/shared-ux-utility';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { controlGroupInputBuilder } from '@kbn/controls-plugin/public';
|
||||
import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { FILTER_DEBUGGER_EMBEDDABLE } from '@kbn/embeddable-examples-plugin/public';
|
||||
import { LazyDashboardContainerRenderer } from '@kbn/dashboard-plugin/public';
|
||||
|
||||
const DashboardContainerRenderer = withSuspense(LazyDashboardContainerRenderer);
|
||||
|
||||
export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView }) => {
|
||||
return (
|
||||
<>
|
||||
<EuiTitle>
|
||||
<h2>Dashboard with controls example</h2>
|
||||
</EuiTitle>
|
||||
<EuiText>
|
||||
<p>A dashboard with a markdown panel that displays the filters from its control group.</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPanel hasBorder={true}>
|
||||
<DashboardContainerRenderer
|
||||
getCreationOptions={async () => {
|
||||
const builder = controlGroupInputBuilder;
|
||||
const controlGroupInput = {};
|
||||
await builder.addDataControlFromField(controlGroupInput, {
|
||||
dataViewId: dataView.id ?? '',
|
||||
title: 'Destintion country',
|
||||
fieldName: 'geo.dest',
|
||||
width: 'medium',
|
||||
grow: false,
|
||||
});
|
||||
await builder.addDataControlFromField(controlGroupInput, {
|
||||
dataViewId: dataView.id ?? '',
|
||||
fieldName: 'bytes',
|
||||
width: 'medium',
|
||||
grow: true,
|
||||
title: 'Bytes',
|
||||
});
|
||||
|
||||
return {
|
||||
useControlGroupIntegration: true,
|
||||
initialInput: {
|
||||
timeRange: { from: 'now-30d', to: 'now' },
|
||||
viewMode: ViewMode.VIEW,
|
||||
controlGroupInput,
|
||||
},
|
||||
};
|
||||
}}
|
||||
onDashboardContainerLoaded={(container) => {
|
||||
const addFilterEmbeddable = async () => {
|
||||
const embeddable = await container.addNewEmbeddable(FILTER_DEBUGGER_EMBEDDABLE, {});
|
||||
const prevPanelState = container.getExplicitInput().panels[embeddable.id];
|
||||
// resize the new panel so that it fills up the entire width of the dashboard
|
||||
container.updateInput({
|
||||
panels: {
|
||||
[embeddable.id]: {
|
||||
...prevPanelState,
|
||||
gridData: { i: embeddable.id, x: 0, y: 0, w: 48, h: 12 },
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
addFilterEmbeddable();
|
||||
}}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { DashboardContainer, LazyDashboardContainerRenderer } from '@kbn/dashboard-plugin/public';
|
||||
import {
|
||||
EuiButtonGroup,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { withSuspense } from '@kbn/presentation-util-plugin/public';
|
||||
import { useDashboardContainerContext } from '@kbn/dashboard-plugin/public';
|
||||
|
||||
const DashboardContainerRenderer = withSuspense(LazyDashboardContainerRenderer);
|
||||
|
||||
export const DualReduxExample = () => {
|
||||
const [firstDashboardContainer, setFirstDashboardContainer] = useState<
|
||||
DashboardContainer | undefined
|
||||
>();
|
||||
const [secondDashboardContainer, setSecondDashboardContainer] = useState<
|
||||
DashboardContainer | undefined
|
||||
>();
|
||||
|
||||
const FirstDashboardReduxWrapper = useMemo(() => {
|
||||
if (firstDashboardContainer) return firstDashboardContainer.getReduxEmbeddableTools().Wrapper;
|
||||
}, [firstDashboardContainer]);
|
||||
const SecondDashboardReduxWrapper = useMemo(() => {
|
||||
if (secondDashboardContainer) return secondDashboardContainer.getReduxEmbeddableTools().Wrapper;
|
||||
}, [secondDashboardContainer]);
|
||||
|
||||
const ButtonControls = () => {
|
||||
const {
|
||||
useEmbeddableDispatch,
|
||||
useEmbeddableSelector: select,
|
||||
actions: { setViewMode },
|
||||
} = useDashboardContainerContext();
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
const viewMode = select((state) => state.explicitInput.viewMode);
|
||||
|
||||
return (
|
||||
<EuiButtonGroup
|
||||
legend="View mode"
|
||||
options={[
|
||||
{
|
||||
id: ViewMode.VIEW,
|
||||
label: 'View mode',
|
||||
value: ViewMode.VIEW,
|
||||
},
|
||||
{
|
||||
id: ViewMode.EDIT,
|
||||
label: 'Edit mode',
|
||||
value: ViewMode.EDIT,
|
||||
},
|
||||
]}
|
||||
idSelected={viewMode}
|
||||
onChange={(id, value) => {
|
||||
dispatch(setViewMode(value));
|
||||
}}
|
||||
type="single"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle>
|
||||
<h2>Dual redux example</h2>
|
||||
</EuiTitle>
|
||||
<EuiText>
|
||||
<p>
|
||||
Use the redux contexts from two different dashboard containers to independently set the
|
||||
view mode of each dashboard.
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPanel hasBorder={true}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h3>Dashboard #1</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
{FirstDashboardReduxWrapper && (
|
||||
<FirstDashboardReduxWrapper>
|
||||
<ButtonControls />
|
||||
</FirstDashboardReduxWrapper>
|
||||
)}
|
||||
<EuiSpacer size="m" />
|
||||
<DashboardContainerRenderer
|
||||
onDashboardContainerLoaded={(container) => {
|
||||
setFirstDashboardContainer(container);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h3>Dashboard #2</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
{SecondDashboardReduxWrapper && (
|
||||
<SecondDashboardReduxWrapper>
|
||||
<ButtonControls />
|
||||
</SecondDashboardReduxWrapper>
|
||||
)}
|
||||
<EuiSpacer size="m" />
|
||||
<DashboardContainerRenderer
|
||||
onDashboardContainerLoaded={(container) => {
|
||||
setSecondDashboardContainer(container);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { DashboardContainer, LazyDashboardContainerRenderer } from '@kbn/dashboard-plugin/public';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
VisualizeEmbeddable,
|
||||
VisualizeInput,
|
||||
VisualizeOutput,
|
||||
} from '@kbn/visualizations-plugin/public/embeddable/visualize_embeddable';
|
||||
import { withSuspense } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
const INPUT_KEY = 'portableDashboard:saveExample:input';
|
||||
|
||||
const DashboardContainerRenderer = withSuspense(LazyDashboardContainerRenderer); // make this so we don't have two loading states - loading in the dashboard plugin instead
|
||||
|
||||
export const DynamicByReferenceExample = () => {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [dashboardContainer, setDashboardContainer] = useState<DashboardContainer | undefined>();
|
||||
|
||||
const onSave = async () => {
|
||||
setIsSaving(true);
|
||||
localStorage.setItem(INPUT_KEY, JSON.stringify(dashboardContainer!.getInput()));
|
||||
// simulated async save await
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const getPersistableInput = () => {
|
||||
let input = {};
|
||||
const inputAsString = localStorage.getItem(INPUT_KEY);
|
||||
if (inputAsString) {
|
||||
try {
|
||||
input = JSON.parse(inputAsString);
|
||||
} catch (e) {
|
||||
// ignore parse errors
|
||||
}
|
||||
return input;
|
||||
}
|
||||
};
|
||||
|
||||
const resetPersistableInput = () => {
|
||||
localStorage.removeItem(INPUT_KEY);
|
||||
if (dashboardContainer) {
|
||||
const children = dashboardContainer.getChildIds();
|
||||
children.map((childId) => {
|
||||
dashboardContainer.removeEmbeddable(childId);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const addByReference = () => {
|
||||
if (dashboardContainer) {
|
||||
dashboardContainer.addFromLibrary();
|
||||
}
|
||||
};
|
||||
|
||||
const addByValue = async () => {
|
||||
if (dashboardContainer) {
|
||||
dashboardContainer.addNewEmbeddable<VisualizeInput, VisualizeOutput, VisualizeEmbeddable>(
|
||||
'visualization',
|
||||
{
|
||||
title: 'Sample Markdown Vis',
|
||||
savedVis: {
|
||||
type: 'markdown',
|
||||
title: '',
|
||||
data: { aggs: [], searchSource: {} },
|
||||
params: {
|
||||
fontSize: 12,
|
||||
openLinksInNewTab: false,
|
||||
markdown: '### By Value Visualization\nThis is a sample by value panel.',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const disableButtons = useMemo(() => {
|
||||
return dashboardContainer === undefined || isSaving;
|
||||
}, [dashboardContainer, isSaving]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle>
|
||||
<h2>Edit and save example</h2>
|
||||
</EuiTitle>
|
||||
<EuiText>
|
||||
<p>Customize the dashboard and persist the state to local storage.</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPanel hasBorder={true}>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiButton onClick={addByValue} isDisabled={disableButtons}>
|
||||
Add visualization by value
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton onClick={addByReference} isDisabled={disableButtons}>
|
||||
Add visualization from library
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiButton fill onClick={onSave} isLoading={isSaving} isDisabled={disableButtons}>
|
||||
Save to local storage
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
onClick={resetPersistableInput}
|
||||
isLoading={isSaving}
|
||||
isDisabled={disableButtons}
|
||||
>
|
||||
Empty dashboard and reset local storage
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<DashboardContainerRenderer
|
||||
getCreationOptions={async () => {
|
||||
const persistedInput = getPersistableInput();
|
||||
return {
|
||||
initialInput: {
|
||||
...persistedInput,
|
||||
timeRange: { from: 'now-30d', to: 'now' }, // need to set the time range for the by value vis
|
||||
},
|
||||
};
|
||||
}}
|
||||
onDashboardContainerLoaded={(container) => {
|
||||
setDashboardContainer(container);
|
||||
}}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
};
|
13
examples/portable_dashboards_example/public/index.ts
Normal file
13
examples/portable_dashboards_example/public/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PortableDashboardsExamplePlugin } from './plugin';
|
||||
|
||||
export function plugin() {
|
||||
return new PortableDashboardsExamplePlugin();
|
||||
}
|
63
examples/portable_dashboards_example/public/plugin.tsx
Normal file
63
examples/portable_dashboards_example/public/plugin.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
AppMountParameters,
|
||||
AppNavLinkStatus,
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
Plugin,
|
||||
} from '@kbn/core/public';
|
||||
import type { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
|
||||
import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { DashboardStart } from '@kbn/dashboard-plugin/public';
|
||||
|
||||
import img from './portable_dashboard_image.png';
|
||||
import { PLUGIN_ID } from './constants';
|
||||
|
||||
interface SetupDeps {
|
||||
developerExamples: DeveloperExamplesSetup;
|
||||
}
|
||||
|
||||
export interface PortableDashboardsExampleStartDeps {
|
||||
dashboard: DashboardStart;
|
||||
data: DataPublicPluginStart;
|
||||
navigation: NavigationPublicPluginStart;
|
||||
}
|
||||
|
||||
export class PortableDashboardsExamplePlugin
|
||||
implements Plugin<void, void, SetupDeps, PortableDashboardsExampleStartDeps>
|
||||
{
|
||||
public setup(
|
||||
core: CoreSetup<PortableDashboardsExampleStartDeps>,
|
||||
{ developerExamples }: SetupDeps
|
||||
) {
|
||||
core.application.register({
|
||||
id: PLUGIN_ID,
|
||||
title: 'Portable dashboard examples',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
async mount(params: AppMountParameters) {
|
||||
const [, depsStart] = await core.getStartServices();
|
||||
const { renderApp } = await import('./app');
|
||||
return renderApp(depsStart, params);
|
||||
},
|
||||
});
|
||||
|
||||
developerExamples.register({
|
||||
appId: PLUGIN_ID,
|
||||
title: 'Portable Dashboards',
|
||||
description: `Showcases different ways to embed a dashboard into your app`,
|
||||
image: img,
|
||||
});
|
||||
}
|
||||
|
||||
public async start(core: CoreStart, { dashboard }: PortableDashboardsExampleStartDeps) {}
|
||||
|
||||
public stop() {}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 126 KiB |
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { buildPhraseFilter, Filter } from '@kbn/es-query';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import {
|
||||
LazyDashboardContainerRenderer,
|
||||
DashboardCreationOptions,
|
||||
} from '@kbn/dashboard-plugin/public';
|
||||
import { EuiCode, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { withSuspense } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
const DashboardContainerRenderer = withSuspense(LazyDashboardContainerRenderer);
|
||||
|
||||
export const StaticByReferenceExample = ({
|
||||
dashboardId,
|
||||
dataView,
|
||||
}: {
|
||||
dashboardId?: string;
|
||||
dataView: DataView;
|
||||
}) => {
|
||||
return dashboardId ? (
|
||||
<>
|
||||
<EuiTitle>
|
||||
<h2>Static, by reference example</h2>
|
||||
</EuiTitle>
|
||||
<EuiText>
|
||||
<p>
|
||||
Loads a static, non-editable version of the <EuiCode>[Logs] Web Traffic</EuiCode>{' '}
|
||||
dashboard, excluding any logs with an operating system of <EuiCode>win xip</EuiCode>.
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPanel
|
||||
hasBorder={true}
|
||||
// By specifying the height of the EuiPanel, we make it so that the dashboard height is
|
||||
// constrained to the container - so, the dashboard is rendered with a vertical scrollbar
|
||||
css={css`
|
||||
height: 600px;
|
||||
`}
|
||||
>
|
||||
<DashboardContainerRenderer
|
||||
savedObjectId={dashboardId}
|
||||
getCreationOptions={async () => {
|
||||
const field = dataView.getFieldByName('machine.os.keyword');
|
||||
let filter: Filter;
|
||||
let creationOptions: DashboardCreationOptions = {
|
||||
initialInput: { viewMode: ViewMode.VIEW },
|
||||
};
|
||||
if (field) {
|
||||
filter = buildPhraseFilter(field, 'win xp', dataView);
|
||||
filter.meta.negate = true;
|
||||
creationOptions = { ...creationOptions, overrideInput: { filters: [filter] } };
|
||||
}
|
||||
return creationOptions; // if can't find the field, then just return no special creation options
|
||||
}}
|
||||
onDashboardContainerLoaded={(container) => {
|
||||
return; // this example is static, so don't need to do anything with the dashboard container
|
||||
}}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
) : (
|
||||
<div>Ensure that the web logs sample dashboard is loaded to view this example.</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import type { DashboardPanelMap } from '@kbn/dashboard-plugin/common';
|
||||
import { LazyDashboardContainerRenderer } from '@kbn/dashboard-plugin/public';
|
||||
import { withSuspense } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import panelsJson from './static_by_value_example_panels.json';
|
||||
|
||||
const DashboardContainerRenderer = withSuspense(LazyDashboardContainerRenderer);
|
||||
|
||||
export const StaticByValueExample = () => {
|
||||
return (
|
||||
<>
|
||||
<EuiTitle>
|
||||
<h2>Static, by value example</h2>
|
||||
</EuiTitle>
|
||||
<EuiText>
|
||||
<p>Creates and displays static, non-editable by value dashboard.</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPanel hasBorder={true}>
|
||||
<DashboardContainerRenderer
|
||||
getCreationOptions={async () => {
|
||||
return {
|
||||
initialInput: {
|
||||
timeRange: { from: 'now-30d', to: 'now' },
|
||||
viewMode: ViewMode.VIEW,
|
||||
panels: panelsJson as DashboardPanelMap,
|
||||
},
|
||||
};
|
||||
}}
|
||||
onDashboardContainerLoaded={(container) => {
|
||||
return; // this example is static, so don't need to do anything with the dashboard container
|
||||
}}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,335 @@
|
|||
{
|
||||
"a514e5f6-1d0d-4fe9-85a9-f7ba40665033": {
|
||||
"type": "visualization",
|
||||
"gridData": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 28,
|
||||
"h": 10,
|
||||
"i": "a514e5f6-1d0d-4fe9-85a9-f7ba40665033"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "a514e5f6-1d0d-4fe9-85a9-f7ba40665033",
|
||||
"savedVis": {
|
||||
"id": "",
|
||||
"title": "",
|
||||
"description": "",
|
||||
"type": "markdown",
|
||||
"params": {
|
||||
"fontSize": 12,
|
||||
"openLinksInNewTab": false,
|
||||
"markdown": "### By Value Dashboard\nThis dashboard is currently being loaded using some pre-configured JSON for the panels. This isn't ideal, and there are plans to improve the way that by-value embedded dashboards are handled - specifically, we want to add the ability to create a by-value dashboard input as part of the `getCreationOptions` callback.\n\nThat being said, we currently recommend by-reference dashboards until this process can be improved. "
|
||||
},
|
||||
"uiState": {},
|
||||
"data": {
|
||||
"aggs": [],
|
||||
"searchSource": {
|
||||
"query": {
|
||||
"query": "",
|
||||
"language": "kuery"
|
||||
},
|
||||
"filter": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"enhancements": {}
|
||||
}
|
||||
},
|
||||
"b06b849e-f4fd-423c-a582-5c4bfec812c9": {
|
||||
"type": "lens",
|
||||
"gridData": {
|
||||
"x": 30,
|
||||
"y": 0,
|
||||
"w": 20,
|
||||
"h": 21,
|
||||
"i": "b06b849e-f4fd-423c-a582-5c4bfec812c9"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "b06b849e-f4fd-423c-a582-5c4bfec812c9",
|
||||
"title": "Destinations",
|
||||
"attributes": {
|
||||
"title": "",
|
||||
"visualizationType": "lnsPie",
|
||||
"type": "lens",
|
||||
"references": [
|
||||
{
|
||||
"type": "index-pattern",
|
||||
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
|
||||
"name": "indexpattern-datasource-layer-40169fc7-829a-4158-b280-b2e058a980c0"
|
||||
}
|
||||
],
|
||||
"state": {
|
||||
"visualization": {
|
||||
"shape": "donut",
|
||||
"layers": [
|
||||
{
|
||||
"layerId": "40169fc7-829a-4158-b280-b2e058a980c0",
|
||||
"primaryGroups": ["5cb6a35a-6ae5-4463-b3b2-639d04824cc2"],
|
||||
"metrics": ["7f731486-71f2-40d9-8069-ef3fdb5ed2e7"],
|
||||
"numberDisplay": "percent",
|
||||
"categoryDisplay": "default",
|
||||
"legendDisplay": "default",
|
||||
"nestedLegend": false,
|
||||
"layerType": "data"
|
||||
}
|
||||
]
|
||||
},
|
||||
"query": {
|
||||
"query": "",
|
||||
"language": "kuery"
|
||||
},
|
||||
"filters": [],
|
||||
"datasourceStates": {
|
||||
"formBased": {
|
||||
"layers": {
|
||||
"40169fc7-829a-4158-b280-b2e058a980c0": {
|
||||
"columns": {
|
||||
"5cb6a35a-6ae5-4463-b3b2-639d04824cc2": {
|
||||
"label": "Top 5 values of geo.dest",
|
||||
"dataType": "string",
|
||||
"operationType": "terms",
|
||||
"scale": "ordinal",
|
||||
"sourceField": "geo.dest",
|
||||
"isBucketed": true,
|
||||
"params": {
|
||||
"size": 5,
|
||||
"orderBy": {
|
||||
"type": "column",
|
||||
"columnId": "7f731486-71f2-40d9-8069-ef3fdb5ed2e7"
|
||||
},
|
||||
"orderDirection": "desc",
|
||||
"otherBucket": true,
|
||||
"missingBucket": false,
|
||||
"parentFormat": {
|
||||
"id": "terms"
|
||||
},
|
||||
"include": [],
|
||||
"exclude": [],
|
||||
"includeIsRegex": false,
|
||||
"excludeIsRegex": false
|
||||
}
|
||||
},
|
||||
"7f731486-71f2-40d9-8069-ef3fdb5ed2e7": {
|
||||
"label": "Count of records",
|
||||
"dataType": "number",
|
||||
"operationType": "count",
|
||||
"isBucketed": false,
|
||||
"scale": "ratio",
|
||||
"sourceField": "___records___",
|
||||
"params": {
|
||||
"emptyAsNull": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"columnOrder": [
|
||||
"5cb6a35a-6ae5-4463-b3b2-639d04824cc2",
|
||||
"7f731486-71f2-40d9-8069-ef3fdb5ed2e7"
|
||||
],
|
||||
"incompleteColumns": {},
|
||||
"sampling": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"textBased": {
|
||||
"layers": {}
|
||||
}
|
||||
},
|
||||
"internalReferences": [],
|
||||
"adHocDataViews": {}
|
||||
}
|
||||
},
|
||||
"hidePanelTitles": false,
|
||||
"enhancements": {}
|
||||
}
|
||||
},
|
||||
"a4121cab-b6f2-4de3-af71-ec9b5a6f0a2a": {
|
||||
"type": "lens",
|
||||
"gridData": {
|
||||
"x": 0,
|
||||
"y": 9,
|
||||
"w": 28,
|
||||
"h": 11,
|
||||
"i": "a4121cab-b6f2-4de3-af71-ec9b5a6f0a2a"
|
||||
},
|
||||
"explicitInput": {
|
||||
"id": "a4121cab-b6f2-4de3-af71-ec9b5a6f0a2a",
|
||||
"enhancements": {},
|
||||
"attributes": {
|
||||
"visualizationType": "lnsXY",
|
||||
"state": {
|
||||
"datasourceStates": {
|
||||
"formBased": {
|
||||
"layers": {
|
||||
"7d9a32b1-8cc2-410c-83a5-2eb66a3f0321": {
|
||||
"columnOrder": [
|
||||
"a8511a62-2b78-4ba4-9425-a417df6e059f",
|
||||
"b5f3dc78-dba8-4db8-87b6-24a0b9cca260",
|
||||
"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X0",
|
||||
"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X1",
|
||||
"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X2",
|
||||
"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X3"
|
||||
],
|
||||
"columns": {
|
||||
"a8511a62-2b78-4ba4-9425-a417df6e059f": {
|
||||
"dataType": "number",
|
||||
"isBucketed": true,
|
||||
"label": "bytes",
|
||||
"operationType": "range",
|
||||
"params": {
|
||||
"maxBars": "auto",
|
||||
"ranges": [
|
||||
{
|
||||
"from": 0,
|
||||
"label": "",
|
||||
"to": 1000
|
||||
}
|
||||
],
|
||||
"type": "histogram"
|
||||
},
|
||||
"scale": "interval",
|
||||
"sourceField": "bytes"
|
||||
},
|
||||
"b5f3dc78-dba8-4db8-87b6-24a0b9cca260": {
|
||||
"customLabel": true,
|
||||
"dataType": "number",
|
||||
"isBucketed": false,
|
||||
"label": "% of visits",
|
||||
"operationType": "formula",
|
||||
"params": {
|
||||
"format": {
|
||||
"id": "percent",
|
||||
"params": {
|
||||
"decimals": 1
|
||||
}
|
||||
},
|
||||
"formula": "count() / overall_sum(count())",
|
||||
"isFormulaBroken": false
|
||||
},
|
||||
"references": ["b5f3dc78-dba8-4db8-87b6-24a0b9cca260X3"],
|
||||
"scale": "ratio"
|
||||
},
|
||||
"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X0": {
|
||||
"customLabel": true,
|
||||
"dataType": "number",
|
||||
"isBucketed": false,
|
||||
"label": "Part of count() / overall_sum(count())",
|
||||
"operationType": "count",
|
||||
"scale": "ratio",
|
||||
"sourceField": "___records___"
|
||||
},
|
||||
"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X1": {
|
||||
"customLabel": true,
|
||||
"dataType": "number",
|
||||
"isBucketed": false,
|
||||
"label": "Part of count() / overall_sum(count())",
|
||||
"operationType": "count",
|
||||
"scale": "ratio",
|
||||
"sourceField": "___records___"
|
||||
},
|
||||
"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X2": {
|
||||
"customLabel": true,
|
||||
"dataType": "number",
|
||||
"isBucketed": false,
|
||||
"label": "Part of count() / overall_sum(count())",
|
||||
"operationType": "overall_sum",
|
||||
"references": ["b5f3dc78-dba8-4db8-87b6-24a0b9cca260X1"],
|
||||
"scale": "ratio"
|
||||
},
|
||||
"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X3": {
|
||||
"customLabel": true,
|
||||
"dataType": "number",
|
||||
"isBucketed": false,
|
||||
"label": "Part of count() / overall_sum(count())",
|
||||
"operationType": "math",
|
||||
"params": {
|
||||
"tinymathAst": {
|
||||
"args": [
|
||||
"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X0",
|
||||
"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X2"
|
||||
],
|
||||
"location": {
|
||||
"max": 30,
|
||||
"min": 0
|
||||
},
|
||||
"name": "divide",
|
||||
"text": "count() / overall_sum(count())",
|
||||
"type": "function"
|
||||
}
|
||||
},
|
||||
"references": [
|
||||
"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X0",
|
||||
"b5f3dc78-dba8-4db8-87b6-24a0b9cca260X2"
|
||||
],
|
||||
"scale": "ratio"
|
||||
}
|
||||
},
|
||||
"incompleteColumns": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"filters": [],
|
||||
"query": {
|
||||
"language": "kuery",
|
||||
"query": ""
|
||||
},
|
||||
"visualization": {
|
||||
"axisTitlesVisibilitySettings": {
|
||||
"x": false,
|
||||
"yLeft": false,
|
||||
"yRight": true
|
||||
},
|
||||
"fittingFunction": "None",
|
||||
"gridlinesVisibilitySettings": {
|
||||
"x": true,
|
||||
"yLeft": true,
|
||||
"yRight": true
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"accessors": ["b5f3dc78-dba8-4db8-87b6-24a0b9cca260"],
|
||||
"layerId": "7d9a32b1-8cc2-410c-83a5-2eb66a3f0321",
|
||||
"position": "top",
|
||||
"seriesType": "bar_stacked",
|
||||
"showGridlines": false,
|
||||
"xAccessor": "a8511a62-2b78-4ba4-9425-a417df6e059f",
|
||||
"layerType": "data"
|
||||
}
|
||||
],
|
||||
"legend": {
|
||||
"isVisible": true,
|
||||
"position": "right",
|
||||
"legendSize": "auto"
|
||||
},
|
||||
"preferredSeriesType": "bar_stacked",
|
||||
"tickLabelsVisibilitySettings": {
|
||||
"x": true,
|
||||
"yLeft": true,
|
||||
"yRight": true
|
||||
},
|
||||
"valueLabels": "hide",
|
||||
"yLeftExtent": {
|
||||
"mode": "full"
|
||||
},
|
||||
"yRightExtent": {
|
||||
"mode": "full"
|
||||
}
|
||||
}
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
|
||||
"name": "indexpattern-datasource-current-indexpattern",
|
||||
"type": "index-pattern"
|
||||
},
|
||||
{
|
||||
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
|
||||
"name": "indexpattern-datasource-layer-7d9a32b1-8cc2-410c-83a5-2eb66a3f0321",
|
||||
"type": "index-pattern"
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": "[Logs] Bytes distribution"
|
||||
}
|
||||
}
|
||||
}
|
30
examples/portable_dashboards_example/tsconfig.json
Normal file
30
examples/portable_dashboards_example/tsconfig.json
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"public/**/*.ts",
|
||||
"public/**/*.tsx",
|
||||
"public/**/*.json",
|
||||
"../../typings/**/*"
|
||||
],
|
||||
"exclude": ["target/**/*"],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/es-query",
|
||||
"@kbn/data-plugin",
|
||||
"@kbn/dashboard-plugin",
|
||||
"@kbn/navigation-plugin",
|
||||
"@kbn/embeddable-plugin",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/visualizations-plugin",
|
||||
"@kbn/presentation-util-plugin",
|
||||
"@kbn/developer-examples-plugin",
|
||||
"@kbn/embeddable-examples-plugin",
|
||||
"@kbn/shared-ux-page-kibana-template",
|
||||
"@kbn/shared-ux-utility",
|
||||
"@kbn/controls-plugin"
|
||||
]
|
||||
}
|
|
@ -341,7 +341,6 @@
|
|||
"@kbn/crypto-browser": "link:packages/kbn-crypto-browser",
|
||||
"@kbn/custom-branding-plugin": "link:x-pack/plugins/custom_branding",
|
||||
"@kbn/custom-integrations-plugin": "link:src/plugins/custom_integrations",
|
||||
"@kbn/dashboard-embeddable-examples-plugin": "link:examples/dashboard_embeddable_examples",
|
||||
"@kbn/dashboard-enhanced-plugin": "link:x-pack/plugins/dashboard_enhanced",
|
||||
"@kbn/dashboard-plugin": "link:src/plugins/dashboard",
|
||||
"@kbn/data-plugin": "link:src/plugins/data",
|
||||
|
@ -495,6 +494,7 @@
|
|||
"@kbn/osquery-plugin": "link:x-pack/plugins/osquery",
|
||||
"@kbn/paertial-results-example-plugin": "link:examples/partial_results_example",
|
||||
"@kbn/painless-lab-plugin": "link:x-pack/plugins/painless_lab",
|
||||
"@kbn/portable-dashboards-example": "link:examples/portable_dashboards_example",
|
||||
"@kbn/preboot-example-plugin": "link:examples/preboot_example",
|
||||
"@kbn/presentation-util-plugin": "link:src/plugins/presentation_util",
|
||||
"@kbn/profiling-plugin": "link:x-pack/plugins/profiling",
|
||||
|
|
|
@ -115,11 +115,11 @@ export function DashboardApp({
|
|||
* Create options to pass into the dashboard renderer
|
||||
*/
|
||||
const stateFromLocator = loadDashboardHistoryLocationState(getScopedHistory);
|
||||
const getCreationOptions = useCallback((): DashboardCreationOptions => {
|
||||
const getCreationOptions = useCallback((): Promise<DashboardCreationOptions> => {
|
||||
const initialUrlState = loadAndRemoveDashboardState(kbnUrlStateStorage);
|
||||
const searchSessionIdFromURL = getSearchSessionIdFromURL(history);
|
||||
|
||||
return {
|
||||
return Promise.resolve({
|
||||
incomingEmbeddable,
|
||||
|
||||
// integrations
|
||||
|
@ -151,7 +151,7 @@ export function DashboardApp({
|
|||
},
|
||||
|
||||
validateLoadedSavedObject: validateOutcome,
|
||||
};
|
||||
});
|
||||
}, [
|
||||
history,
|
||||
validateOutcome,
|
||||
|
|
|
@ -22,7 +22,7 @@ import { useEuiTheme } from '@elastic/eui';
|
|||
import { css } from '@emotion/react';
|
||||
import { dashboardReplacePanelActionStrings } from '../../dashboard_actions/_dashboard_actions_strings';
|
||||
import { DASHBOARD_APP_ID, DASHBOARD_UI_METRIC_ID } from '../../dashboard_constants';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container/dashboard_container_renderer';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container/dashboard_container_context';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { getCreateVisualizationButtonTitle } from '../_dashboard_app_strings';
|
||||
import { EditorMenu } from './editor_menu';
|
||||
|
|
|
@ -31,7 +31,7 @@ import { DashboardEmbedSettings, DashboardRedirect } from '../types';
|
|||
import { DashboardEditingToolbar } from './dashboard_editing_toolbar';
|
||||
import { useDashboardMountContext } from '../hooks/dashboard_mount_context';
|
||||
import { getFullEditPath, LEGACY_DASHBOARD_APP_ID } from '../../dashboard_constants';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container/dashboard_container_renderer';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container/dashboard_container_context';
|
||||
|
||||
export interface DashboardTopNavProps {
|
||||
embedSettings?: DashboardEmbedSettings;
|
||||
|
|
|
@ -19,7 +19,7 @@ import { ShowShareModal } from './share/show_share_modal';
|
|||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { CHANGE_CHECK_DEBOUNCE } from '../../dashboard_constants';
|
||||
import { SaveDashboardReturn } from '../../services/dashboard_saved_object/types';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container/dashboard_container_renderer';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container/dashboard_container_context';
|
||||
import { confirmDiscardUnsavedChanges } from '../listing/confirm_overlays';
|
||||
|
||||
export const useDashboardMenuItems = ({
|
||||
|
|
|
@ -18,7 +18,7 @@ import { ViewMode, EmbeddablePhaseEvent } from '@kbn/embeddable-plugin/public';
|
|||
|
||||
import { DashboardPanelState } from '../../../../common';
|
||||
import { DashboardGridItem } from './dashboard_grid_item';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container_renderer';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container_context';
|
||||
import { DashboardLoadedEventStatus, DashboardRenderPerformanceStats } from '../../types';
|
||||
import { DASHBOARD_GRID_COLUMN_COUNT, DASHBOARD_GRID_HEIGHT } from '../../../dashboard_constants';
|
||||
import { getPanelLayoutsAreEqual } from '../../embeddable/integrations/diff_state/dashboard_diffing_utils';
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
|
||||
import { DashboardPanelState } from '../../../../common';
|
||||
import { pluginServices } from '../../../services/plugin_services';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container_renderer';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container_context';
|
||||
|
||||
type DivProps = Pick<React.HTMLAttributes<HTMLDivElement>, 'className' | 'style' | 'children'>;
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import { EuiPortal } from '@elastic/eui';
|
|||
import { DashboardGrid } from '../grid';
|
||||
import { pluginServices } from '../../../services/plugin_services';
|
||||
import { DashboardEmptyScreen } from '../empty_screen/dashboard_empty_screen';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container_renderer';
|
||||
import { useDashboardContainerContext } from '../../dashboard_container_context';
|
||||
|
||||
export const DashboardViewportComponent = () => {
|
||||
const {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
import { dashboardContainerReducers } from './state/dashboard_container_reducers';
|
||||
import { DashboardReduxState } from './types';
|
||||
import { DashboardContainer } from '..';
|
||||
|
||||
export const useDashboardContainerContext = () =>
|
||||
useReduxEmbeddableContext<
|
||||
DashboardReduxState,
|
||||
typeof dashboardContainerReducers,
|
||||
DashboardContainer
|
||||
>();
|
|
@ -15,7 +15,6 @@ import useObservable from 'react-use/lib/useObservable';
|
|||
|
||||
import { EuiLoadingElastic, EuiLoadingSpinner, useEuiOverflowScroll } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import {
|
||||
DashboardContainerFactory,
|
||||
|
@ -23,15 +22,13 @@ import {
|
|||
DashboardCreationOptions,
|
||||
} from './embeddable/dashboard_container_factory';
|
||||
import { DASHBOARD_CONTAINER_TYPE } from '..';
|
||||
import { DashboardReduxState } from './types';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { DEFAULT_DASHBOARD_INPUT } from '../dashboard_constants';
|
||||
import { DashboardContainer } from './embeddable/dashboard_container';
|
||||
import { dashboardContainerReducers } from './state/dashboard_container_reducers';
|
||||
|
||||
export interface DashboardContainerRendererProps {
|
||||
savedObjectId?: string;
|
||||
getCreationOptions?: () => DashboardCreationOptions;
|
||||
getCreationOptions?: () => Promise<DashboardCreationOptions>;
|
||||
onDashboardContainerLoaded?: (dashboardContainer: DashboardContainer) => void;
|
||||
}
|
||||
|
||||
|
@ -68,7 +65,7 @@ export const DashboardContainerRenderer = ({
|
|||
let destroyContainer: () => void;
|
||||
|
||||
(async () => {
|
||||
const creationOptions = getCreationOptions?.();
|
||||
const creationOptions = await getCreationOptions?.();
|
||||
const dashboardFactory = embeddable.getEmbeddableFactory(
|
||||
DASHBOARD_CONTAINER_TYPE
|
||||
) as DashboardContainerFactory & { create: DashboardContainerFactoryDefinition['create'] };
|
||||
|
@ -128,13 +125,6 @@ export const DashboardContainerRenderer = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const useDashboardContainerContext = () =>
|
||||
useReduxEmbeddableContext<
|
||||
DashboardReduxState,
|
||||
typeof dashboardContainerReducers,
|
||||
DashboardContainer
|
||||
>();
|
||||
|
||||
// required for dynamic import using React.lazy()
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default DashboardContainerRenderer;
|
||||
|
|
|
@ -10,7 +10,7 @@ import React from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiForm, EuiFormRow, EuiSwitch } from '@elastic/eui';
|
||||
import { useDashboardContainerContext } from '../../../dashboard_container_renderer';
|
||||
import { useDashboardContainerContext } from '../../../dashboard_container_context';
|
||||
|
||||
export const DashboardOptions = () => {
|
||||
const {
|
||||
|
|
|
@ -6,10 +6,18 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export const DASHBOARD_CONTAINER_TYPE = 'dashboard';
|
||||
|
||||
export { useDashboardContainerContext } from './dashboard_container_context';
|
||||
export const LazyDashboardContainerRenderer = React.lazy(
|
||||
() => import('./dashboard_container_renderer')
|
||||
);
|
||||
|
||||
export type { DashboardContainer } from './embeddable/dashboard_container';
|
||||
export {
|
||||
type DashboardContainerFactory,
|
||||
type DashboardCreationOptions,
|
||||
DashboardContainerFactoryDefinition,
|
||||
} from './embeddable/dashboard_container_factory';
|
||||
|
|
|
@ -7,15 +7,20 @@
|
|||
*/
|
||||
|
||||
import { PluginInitializerContext } from '@kbn/core/public';
|
||||
import { DashboardPlugin } from './plugin';
|
||||
|
||||
import { DashboardPlugin } from './plugin';
|
||||
export {
|
||||
createDashboardEditUrl,
|
||||
DASHBOARD_APP_ID,
|
||||
LEGACY_DASHBOARD_APP_ID,
|
||||
} from './dashboard_constants';
|
||||
export { DASHBOARD_CONTAINER_TYPE } from './dashboard_container';
|
||||
export type { DashboardContainer } from './dashboard_container/embeddable/dashboard_container';
|
||||
export {
|
||||
DASHBOARD_CONTAINER_TYPE,
|
||||
type DashboardContainer,
|
||||
type DashboardCreationOptions,
|
||||
LazyDashboardContainerRenderer,
|
||||
useDashboardContainerContext,
|
||||
} from './dashboard_container';
|
||||
export type { DashboardSetup, DashboardStart, DashboardFeatureFlagConfig } from './plugin';
|
||||
|
||||
export {
|
||||
|
|
|
@ -63,6 +63,7 @@ import {
|
|||
} from './dashboard_constants';
|
||||
import { PlaceholderEmbeddableFactory } from './placeholder_embeddable';
|
||||
import { DashboardMountContextProps } from './dashboard_app/types';
|
||||
import type { FindDashboardsService } from './services/dashboard_saved_object/types';
|
||||
|
||||
export interface DashboardFeatureFlagConfig {
|
||||
allowByValueEmbeddables: boolean;
|
||||
|
@ -108,6 +109,7 @@ export interface DashboardSetup {
|
|||
export interface DashboardStart {
|
||||
locator?: DashboardAppLocator;
|
||||
dashboardFeatureFlagConfig: DashboardFeatureFlagConfig;
|
||||
findDashboardsService: () => Promise<FindDashboardsService>;
|
||||
}
|
||||
|
||||
export class DashboardPlugin
|
||||
|
@ -306,6 +308,13 @@ export class DashboardPlugin
|
|||
return {
|
||||
locator: this.locator,
|
||||
dashboardFeatureFlagConfig: this.dashboardFeatureFlagConfig!,
|
||||
findDashboardsService: async () => {
|
||||
const { pluginServices } = await import('./services/plugin_services');
|
||||
const {
|
||||
dashboardSavedObject: { findDashboards },
|
||||
} = pluginServices.getServices();
|
||||
return findDashboards;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,18 @@ export interface DashboardSavedObjectRequiredServices {
|
|||
savedObjectsTagging: DashboardSavedObjectsTaggingService;
|
||||
dashboardSessionStorage: DashboardSessionStorageServiceType;
|
||||
}
|
||||
|
||||
export interface FindDashboardsService {
|
||||
findSavedObjects: (
|
||||
props: Pick<
|
||||
FindDashboardSavedObjectsArgs,
|
||||
'hasReference' | 'hasNoReference' | 'search' | 'size'
|
||||
>
|
||||
) => Promise<FindDashboardSavedObjectsResponse>;
|
||||
findByIds: (ids: string[]) => Promise<FindDashboardBySavedObjectIdsResult[]>;
|
||||
findByTitle: (title: string) => Promise<{ id: string } | undefined>;
|
||||
}
|
||||
|
||||
export interface DashboardSavedObjectService {
|
||||
loadDashboardStateFromSavedObject: (
|
||||
props: Pick<LoadDashboardFromSavedObjectProps, 'id'>
|
||||
|
@ -50,16 +62,7 @@ export interface DashboardSavedObjectService {
|
|||
saveDashboardStateToSavedObject: (
|
||||
props: Pick<SaveDashboardProps, 'currentState' | 'saveOptions' | 'lastSavedId'>
|
||||
) => Promise<SaveDashboardReturn>;
|
||||
findDashboards: {
|
||||
findSavedObjects: (
|
||||
props: Pick<
|
||||
FindDashboardSavedObjectsArgs,
|
||||
'hasReference' | 'hasNoReference' | 'search' | 'size'
|
||||
>
|
||||
) => Promise<FindDashboardSavedObjectsResponse>;
|
||||
findByIds: (ids: string[]) => Promise<FindDashboardBySavedObjectIdsResult[]>;
|
||||
findByTitle: (title: string) => Promise<{ id: string } | undefined>;
|
||||
};
|
||||
findDashboards: FindDashboardsService;
|
||||
checkForDuplicateDashboardTitle: (meta: DashboardDuplicateTitleCheckProps) => Promise<boolean>;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
# Embeddables Plugin
|
||||
|
||||
The Embeddables Plugin provides an opportunity to expose reusable interactive widgets that can be embedded outside the original plugin.
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Framework-agnostic API.
|
||||
- Out-of-the-box React support.
|
||||
- Integration with Redux.
|
||||
|
@ -10,33 +12,41 @@ The Embeddables Plugin provides an opportunity to expose reusable interactive wi
|
|||
- Error handling.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Embeddable
|
||||
|
||||
Embeddable is a re-usable widget that can be rendered on a dashboard as well as in other applications.
|
||||
Developers are free to embed them directly in their plugins.
|
||||
End users can dynamically select an embeddable to add to a _container_.
|
||||
Dashboard container powers the grid of panels on the Dashboard app.
|
||||
|
||||
### Container
|
||||
|
||||
Container is a special type of embeddable that can hold other embeddable items.
|
||||
Embeddables can be added dynamically to the containers, but that should be implemented on the end plugin side.
|
||||
Currently, the dashboard plugin provides such functionality.
|
||||
|
||||
### Input
|
||||
|
||||
Every embeddable has an input which is a serializable set of data.
|
||||
This data can be used to update the state of the embeddable widget.
|
||||
The input can be updated later so that the embeddable should be capable of reacting to those changes.
|
||||
|
||||
### Output
|
||||
|
||||
Every embeddable may expose some data to the external interface.
|
||||
Usually, it is diverged from the input and not necessarily serializable.
|
||||
Output data can also be updated, but that should always be done inside the embeddable.
|
||||
|
||||
## Usage
|
||||
|
||||
### Getting Started
|
||||
|
||||
After listing the `embeddable` plugin in your dependencies, the plugin will be intitalized on the setup stage.
|
||||
|
||||
The setup contract exposes a handle to register an embeddable factory.
|
||||
At this point, we can provide all the dependencies needed for the widget via the factory.
|
||||
|
||||
```typescript
|
||||
import { EmbeddableSetup } from '@kbn/embeddable-plugin/public';
|
||||
import { HELLO_WORLD } from './hello_world';
|
||||
|
@ -61,6 +71,7 @@ export function plugin() {
|
|||
|
||||
The factory should implement the `EmbeddableFactoryDefinition` interface.
|
||||
At this stage, we can inject all the dependencies into the embeddable instance.
|
||||
|
||||
```typescript
|
||||
import {
|
||||
IContainer,
|
||||
|
@ -86,8 +97,8 @@ export class HelloWorldEmbeddableFactoryDefinition implements EmbeddableFactoryD
|
|||
}
|
||||
```
|
||||
|
||||
|
||||
The embeddable should implement the `IEmbeddable` interface, and usually, that just extends the base class `Embeddable`.
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { Embeddable } from '@kbn/embeddable-plugin/public';
|
||||
|
@ -106,11 +117,14 @@ export class HelloWorld extends Embeddable {
|
|||
```
|
||||
|
||||
### Life-Cycle Hooks
|
||||
|
||||
Every embeddable can implement a specific behavior for the following life-cycle stages.
|
||||
|
||||
#### `render`
|
||||
|
||||
This is a mandatory method to implement.
|
||||
It is used for the initial render of the embeddable.
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
|
@ -127,6 +141,7 @@ export class HelloWorld extends Embeddable {
|
|||
|
||||
There is also an option to return a [React node](https://reactjs.org/docs/react-component.html#render) directly.
|
||||
In that case, the returned node will be automatically mounted and unmounted.
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { Embeddable } from '@kbn/embeddable-plugin/public';
|
||||
|
@ -141,7 +156,9 @@ export class HelloWorld extends Embeddable {
|
|||
```
|
||||
|
||||
#### `reload`
|
||||
|
||||
This hook is called after every input update to perform some UI changes.
|
||||
|
||||
```typescript
|
||||
import { Embeddable } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
|
@ -198,9 +215,9 @@ const component = createSlice({
|
|||
export class HelloWorld extends Embeddable {
|
||||
readonly store = createStore<HelloWorld, HelloWorldState>(this, {
|
||||
preloadedState: {
|
||||
component: {}
|
||||
component: {},
|
||||
},
|
||||
reducer: { component: component.reducer }
|
||||
reducer: { component: component.reducer },
|
||||
});
|
||||
|
||||
render() {
|
||||
|
@ -220,6 +237,7 @@ export class HelloWorld extends Embeddable {
|
|||
Alternatively, a [state modifier](https://reactjs.org/docs/hooks-faq.html#is-there-something-like-forceupdate) can be exposed via a reference object and later called from the `reload` hook.
|
||||
|
||||
#### `catchError`
|
||||
|
||||
This is an optional error handler to provide a custom UI for the error state.
|
||||
|
||||
The embeddable may change its state in the future so that the error should be able to disappear.
|
||||
|
@ -248,6 +266,7 @@ export class HelloWorld extends Embeddable {
|
|||
|
||||
There is also an option to return a [React node](https://reactjs.org/docs/react-component.html#render) directly.
|
||||
In that case, the returned node will be automatically mounted and unmounted.
|
||||
|
||||
```typescript
|
||||
import React from 'react';
|
||||
import { Embeddable } from '@kbn/embeddable-plugin/public';
|
||||
|
@ -262,7 +281,9 @@ export class HelloWorld extends Embeddable {
|
|||
```
|
||||
|
||||
#### `destroy`
|
||||
|
||||
This hook is invoked when the embeddable is destroyed and should perform cleanup actions.
|
||||
|
||||
```typescript
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
|
@ -286,7 +307,9 @@ export class HelloWorld extends Embeddable {
|
|||
```
|
||||
|
||||
### Input State
|
||||
|
||||
The input state can be updated throughout the lifecycle of an embeddable. That can be done via `updateInput` method call.
|
||||
|
||||
```typescript
|
||||
import { Embeddable } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
|
@ -303,8 +326,10 @@ For example, the time range on a dashboard is _inherited_ by all children _unles
|
|||
This is the way the per panel time range works. In that case, there is a call `item.updateInput({ timeRange })` that detaches the time range from the container.
|
||||
|
||||
### Containers
|
||||
|
||||
The plugin provides a way to organize a collection of embeddable widgets inside containers.
|
||||
In this case, the container holds the state of all the children and manages all the input state updates.
|
||||
|
||||
```typescript
|
||||
import { Container } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
|
@ -324,11 +349,13 @@ _Note 2:_ Keep in mind that this input state will be passed down to all the chil
|
|||
It is better to return only necessary generic information that all children will likely consume.
|
||||
|
||||
### Inheritance
|
||||
|
||||
In the example above, all the container children will share the `timeRange` and `viewMode` properties.
|
||||
If the container has other properties in the input state, they will not be shared with the children.
|
||||
From the embeddable point, that works transparently, and there is no difference whether the embeddable is placed inside a container or not.
|
||||
|
||||
Let's take, for example, a container with the following input:
|
||||
|
||||
```typescript
|
||||
{
|
||||
gridData: { /* ... */ },
|
||||
|
@ -356,6 +383,7 @@ Let's take, for example, a container with the following input:
|
|||
```
|
||||
|
||||
That could result in the following input being passed to a child embeddable:
|
||||
|
||||
```typescript
|
||||
{
|
||||
timeRange: 'now-15m to now',
|
||||
|
@ -364,10 +392,12 @@ That could result in the following input being passed to a child embeddable:
|
|||
```
|
||||
|
||||
#### Input Overriding
|
||||
|
||||
There is a way of _overriding_ this inherited state.
|
||||
For example, the _inherited_ `timeRange` input can be overridden by the _explicit_ `timeRange` input.
|
||||
|
||||
Let's take this example dashboard container input:
|
||||
|
||||
```javascript
|
||||
{
|
||||
gridData: { /* ... */ },
|
||||
|
@ -390,6 +420,7 @@ Let's take this example dashboard container input:
|
|||
```
|
||||
|
||||
The first child embeddable will get the following state:
|
||||
|
||||
```javascript
|
||||
{
|
||||
timeRange: 'now-30m to now',
|
||||
|
@ -398,6 +429,7 @@ The first child embeddable will get the following state:
|
|||
```
|
||||
|
||||
This override wouldn't affect other children, so the second child would get:
|
||||
|
||||
```javascript
|
||||
{
|
||||
timeRange: 'now-15m to now',
|
||||
|
@ -406,29 +438,35 @@ This override wouldn't affect other children, so the second child would get:
|
|||
```
|
||||
|
||||
#### Embeddable Id
|
||||
|
||||
The `id` parameter in the input is marked as required even though it is only used when the embeddable is inside a container.
|
||||
That is done to guarantee consistency.
|
||||
|
||||
This has nothing to do with a saved object id, even though in the dashboard app, the saved object happens to be the same.
|
||||
|
||||
#### Accessing Container
|
||||
|
||||
The parent container can be retrieved via either `embeddabble.parent` or `embeddable.getRoot()`.
|
||||
The `getRoot` variety will walk up to find the root parent.
|
||||
|
||||
We can use those to get an explicit input from the child embeddable:
|
||||
|
||||
```typescript
|
||||
return parent.getInput().panels[embeddable.id].explicitInput;
|
||||
return parent.getInput().panels[embeddable.id].explicitInput;
|
||||
```
|
||||
|
||||
#### Encapsulated Explicit Input
|
||||
|
||||
It is possible for a container to store an explicit input state on the embeddable side. It would be encapsulated from a container in this case.
|
||||
|
||||
This can ne achieved in two ways by implementing one of the following:
|
||||
|
||||
- `EmbeddableFactory.getExplicitInput` was intended as a way for an embeddable to retrieve input state it needs that is not provided by a container.
|
||||
- `EmbeddableFactory.getDefaultInput` will provide default values, only if the container did not supply them through inheritance.
|
||||
Explicit input will always provide these values, and will always be stored in a containers `panel[id].explicitInput`, even if the container _did_ provide it.
|
||||
Explicit input will always provide these values, and will always be stored in a containers `panel[id].explicitInput`, even if the container _did_ provide it.
|
||||
|
||||
### React
|
||||
|
||||
The plugin provides a set of ready-to-use React components that abstract rendering of an embeddable behind a React component:
|
||||
|
||||
- `EmbeddablePanel` provides a way to render an embeddable inside a rectangular panel. This also provides error handling and a basic user interface over some of the embeddable properties.
|
||||
|
@ -440,6 +478,7 @@ Apart from the React components, there is also a way to construct an embeddable
|
|||
This React hook takes care of producing an embeddable and updating its input state if passed state changes.
|
||||
|
||||
### Redux
|
||||
|
||||
The plugin provides an adapter for Redux over the embeddable state.
|
||||
It uses the Redux Toolkit library underneath and works as a decorator on top of the [`configureStore`](https://redux-toolkit.js.org/api/configureStore) function.
|
||||
In other words, it provides a way to use the full power of the library together with the embeddable plugin features.
|
||||
|
@ -448,6 +487,7 @@ The adapter implements a bi-directional sync mechanism between the embeddable in
|
|||
To perform state mutations, the plugin also exposes a pre-defined state of the actions that can be extended by an additional reducer.
|
||||
|
||||
Here is an example of initializing a Redux store:
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { connect, Provider } from 'react-redux';
|
||||
|
@ -479,6 +519,7 @@ export class HelloWorld extends Embeddable {
|
|||
```
|
||||
|
||||
Then inside the embedded component, it is possible to use the [`useSelector`](https://react-redux.js.org/api/hooks#useselector) and [`useDispatch`](https://react-redux.js.org/api/hooks#usedispatch) hooks.
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
@ -510,6 +551,7 @@ export function HelloWorldComponent({ title }: HelloWorldComponentProps) {
|
|||
```
|
||||
|
||||
#### Custom Properties
|
||||
|
||||
The `createStore` function provides an option to pass a custom reducer in the second argument.
|
||||
That reducer will be merged with the one the embeddable plugin provides.
|
||||
That means there is no need to reimplement already existing actions.
|
||||
|
@ -521,7 +563,7 @@ import {
|
|||
Embeddable,
|
||||
EmbeddableInput,
|
||||
EmbeddableOutput,
|
||||
IEmbeddable
|
||||
IEmbeddable,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { createStore, State } from '@kbn/embeddable-plugin/public/store';
|
||||
|
||||
|
@ -563,7 +605,7 @@ export class HelloWorld extends Embeddable<HelloWorldInput, HelloWorldOutput> {
|
|||
reducer: {
|
||||
input: input.reducer,
|
||||
output: output.reducer,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ...
|
||||
|
@ -571,6 +613,7 @@ export class HelloWorld extends Embeddable<HelloWorldInput, HelloWorldOutput> {
|
|||
```
|
||||
|
||||
There is a way to provide a custom reducer that will manipulate the root state:
|
||||
|
||||
```typescript
|
||||
// ...
|
||||
|
||||
|
@ -582,8 +625,14 @@ const setGreeting = createAction<HelloWorldInput['greeting']>('greeting');
|
|||
const setMessage = createAction<HelloWorldOutput['message']>('message');
|
||||
const reducer = createReducer({} as State<HelloWorld>, (builder) =>
|
||||
builder
|
||||
.addCase(setGreeting, (state, action) => ({ ...state, input: { ...state.input, greeting: action.payload } }))
|
||||
.addCase(setMessage, (state, action) => ({ ...state, output: { ...state.output, message: action.payload } }))
|
||||
.addCase(setGreeting, (state, action) => ({
|
||||
...state,
|
||||
input: { ...state.input, greeting: action.payload },
|
||||
}))
|
||||
.addCase(setMessage, (state, action) => ({
|
||||
...state,
|
||||
output: { ...state.output, message: action.payload },
|
||||
}))
|
||||
);
|
||||
|
||||
export const actions = {
|
||||
|
@ -599,6 +648,7 @@ export class HelloWorld extends Embeddable<HelloWorldInput, HelloWorldOutput> {
|
|||
```
|
||||
|
||||
#### Custom State
|
||||
|
||||
Sometimes, there is a need to store a custom state next to the embeddable state.
|
||||
This can be achieved by passing a custom reducer.
|
||||
|
||||
|
@ -638,9 +688,9 @@ export class HelloWorld extends Embeddable {
|
|||
component: {
|
||||
foo: 'bar',
|
||||
bar: 'foo',
|
||||
}
|
||||
},
|
||||
},
|
||||
reducer: { component: component.reducer }
|
||||
reducer: { component: component.reducer },
|
||||
});
|
||||
|
||||
// ...
|
||||
|
@ -648,9 +698,11 @@ export class HelloWorld extends Embeddable {
|
|||
```
|
||||
|
||||
#### Typings
|
||||
|
||||
When using the `useSelector` hook, it is convenient to have a `State` type to guarantee type safety and determine types implicitly.
|
||||
|
||||
For the state containing input and output substates only, it is enough to use a utility type `State`:
|
||||
|
||||
```typescript
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { State } from '@kbn/embeddable-plugin/public/store';
|
||||
|
@ -661,6 +713,7 @@ const title = useSelector<State<Embeddable>>((state) => state.input.title);
|
|||
```
|
||||
|
||||
For the more complex case, the best way would be to define a state type separately:
|
||||
|
||||
```typescript
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { State } from '@kbn/embeddable-plugin/public/store';
|
||||
|
@ -676,25 +729,30 @@ const foo = useSelector<EmbeddableState>((state) => state.foo);
|
|||
```
|
||||
|
||||
#### Advanced Usage
|
||||
|
||||
In case when there is a need to enhance the produced store in some way (e.g., perform custom serialization or debugging), it is possible to use [parameters](https://redux-toolkit.js.org/api/configureStore#parameters) supported by the `configureStore` function.
|
||||
|
||||
In case when custom serialization is needed, that should be done using middleware. The embeddable plugin's `createStore` function does not apply any middleware, so all the synchronization job is done outside the store.
|
||||
|
||||
## API
|
||||
|
||||
Please use automatically generated API reference or generated TypeDoc comments to find the complete documentation.
|
||||
|
||||
## Examples
|
||||
|
||||
- Multiple embeddable examples are implemented and registered [here](https://github.com/elastic/kibana/tree/HEAD/examples/embeddable_examples).
|
||||
- They can be played around with and explored in the [Embeddable Explorer](https://github.com/elastic/kibana/tree/HEAD/examples/embeddable_explorer) example plugin.
|
||||
- There is an [example](https://github.com/elastic/kibana/tree/HEAD/examples/dashboard_embeddable_examples) of rendering a dashboard container outside the dashboard app.
|
||||
- There is an [example](https://github.com/elastic/kibana/tree/HEAD/examples/portable_dashboards_example) of rendering a dashboard container outside the dashboard app.
|
||||
- There are storybook [stories](https://github.com/elastic/kibana/tree/HEAD/src/plugins/embeddable/public/__stories__) that demonstrate usage of the embeddable components.
|
||||
|
||||
To run the examples plugin use the following command:
|
||||
|
||||
```bash
|
||||
yarn start --run-examples
|
||||
```
|
||||
|
||||
To run the storybook:
|
||||
|
||||
```bash
|
||||
yarn storybook embeddable
|
||||
```
|
||||
|
|
|
@ -568,8 +568,6 @@
|
|||
"@kbn/custom-integrations-plugin/*": ["src/plugins/custom_integrations/*"],
|
||||
"@kbn/cypress-config": ["packages/kbn-cypress-config"],
|
||||
"@kbn/cypress-config/*": ["packages/kbn-cypress-config/*"],
|
||||
"@kbn/dashboard-embeddable-examples-plugin": ["examples/dashboard_embeddable_examples"],
|
||||
"@kbn/dashboard-embeddable-examples-plugin/*": ["examples/dashboard_embeddable_examples/*"],
|
||||
"@kbn/dashboard-enhanced-plugin": ["x-pack/plugins/dashboard_enhanced"],
|
||||
"@kbn/dashboard-enhanced-plugin/*": ["x-pack/plugins/dashboard_enhanced/*"],
|
||||
"@kbn/dashboard-plugin": ["src/plugins/dashboard"],
|
||||
|
@ -948,6 +946,8 @@
|
|||
"@kbn/plugin-generator/*": ["packages/kbn-plugin-generator/*"],
|
||||
"@kbn/plugin-helpers": ["packages/kbn-plugin-helpers"],
|
||||
"@kbn/plugin-helpers/*": ["packages/kbn-plugin-helpers/*"],
|
||||
"@kbn/portable-dashboards-example": ["examples/portable_dashboards_example"],
|
||||
"@kbn/portable-dashboards-example/*": ["examples/portable_dashboards_example/*"],
|
||||
"@kbn/preboot-example-plugin": ["examples/preboot_example"],
|
||||
"@kbn/preboot-example-plugin/*": ["examples/preboot_example/*"],
|
||||
"@kbn/presentation-util-plugin": ["src/plugins/presentation_util"],
|
||||
|
@ -1445,5 +1445,5 @@
|
|||
"@kbn/ambient-common-types",
|
||||
"@kbn/ambient-storybook-types"
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3861,10 +3861,6 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/dashboard-embeddable-examples-plugin@link:examples/dashboard_embeddable_examples":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/dashboard-enhanced-plugin@link:x-pack/plugins/dashboard_enhanced":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
@ -4621,6 +4617,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/portable-dashboards-example@link:examples/portable_dashboards_example":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/preboot-example-plugin@link:examples/preboot_example":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue