diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index a4c7c64762299..b17a63170f907 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -115,9 +115,15 @@ enabled: - test/functional/apps/discover/classic/config.ts - test/functional/apps/discover/embeddable/config.ts - test/functional/apps/discover/group1/config.ts - - test/functional/apps/discover/group2/config.ts + - test/functional/apps/discover/group2_data_grid1/config.ts + - test/functional/apps/discover/group2_data_grid2/config.ts + - test/functional/apps/discover/group2_data_grid3/config.ts - test/functional/apps/discover/group3/config.ts - test/functional/apps/discover/group4/config.ts + - test/functional/apps/discover/group5/config.ts + - test/functional/apps/discover/group6/config.ts + - test/functional/apps/discover/group7/config.ts + - test/functional/apps/discover/group8/config.ts - test/functional/apps/getting_started/config.ts - test/functional/apps/home/config.ts - test/functional/apps/kibana_overview/config.ts diff --git a/.buildkite/pipelines/artifacts_container_image.yml b/.buildkite/pipelines/artifacts_container_image.yml index 972fada9ddde2..63744d64aba92 100644 --- a/.buildkite/pipelines/artifacts_container_image.yml +++ b/.buildkite/pipelines/artifacts_container_image.yml @@ -2,9 +2,5 @@ steps: - command: .buildkite/scripts/steps/artifacts/docker_image.sh label: Build serverless container images agents: - queue: n2-16-spot + queue: c2-16 timeout_in_minutes: 60 - retry: - automatic: - - exit_status: '-1' - limit: 3 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0c57ef0d7dc9a..0f58444069968 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -384,7 +384,6 @@ test/plugin_functional/plugins/elasticsearch_client_plugin @elastic/kibana-core x-pack/test/plugin_api_integration/plugins/elasticsearch_client @elastic/kibana-core x-pack/plugins/embeddable_enhanced @elastic/kibana-presentation examples/embeddable_examples @elastic/kibana-presentation -examples/embeddable_explorer @elastic/kibana-presentation src/plugins/embeddable @elastic/kibana-presentation x-pack/examples/embedded_lens_example @elastic/kibana-visualizations x-pack/plugins/encrypted_saved_objects @elastic/kibana-security diff --git a/config/node.options b/config/node.options index 2bc49f5db1f4a..abcb40a5c19d4 100644 --- a/config/node.options +++ b/config/node.options @@ -13,7 +13,3 @@ ## enable OpenSSL 3 legacy provider --openssl-legacy-provider - -# Enable capturing heap snapshots. See https://nodejs.org/api/cli.html#--heapsnapshot-signalsignal ---heapsnapshot-signal=SIGUSR2 ---diagnostic-dir=./data \ No newline at end of file diff --git a/examples/embeddable_examples/kibana.jsonc b/examples/embeddable_examples/kibana.jsonc index 0788268aedf3f..08e4a02360b2c 100644 --- a/examples/embeddable_examples/kibana.jsonc +++ b/examples/embeddable_examples/kibana.jsonc @@ -14,7 +14,8 @@ "dashboard", "data", "charts", - "fieldFormats" + "fieldFormats", + "developerExamples" ], "requiredBundles": ["presentationUtil"], "extraPublicDirs": ["public/hello_world"] diff --git a/examples/embeddable_examples/public/app/app.tsx b/examples/embeddable_examples/public/app/app.tsx new file mode 100644 index 0000000000000..72c5d4f11779a --- /dev/null +++ b/examples/embeddable_examples/public/app/app.tsx @@ -0,0 +1,81 @@ +/* + * 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, { useState } from 'react'; +import ReactDOM from 'react-dom'; + +import { AppMountParameters } from '@kbn/core-application-browser'; +import { + EuiPage, + EuiPageBody, + EuiPageHeader, + EuiPageSection, + EuiPageTemplate, + EuiSpacer, + EuiTab, + EuiTabs, +} from '@elastic/eui'; +import { Overview } from './overview'; +import { RenderExamples } from './render_examples'; + +const OVERVIEW_TAB_ID = 'overview'; +const RENDER_TAB_ID = 'render'; + +const App = () => { + const [selectedTabId, setSelectedTabId] = useState(OVERVIEW_TAB_ID); + + function onSelectedTabChanged(tabId: string) { + setSelectedTabId(tabId); + } + + function renderTabContent() { + if (selectedTabId === RENDER_TAB_ID) { + return ; + } + + return ; + } + + return ( + + + + + + + + + onSelectedTabChanged(OVERVIEW_TAB_ID)} + isSelected={OVERVIEW_TAB_ID === selectedTabId} + > + Embeddables overview + + onSelectedTabChanged(RENDER_TAB_ID)} + isSelected={RENDER_TAB_ID === selectedTabId} + > + Rendering embeddables in your application + + + + + + {renderTabContent()} + + + + + ); +}; + +export const renderApp = (element: AppMountParameters['element']) => { + ReactDOM.render(, element); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/examples/embeddable_examples/public/app/overview.tsx b/examples/embeddable_examples/public/app/overview.tsx new file mode 100644 index 0000000000000..e074cd148f77e --- /dev/null +++ b/examples/embeddable_examples/public/app/overview.tsx @@ -0,0 +1,22 @@ +/* + * 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 { EuiText } from '@elastic/eui'; + +export const Overview = () => { + return ( + +

+ Embeddables are React components that manage their own state, can be serialized and + deserialized, and return an API that can be used to interact with them imperatively. +

+
+ ); +}; diff --git a/examples/embeddable_examples/public/app/render_examples.tsx b/examples/embeddable_examples/public/app/render_examples.tsx new file mode 100644 index 0000000000000..c3e9b0a79a55b --- /dev/null +++ b/examples/embeddable_examples/public/app/render_examples.tsx @@ -0,0 +1,137 @@ +/* + * 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 { ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public'; +import { + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiSuperDatePicker, + EuiSwitch, + EuiText, + OnTimeChangeProps, +} from '@elastic/eui'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { TimeRange } from '@kbn/es-query'; +import { useBatchedOptionalPublishingSubjects } from '@kbn/presentation-publishing'; +import { SearchEmbeddableRenderer } from '../react_embeddables/search/search_embeddable_renderer'; +import { SEARCH_EMBEDDABLE_ID } from '../react_embeddables/search/constants'; +import type { Api, State } from '../react_embeddables/search/types'; + +export const RenderExamples = () => { + const initialState = useMemo(() => { + return { + rawState: { + timeRange: undefined, + }, + references: [], + }; + // only run onMount + }, []); + + const parentApi = useMemo(() => { + return { + reload$: new Subject(), + timeRange$: new BehaviorSubject({ + from: 'now-24h', + to: 'now', + }), + }; + // only run onMount + }, []); + + const [api, setApi] = useState(null); + const [hidePanelChrome, setHidePanelChrome] = useState(false); + const [dataLoading, timeRange] = useBatchedOptionalPublishingSubjects( + api?.dataLoading, + parentApi.timeRange$ + ); + + return ( +
+ { + parentApi.timeRange$.next({ + from: start, + to: end, + }); + }} + onRefresh={() => { + parentApi.reload$.next(); + }} + /> + + + + + + +

+ Use ReactEmbeddableRenderer to render embeddables. +

+
+ + + {` + type={SEARCH_EMBEDDABLE_ID} + state={initialState} + parentApi={parentApi} + onApiAvailable={(newApi) => { + setApi(newApi); + }} + hidePanelChrome={hidePanelChrome} +/>`} + + + + + setHidePanelChrome(e.target.checked)} + /> + + + + + key={hidePanelChrome ? 'hideChrome' : 'showChrome'} + type={SEARCH_EMBEDDABLE_ID} + state={initialState} + parentApi={parentApi} + onApiAvailable={(newApi) => { + setApi(newApi); + }} + hidePanelChrome={hidePanelChrome} + /> +
+ + + +

To avoid leaking embeddable details, wrap ReactEmbeddableRenderer in a component.

+
+ + + {``} + + + + + +
+
+
+ ); +}; diff --git a/examples/embeddable_examples/public/app/setup_app.ts b/examples/embeddable_examples/public/app/setup_app.ts new file mode 100644 index 0000000000000..c489b603c877b --- /dev/null +++ b/examples/embeddable_examples/public/app/setup_app.ts @@ -0,0 +1,31 @@ +/* + * 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, CoreSetup } from '@kbn/core/public'; +import { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public'; +import type { StartDeps } from '../plugin'; + +const APP_ID = 'embeddablesApp'; +const title = 'Embeddables'; + +export function setupApp(core: CoreSetup, developerExamples: DeveloperExamplesSetup) { + core.application.register({ + id: APP_ID, + title, + visibleIn: [], + async mount(params: AppMountParameters) { + const { renderApp } = await import('./app'); + return renderApp(params.element); + }, + }); + developerExamples.register({ + appId: APP_ID, + title, + description: `Learn how to create new embeddable types and use embeddables in your application.`, + }); +} diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index e3916b8b409cb..85faad072920a 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -17,6 +17,7 @@ import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { ChartsPluginStart } from '@kbn/charts-plugin/public'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public'; import { HelloWorldEmbeddableFactory, HELLO_WORLD_EMBEDDABLE, @@ -45,13 +46,15 @@ import { registerAddSearchPanelAction } from './react_embeddables/search/registe import { EUI_MARKDOWN_ID } from './react_embeddables/eui_markdown/constants'; import { FIELD_LIST_ID } from './react_embeddables/field_list/constants'; import { SEARCH_EMBEDDABLE_ID } from './react_embeddables/search/constants'; +import { setupApp } from './app/setup_app'; -export interface EmbeddableExamplesSetupDependencies { +export interface SetupDeps { + developerExamples: DeveloperExamplesSetup; embeddable: EmbeddableSetup; uiActions: UiActionsStart; } -export interface EmbeddableExamplesStartDependencies { +export interface StartDeps { dataViews: DataViewsPublicPluginStart; embeddable: EmbeddableStart; uiActions: UiActionsStart; @@ -67,40 +70,31 @@ interface ExampleEmbeddableFactories { getFilterDebuggerEmbeddableFactory: () => FilterDebuggerEmbeddableFactory; } -export interface EmbeddableExamplesStart { +export interface StartApi { createSampleData: () => Promise; factories: ExampleEmbeddableFactories; } -export class EmbeddableExamplesPlugin - implements - Plugin< - void, - EmbeddableExamplesStart, - EmbeddableExamplesSetupDependencies, - EmbeddableExamplesStartDependencies - > -{ +export class EmbeddableExamplesPlugin implements Plugin { private exampleEmbeddableFactories: Partial = {}; - public setup( - core: CoreSetup, - deps: EmbeddableExamplesSetupDependencies - ) { + public setup(core: CoreSetup, { embeddable, developerExamples }: SetupDeps) { + setupApp(core, developerExamples); + this.exampleEmbeddableFactories.getHelloWorldEmbeddableFactory = - deps.embeddable.registerEmbeddableFactory( + embeddable.registerEmbeddableFactory( HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactoryDefinition() ); this.exampleEmbeddableFactories.getMigrationsEmbeddableFactory = - deps.embeddable.registerEmbeddableFactory( + embeddable.registerEmbeddableFactory( SIMPLE_EMBEDDABLE, new SimpleEmbeddableFactoryDefinition() ); this.exampleEmbeddableFactories.getListContainerEmbeddableFactory = - deps.embeddable.registerEmbeddableFactory( + embeddable.registerEmbeddableFactory( LIST_CONTAINER, new ListContainerFactoryDefinition(async () => ({ embeddableServices: (await core.getStartServices())[1].embeddable, @@ -108,16 +102,13 @@ export class EmbeddableExamplesPlugin ); this.exampleEmbeddableFactories.getFilterDebuggerEmbeddableFactory = - deps.embeddable.registerEmbeddableFactory( + embeddable.registerEmbeddableFactory( FILTER_DEBUGGER_EMBEDDABLE, new FilterDebuggerEmbeddableFactoryDefinition() ); } - public start( - core: CoreStart, - deps: EmbeddableExamplesStartDependencies - ): EmbeddableExamplesStart { + public start(core: CoreStart, deps: StartDeps): StartApi { registerCreateFieldListAction(deps.uiActions); registerReactEmbeddableFactory(FIELD_LIST_ID, async () => { const { getFieldListFactory } = await import( diff --git a/examples/embeddable_examples/public/react_embeddables/search/search_embeddable_renderer.tsx b/examples/embeddable_examples/public/react_embeddables/search/search_embeddable_renderer.tsx new file mode 100644 index 0000000000000..0fa6e785b72c1 --- /dev/null +++ b/examples/embeddable_examples/public/react_embeddables/search/search_embeddable_renderer.tsx @@ -0,0 +1,53 @@ +/* + * 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, useMemo } from 'react'; +import { BehaviorSubject } from 'rxjs'; +import { TimeRange } from '@kbn/es-query'; +import { ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public'; +import type { Api, State } from './types'; +import { SEARCH_EMBEDDABLE_ID } from './constants'; + +interface Props { + timeRange?: TimeRange; +} + +export function SearchEmbeddableRenderer(props: Props) { + const initialState = useMemo(() => { + return { + rawState: { + timeRange: undefined, + }, + references: [], + }; + // only run onMount + }, []); + + const parentApi = useMemo(() => { + return { + timeRange$: new BehaviorSubject(props.timeRange), + }; + // only run onMount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + parentApi.timeRange$.next(props.timeRange); + }, [props.timeRange, parentApi.timeRange$]); + + return ( +
+ + type={SEARCH_EMBEDDABLE_ID} + state={initialState} + parentApi={parentApi} + hidePanelChrome={true} + /> +
+ ); +} diff --git a/examples/embeddable_examples/public/react_embeddables/search/types.ts b/examples/embeddable_examples/public/react_embeddables/search/types.ts index 5ba940b2d40b0..dffe119eee7a0 100644 --- a/examples/embeddable_examples/public/react_embeddables/search/types.ts +++ b/examples/embeddable_examples/public/react_embeddables/search/types.ts @@ -19,6 +19,9 @@ import { } from '@kbn/presentation-publishing'; export interface State { + /* + * Time range only applied to this embeddable, overrides parentApi.timeRange$ + */ timeRange: TimeRange | undefined; } diff --git a/examples/embeddable_examples/tsconfig.json b/examples/embeddable_examples/tsconfig.json index 0225005262720..1165c05b189ad 100644 --- a/examples/embeddable_examples/tsconfig.json +++ b/examples/embeddable_examples/tsconfig.json @@ -29,6 +29,8 @@ "@kbn/core-lifecycle-browser", "@kbn/presentation-util-plugin", "@kbn/unified-field-list", - "@kbn/presentation-containers" + "@kbn/presentation-containers", + "@kbn/core-application-browser", + "@kbn/developer-examples-plugin" ] } diff --git a/examples/embeddable_explorer/README.md b/examples/embeddable_explorer/README.md deleted file mode 100644 index 0425790f07487..0000000000000 --- a/examples/embeddable_explorer/README.md +++ /dev/null @@ -1,10 +0,0 @@ -## Embeddable explorer - -This example app shows how to: - - Create a basic "hello world" embeddable - - Create embeddables that accept inputs and use an EmbeddableRenderer - - Nest embeddables inside a container - - Dynamically add children to embeddable containers - - Work with the EmbeddablePanel component - -To run this example, use the command `yarn start --run-examples`. diff --git a/examples/embeddable_explorer/kibana.jsonc b/examples/embeddable_explorer/kibana.jsonc deleted file mode 100644 index 3279a7ef92f51..0000000000000 --- a/examples/embeddable_explorer/kibana.jsonc +++ /dev/null @@ -1,20 +0,0 @@ -{ - "type": "plugin", - "id": "@kbn/embeddable-explorer-plugin", - "owner": "@elastic/kibana-presentation", - "description": "Example app that relies on registered functionality in the embeddable_examples plugin", - "plugin": { - "id": "embeddableExplorer", - "server": false, - "browser": true, - "requiredPlugins": [ - "uiActions", - "inspector", - "embeddable", - "embeddableExamples", - "developerExamples", - "dashboard", - "kibanaReact" - ] - } -} diff --git a/examples/embeddable_explorer/public/app.tsx b/examples/embeddable_explorer/public/app.tsx deleted file mode 100644 index 1d1938c8dbebd..0000000000000 --- a/examples/embeddable_explorer/public/app.tsx +++ /dev/null @@ -1,123 +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, withRouter, RouteComponentProps } from 'react-router-dom'; -import { Route } from '@kbn/shared-ux-router'; -import { EuiPageTemplate, EuiSideNav } from '@elastic/eui'; -import { EmbeddableStart } from '@kbn/embeddable-plugin/public'; -import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { Start as InspectorStartContract } from '@kbn/inspector-plugin/public'; -import { AppMountParameters, CoreStart, IUiSettingsClient, OverlayStart } from '@kbn/core/public'; -import { EmbeddableExamplesStart } from '@kbn/embeddable-examples-plugin/public/plugin'; -import { HelloWorldEmbeddableExample } from './hello_world_embeddable_example'; -import { ListContainerExample } from './list_container_example'; -import { EmbeddablePanelExample } from './embeddable_panel_example'; - -interface PageDef { - title: string; - id: string; - component: React.ReactNode; -} - -type NavProps = RouteComponentProps & { - navigateToApp: CoreStart['application']['navigateToApp']; - pages: PageDef[]; -}; - -const Nav = withRouter(({ history, navigateToApp, pages }: NavProps) => { - const navItems = pages.map((page) => ({ - id: page.id, - name: page.title, - onClick: () => history.push(`/${page.id}`), - 'data-test-subj': page.id, - })); - - return ( - - ); -}); - -interface Props { - basename: string; - navigateToApp: CoreStart['application']['navigateToApp']; - embeddableApi: EmbeddableStart; - uiActionsApi: UiActionsStart; - overlays: OverlayStart; - notifications: CoreStart['notifications']; - inspector: InspectorStartContract; - uiSettingsClient: IUiSettingsClient; - embeddableExamples: EmbeddableExamplesStart; -} - -const EmbeddableExplorerApp = ({ - basename, - navigateToApp, - embeddableApi, - embeddableExamples, -}: Props) => { - const pages: PageDef[] = [ - { - title: 'Render embeddable', - id: 'helloWorldEmbeddableSection', - component: ( - - ), - }, - { - title: 'Groups of embeddables', - id: 'listContainerSection', - component: ( - - ), - }, - { - title: 'Context menu', - id: 'embeddablePanelExample', - component: ( - - ), - }, - ]; - - const routes = pages.map((page, i) => ( - page.component} /> - )); - - return ( - - - -