[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

![Feb-07-2023
11-41-13](https://user-images.githubusercontent.com/8698078/217336429-d4bbd7be-a453-45f1-a008-6046d58874b6.gif)

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

![Feb-07-2023
11-43-37](https://user-images.githubusercontent.com/8698078/217336922-48348617-1fdf-445a-851a-3507c6920805.gif)

3. Two side-by-side by-value empty dashboards with independent redux
states

![Feb-07-2023
11-45-57](https://user-images.githubusercontent.com/8698078/217337433-8e00b24f-3363-4ff0-a2bd-5fa15c736d08.gif)

4. A static, by-reference dashboard


![StaticByRefernece](https://user-images.githubusercontent.com/8698078/217340227-5b8ac1ab-0cdc-4ff4-8fb8-2b2792fa3959.png)

5. A static, by-value dashboard


![StaticByValue](https://user-images.githubusercontent.com/8698078/217339782-c4ab2a4c-6c62-4045-a823-648befc6959f.png)


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:
Hannah Mudge 2023-02-10 13:21:53 -07:00 committed by GitHub
parent 6ad4a44917
commit 27dda79627
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 1349 additions and 256 deletions

2
.github/CODEOWNERS vendored
View file

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

View file

@ -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"
]
}

View file

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

View file

@ -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"
]
}
}

View file

@ -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);
};

View file

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

View file

@ -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",
]
}

View file

@ -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);
}
}

View file

@ -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>
);
}

View file

@ -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';
}
}

View file

@ -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';

View file

@ -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();

View file

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

View file

@ -24,5 +24,6 @@
"@kbn/saved-objects-plugin",
"@kbn/i18n",
"@kbn/utility-types",
"@kbn/es-query",
]
}

View 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"]
}
}

View 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);
};

View 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';

View file

@ -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>
</>
);
};

View file

@ -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>
</>
);
};

View file

@ -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>
</>
);
};

View 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();
}

View 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

View file

@ -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>
);
};

View file

@ -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>
</>
);
};

View file

@ -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"
}
}
}

View 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"
]
}

View file

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

View file

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

View file

@ -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';

View file

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

View file

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

View file

@ -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';

View file

@ -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'>;

View file

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

View file

@ -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
>();

View file

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

View file

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

View file

@ -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';

View file

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

View file

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

View file

@ -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;
}

View file

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

View file

@ -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"
]
},
}
}

View file

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