From 1a0b93aa9f8ad94a8d7231a2d4f988f5be20baaf Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 7 Jun 2024 15:50:23 -0600 Subject: [PATCH] [embeddable rebuild] log stream react embeddable (#184247) PR migrates log stream embeddable from the legacy class based system. ### test instructions 1. Run kibana on a system with o11y data and log streams 2. Create a new dashboard, click "Add panel" => "Log stream" 3. Verify panel behavior has not changed with legacy embeddable 4. Click panel context menu and select "Settings" 5. Set custom title, description and time range. Verify behavior has not changed with legacy embeddable 6. Import dashboard with log stream panel. Verify behavior has not changed with legacy embeddable --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../group1/create_and_add_embeddables.ts | 2 +- .../public/components/log_stream/constants.ts | 9 ++ .../log_stream/log_stream_embeddable.tsx | 133 ------------------ .../log_stream_embeddable_factory.ts | 57 -------- .../log_stream_react_embeddable.tsx | 123 ++++++++++++++++ .../public/components/log_stream/types.ts | 21 +++ .../infra/public/plugin.ts | 53 ++++++- .../infra/tsconfig.json | 2 + 8 files changed, 203 insertions(+), 197 deletions(-) create mode 100644 x-pack/plugins/observability_solution/infra/public/components/log_stream/constants.ts delete mode 100644 x-pack/plugins/observability_solution/infra/public/components/log_stream/log_stream_embeddable.tsx delete mode 100644 x-pack/plugins/observability_solution/infra/public/components/log_stream/log_stream_embeddable_factory.ts create mode 100644 x-pack/plugins/observability_solution/infra/public/components/log_stream/log_stream_react_embeddable.tsx create mode 100644 x-pack/plugins/observability_solution/infra/public/components/log_stream/types.ts diff --git a/test/functional/apps/dashboard/group1/create_and_add_embeddables.ts b/test/functional/apps/dashboard/group1/create_and_add_embeddables.ts index 9531606e649f31..1219fb03fd1c26 100644 --- a/test/functional/apps/dashboard/group1/create_and_add_embeddables.ts +++ b/test/functional/apps/dashboard/group1/create_and_add_embeddables.ts @@ -34,7 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickNewDashboard(); await PageObjects.dashboard.switchToEditMode(); await dashboardAddPanel.clickEditorMenuButton(); - await dashboardAddPanel.clickAddNewEmbeddableLink('LOG_STREAM_EMBEDDABLE'); + await dashboardAddPanel.clickAddNewPanelFromUIActionLink('Log stream'); await dashboardAddPanel.expectEditorMenuClosed(); }); diff --git a/x-pack/plugins/observability_solution/infra/public/components/log_stream/constants.ts b/x-pack/plugins/observability_solution/infra/public/components/log_stream/constants.ts new file mode 100644 index 00000000000000..c573295aeb81c4 --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/public/components/log_stream/constants.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const LOG_STREAM_EMBEDDABLE = 'LOG_STREAM_EMBEDDABLE'; +export const ADD_LOG_STREAM_ACTION_ID = 'ADD_SEARCH_ACTION_ID'; diff --git a/x-pack/plugins/observability_solution/infra/public/components/log_stream/log_stream_embeddable.tsx b/x-pack/plugins/observability_solution/infra/public/components/log_stream/log_stream_embeddable.tsx deleted file mode 100644 index 9d8be340d9bd58..00000000000000 --- a/x-pack/plugins/observability_solution/infra/public/components/log_stream/log_stream_embeddable.tsx +++ /dev/null @@ -1,133 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Query, Filter } from '@kbn/es-query'; -import { AppMountParameters, CoreStart } from '@kbn/core/public'; -import React, { FC, PropsWithChildren } from 'react'; -import ReactDOM from 'react-dom'; -import { Subscription } from 'rxjs'; -import type { TimeRange } from '@kbn/es-query'; -import { Embeddable, EmbeddableInput, IContainer } from '@kbn/embeddable-plugin/public'; -import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; -import { LogStream } from '@kbn/logs-shared-plugin/public'; -import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; -import { InfraClientStartDeps, InfraClientStartExports } from '../../types'; -import { datemathToEpochMillis } from '../../utils/datemath'; -import { useKibanaContextForPluginProvider } from '../../hooks/use_kibana'; - -export const LOG_STREAM_EMBEDDABLE = 'LOG_STREAM_EMBEDDABLE'; - -export interface LogStreamEmbeddableInput extends EmbeddableInput { - filters: Filter[]; - timeRange: TimeRange; - query: Query; -} - -export class LogStreamEmbeddable extends Embeddable { - public readonly type = LOG_STREAM_EMBEDDABLE; - private node?: HTMLElement; - private subscription: Subscription; - private isDarkMode = false; - - constructor( - private core: CoreStart, - private pluginDeps: InfraClientStartDeps, - private pluginStart: InfraClientStartExports, - initialInput: LogStreamEmbeddableInput, - parent?: IContainer - ) { - super(initialInput, {}, parent); - - this.subscription = new Subscription(); - - this.subscription.add( - core.theme?.theme$.subscribe((theme) => (this.isDarkMode = theme.darkMode)) - ); - - this.subscription.add(this.getInput$().subscribe(() => this.renderComponent())); - } - - public render(node: HTMLElement) { - if (this.node) { - ReactDOM.unmountComponentAtNode(this.node); - } - this.node = node; - - this.renderComponent(); - } - - public destroy() { - super.destroy(); - this.subscription.unsubscribe(); - if (this.node) { - ReactDOM.unmountComponentAtNode(this.node); - } - } - - public async reload() {} - - private renderComponent() { - if (!this.node) { - return; - } - - const startTimestamp = datemathToEpochMillis(this.input.timeRange.from); - const endTimestamp = datemathToEpochMillis(this.input.timeRange.to, 'up'); - - if (!startTimestamp || !endTimestamp) { - return; - } - - ReactDOM.render( - - -
- -
-
-
, - this.node - ); - } -} - -export interface LogStreamEmbeddableProvidersProps { - core: CoreStart; - pluginStart: InfraClientStartExports; - plugins: InfraClientStartDeps; - theme$: AppMountParameters['theme$']; -} - -export const LogStreamEmbeddableProviders: FC< - PropsWithChildren -> = ({ children, core, pluginStart, plugins }) => { - const KibanaContextProviderForPlugin = useKibanaContextForPluginProvider( - core, - plugins, - pluginStart - ); - - return ( - - - {children} - - - ); -}; diff --git a/x-pack/plugins/observability_solution/infra/public/components/log_stream/log_stream_embeddable_factory.ts b/x-pack/plugins/observability_solution/infra/public/components/log_stream/log_stream_embeddable_factory.ts deleted file mode 100644 index 3d472654c0860e..00000000000000 --- a/x-pack/plugins/observability_solution/infra/public/components/log_stream/log_stream_embeddable_factory.ts +++ /dev/null @@ -1,57 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public'; -import { InfraClientStartServicesAccessor } from '../../types'; -import { - LogStreamEmbeddable, - LogStreamEmbeddableInput, - LOG_STREAM_EMBEDDABLE, -} from './log_stream_embeddable'; - -export class LogStreamEmbeddableFactoryDefinition - implements EmbeddableFactoryDefinition -{ - public readonly type = LOG_STREAM_EMBEDDABLE; - - constructor(private getStartServices: InfraClientStartServicesAccessor) {} - - public async isEditable() { - const [{ application }] = await this.getStartServices(); - return application.capabilities.logs.save as boolean; - } - - public async create(initialInput: LogStreamEmbeddableInput, parent?: IContainer) { - const [core, plugins, pluginStart] = await this.getStartServices(); - return new LogStreamEmbeddable(core, plugins, pluginStart, initialInput, parent); - } - - public getDisplayName() { - return i18n.translate('xpack.infra.logStreamEmbeddable.displayName', { - defaultMessage: 'Log stream', - }); - } - - public getDescription() { - return i18n.translate('xpack.infra.logStreamEmbeddable.description', { - defaultMessage: 'Add a table of live streaming logs.', - }); - } - - public getIconType() { - return 'logsApp'; - } - - public async getExplicitInput() { - return { - title: i18n.translate('xpack.infra.logStreamEmbeddable.title', { - defaultMessage: 'Log stream', - }), - }; - } -} diff --git a/x-pack/plugins/observability_solution/infra/public/components/log_stream/log_stream_react_embeddable.tsx b/x-pack/plugins/observability_solution/infra/public/components/log_stream/log_stream_react_embeddable.tsx new file mode 100644 index 00000000000000..313eb476c4ae5e --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/public/components/log_stream/log_stream_react_embeddable.tsx @@ -0,0 +1,123 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, PropsWithChildren, useEffect, useMemo, useState } from 'react'; +import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; +import { + initializeTimeRange, + initializeTitles, + useFetchContext, +} from '@kbn/presentation-publishing'; +import { LogStream } from '@kbn/logs-shared-plugin/public'; +import { AppMountParameters, CoreStart } from '@kbn/core/public'; +import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; +import { Query } from '@kbn/es-query'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; +import type { LogStreamApi, LogStreamSerializedState, Services } from './types'; +import { datemathToEpochMillis } from '../../utils/datemath'; +import { LOG_STREAM_EMBEDDABLE } from './constants'; +import { useKibanaContextForPluginProvider } from '../../hooks/use_kibana'; +import { InfraClientStartDeps, InfraClientStartExports } from '../../types'; + +export function getLogStreamEmbeddableFactory(services: Services) { + const factory: ReactEmbeddableFactory = { + type: LOG_STREAM_EMBEDDABLE, + deserializeState: (state) => state.rawState, + buildEmbeddable: async (state, buildApi) => { + const timeRangeContext = initializeTimeRange(state); + const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state); + + const api = buildApi( + { + ...timeRangeContext.api, + ...titlesApi, + serializeState: () => { + return { + rawState: { + ...timeRangeContext.serialize(), + ...serializeTitles(), + }, + }; + }, + }, + { + ...timeRangeContext.comparators, + ...titleComparators, + } + ); + + return { + api, + Component: () => { + const { filters, query, timeRange } = useFetchContext(api); + const { startTimestamp, endTimestamp } = useMemo(() => { + return { + startTimestamp: timeRange ? datemathToEpochMillis(timeRange.from) : undefined, + endTimestamp: timeRange ? datemathToEpochMillis(timeRange.to, 'up') : undefined, + }; + }, [timeRange]); + + const [darkMode, setDarkMode] = useState(false); + useEffect(() => { + const subscription = services.coreStart.theme.theme$.subscribe((theme) => { + setDarkMode(theme.darkMode); + }); + return () => subscription.unsubscribe(); + }, []); + + return !startTimestamp || !endTimestamp ? null : ( + + +
+ +
+
+
+ ); + }, + }; + }, + }; + return factory; +} + +export interface LogStreamEmbeddableProvidersProps { + core: CoreStart; + pluginStart: InfraClientStartExports; + plugins: InfraClientStartDeps; + theme$: AppMountParameters['theme$']; +} + +export const LogStreamEmbeddableProviders: FC< + PropsWithChildren +> = ({ children, core, pluginStart, plugins }) => { + const KibanaContextProviderForPlugin = useKibanaContextForPluginProvider( + core, + plugins, + pluginStart + ); + + return ( + + + {children} + + + ); +}; diff --git a/x-pack/plugins/observability_solution/infra/public/components/log_stream/types.ts b/x-pack/plugins/observability_solution/infra/public/components/log_stream/types.ts new file mode 100644 index 00000000000000..26a9201c41a948 --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/public/components/log_stream/types.ts @@ -0,0 +1,21 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreStart } from '@kbn/core/public'; +import { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public'; +import { SerializedTimeRange, SerializedTitles } from '@kbn/presentation-publishing'; +import { InfraClientStartDeps, InfraClientStartExports } from '../../types'; + +export type LogStreamSerializedState = SerializedTitles & SerializedTimeRange; + +export type LogStreamApi = DefaultEmbeddableApi; + +export interface Services { + coreStart: CoreStart; + pluginDeps: InfraClientStartDeps; + pluginStart: InfraClientStartExports; +} diff --git a/x-pack/plugins/observability_solution/infra/public/plugin.ts b/x-pack/plugins/observability_solution/infra/public/plugin.ts index 20a6d85092c866..20f819b7eb34d0 100644 --- a/x-pack/plugins/observability_solution/infra/public/plugin.ts +++ b/x-pack/plugins/observability_solution/infra/public/plugin.ts @@ -19,12 +19,14 @@ import { enableInfrastructureHostsView } from '@kbn/observability-plugin/public' import { ObservabilityTriggerId } from '@kbn/observability-shared-plugin/common'; import { BehaviorSubject, combineLatest, from } from 'rxjs'; import { map } from 'rxjs'; +import type { EmbeddableApiContext } from '@kbn/presentation-publishing'; +import { apiCanAddNewPanel } from '@kbn/presentation-containers'; +import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; import type { InfraPublicConfig } from '../common/plugin_config_types'; import { createInventoryMetricRuleType } from './alerting/inventory'; import { createLogThresholdRuleType } from './alerting/log_threshold'; import { createMetricThresholdRuleType } from './alerting/metric_threshold'; -import { LOG_STREAM_EMBEDDABLE } from './components/log_stream/log_stream_embeddable'; -import { LogStreamEmbeddableFactoryDefinition } from './components/log_stream/log_stream_embeddable_factory'; +import { ADD_LOG_STREAM_ACTION_ID, LOG_STREAM_EMBEDDABLE } from './components/log_stream/constants'; import { type InfraLocators, InfraLogsLocatorDefinition, @@ -44,6 +46,7 @@ import type { InfraClientStartExports, } from './types'; import { getLogsHasDataFetcher, getLogsOverviewDataFetcher } from './utils/logs_overview_fetchers'; +import type { LogStreamSerializedState } from './components/log_stream/types'; export class Plugin implements InfraClientPluginClass { public config: InfraPublicConfig; @@ -173,10 +176,17 @@ export class Plugin implements InfraClientPluginClass { ) ); - pluginsSetup.embeddable.registerEmbeddableFactory( - LOG_STREAM_EMBEDDABLE, - new LogStreamEmbeddableFactoryDefinition(core.getStartServices) - ); + pluginsSetup.embeddable.registerReactEmbeddableFactory(LOG_STREAM_EMBEDDABLE, async () => { + const { getLogStreamEmbeddableFactory } = await import( + './components/log_stream/log_stream_react_embeddable' + ); + const [coreStart, pluginDeps, pluginStart] = await core.getStartServices(); + return getLogStreamEmbeddableFactory({ + coreStart, + pluginDeps, + pluginStart, + }); + }); // Register Locators const logsLocator = this.config.featureFlags.logsUIEnabled @@ -388,6 +398,37 @@ export class Plugin implements InfraClientPluginClass { const telemetry = this.telemetry.start(); + plugins.uiActions.registerAction({ + id: ADD_LOG_STREAM_ACTION_ID, + getDisplayName: () => + i18n.translate('xpack.infra.logStreamEmbeddable.displayName', { + defaultMessage: 'Log stream', + }), + getDisplayNameTooltip: () => + i18n.translate('xpack.infra.logStreamEmbeddable.description', { + defaultMessage: 'Add a table of live streaming logs.', + }), + getIconType: () => 'logsApp', + isCompatible: async ({ embeddable }) => { + return apiCanAddNewPanel(embeddable); + }, + execute: async ({ embeddable }) => { + if (!apiCanAddNewPanel(embeddable)) throw new IncompatibleActionError(); + embeddable.addNewPanel( + { + panelType: LOG_STREAM_EMBEDDABLE, + initialState: { + title: i18n.translate('xpack.infra.logStreamEmbeddable.title', { + defaultMessage: 'Log stream', + }), + }, + }, + true + ); + }, + }); + plugins.uiActions.attachAction('ADD_PANEL_TRIGGER', ADD_LOG_STREAM_ACTION_ID); + const startContract: InfraClientStartExports = { inventoryViews, metricsExplorerViews, diff --git a/x-pack/plugins/observability_solution/infra/tsconfig.json b/x-pack/plugins/observability_solution/infra/tsconfig.json index 22f3fb1d16d70e..cb98f8ab0859e8 100644 --- a/x-pack/plugins/observability_solution/infra/tsconfig.json +++ b/x-pack/plugins/observability_solution/infra/tsconfig.json @@ -104,6 +104,8 @@ "@kbn/router-utils", "@kbn/react-kibana-context-render", "@kbn/react-kibana-context-theme", + "@kbn/presentation-publishing", + "@kbn/presentation-containers", "@kbn/deeplinks-observability" ], "exclude": [