[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>
This commit is contained in:
Nathan Reese 2024-06-07 15:50:23 -06:00 committed by GitHub
parent 257ef7f69e
commit 1a0b93aa9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 203 additions and 197 deletions

View file

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

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

View file

@ -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<LogStreamEmbeddableInput> {
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(
<LogStreamEmbeddableProviders
core={this.core}
plugins={this.pluginDeps}
pluginStart={this.pluginStart}
theme$={this.core.theme.theme$}
>
<EuiThemeProvider darkMode={this.isDarkMode}>
<div style={{ width: '100%' }}>
<LogStream
logView={{ type: 'log-view-reference', logViewId: 'default' }}
startTimestamp={startTimestamp}
endTimestamp={endTimestamp}
height="100%"
query={this.input.query}
filters={this.input.filters}
/>
</div>
</EuiThemeProvider>
</LogStreamEmbeddableProviders>,
this.node
);
}
}
export interface LogStreamEmbeddableProvidersProps {
core: CoreStart;
pluginStart: InfraClientStartExports;
plugins: InfraClientStartDeps;
theme$: AppMountParameters['theme$'];
}
export const LogStreamEmbeddableProviders: FC<
PropsWithChildren<LogStreamEmbeddableProvidersProps>
> = ({ children, core, pluginStart, plugins }) => {
const KibanaContextProviderForPlugin = useKibanaContextForPluginProvider(
core,
plugins,
pluginStart
);
return (
<KibanaRenderContextProvider {...core}>
<KibanaContextProviderForPlugin services={{ ...core, ...plugins, ...pluginStart }}>
{children}
</KibanaContextProviderForPlugin>
</KibanaRenderContextProvider>
);
};

View file

@ -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<LogStreamEmbeddableInput>
{
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',
}),
};
}
}

View file

@ -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<LogStreamSerializedState, LogStreamApi> = {
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 : (
<LogStreamEmbeddableProviders
core={services.coreStart}
plugins={services.pluginDeps}
pluginStart={services.pluginStart}
theme$={services.coreStart.theme.theme$}
>
<EuiThemeProvider darkMode={darkMode}>
<div style={{ width: '100%' }}>
<LogStream
logView={{ type: 'log-view-reference', logViewId: 'default' }}
startTimestamp={startTimestamp}
endTimestamp={endTimestamp}
height="100%"
query={query as Query | undefined}
filters={filters}
/>
</div>
</EuiThemeProvider>
</LogStreamEmbeddableProviders>
);
},
};
},
};
return factory;
}
export interface LogStreamEmbeddableProvidersProps {
core: CoreStart;
pluginStart: InfraClientStartExports;
plugins: InfraClientStartDeps;
theme$: AppMountParameters['theme$'];
}
export const LogStreamEmbeddableProviders: FC<
PropsWithChildren<LogStreamEmbeddableProvidersProps>
> = ({ children, core, pluginStart, plugins }) => {
const KibanaContextProviderForPlugin = useKibanaContextForPluginProvider(
core,
plugins,
pluginStart
);
return (
<KibanaRenderContextProvider {...core}>
<KibanaContextProviderForPlugin services={{ ...core, ...plugins, ...pluginStart }}>
{children}
</KibanaContextProviderForPlugin>
</KibanaRenderContextProvider>
);
};

View file

@ -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<LogStreamSerializedState>;
export interface Services {
coreStart: CoreStart;
pluginDeps: InfraClientStartDeps;
pluginStart: InfraClientStartExports;
}

View file

@ -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<EmbeddableApiContext>({
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<LogStreamSerializedState>(
{
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,

View file

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