[Log Explorer] Flyout details ai assistant registration (#170658)

## 📓  Summary

Closes #169506 

This PR introduces a mechanism to apply customizations on the
LogExplorer component.
The first necessary customization which is implemented is for the flyout
detail, allowing the consumer to display additional content on top of
what is already displayed.

This is a temporary solution which will be updated and embedded in a
more structured customization system as a result of the work done for
https://github.com/elastic/kibana/issues/165255.

The current solution creates already a context to allow granular
consumption of the customizations only for those subtrees where a
specific customization should apply.

The LogAIAssistant is used to customize the current LogExplorer as the
first usage of this customization.


c9e6b40e-e636-456a-9e19-1778c26142db

---------

Co-authored-by: Marco Antonio Ghiani <marcoantonio.ghiani@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marco Antonio Ghiani 2023-11-08 12:35:25 +01:00 committed by GitHub
parent e00566fa98
commit 43f9712dd5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 215 additions and 25 deletions

View file

@ -17,6 +17,7 @@ import { createLogExplorerProfileCustomizations } from '../../customizations/log
import { createPropertyGetProxy } from '../../utils/proxies';
import { LogExplorerProfileContext } from '../../state_machines/log_explorer_profile';
import { LogExplorerStartDeps } from '../../types';
import { LogExplorerCustomizations } from './types';
export interface CreateLogExplorerArgs {
core: CoreStart;
@ -29,6 +30,7 @@ export interface LogExplorerStateContainer {
}
export interface LogExplorerProps {
customizations?: LogExplorerCustomizations;
scopedHistory: ScopedHistory;
state$?: BehaviorSubject<LogExplorerStateContainer>;
}
@ -44,10 +46,10 @@ export const createLogExplorer = ({ core, plugins }: CreateLogExplorerArgs) => {
uiSettings: createUiSettingsServiceProxy(core.uiSettings),
};
return ({ scopedHistory, state$ }: LogExplorerProps) => {
return ({ customizations = {}, scopedHistory, state$ }: LogExplorerProps) => {
const logExplorerCustomizations = useMemo(
() => [createLogExplorerProfileCustomizations({ core, plugins, state$ })],
[state$]
() => [createLogExplorerProfileCustomizations({ core, customizations, plugins, state$ })],
[customizations, state$]
);
return (

View file

@ -0,0 +1,25 @@
/*
* 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 { DataTableRecord } from '@kbn/discover-utils/types';
export type RenderPreviousContent = () => React.ReactNode;
export interface LogExplorerFlyoutContentProps {
doc: DataTableRecord;
}
export type FlyoutRenderContent = (
renderPreviousContent: RenderPreviousContent,
props: LogExplorerFlyoutContentProps
) => React.ReactNode;
export interface LogExplorerCustomizations {
flyout?: {
renderContent?: FlyoutRenderContent;
};
}

View file

@ -5,10 +5,11 @@
* 2.0.
*/
import React from 'react';
import React, { useCallback } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FlyoutDetail } from '../components/flyout_detail/flyout_detail';
import { FlyoutProps } from '../components/flyout_detail';
import { useLogExplorerCustomizationsContext } from '../hooks/use_log_explorer_customizations';
export const CustomFlyoutContent = ({
actions,
@ -16,12 +17,28 @@ export const CustomFlyoutContent = ({
doc,
renderDefaultContent,
}: FlyoutProps) => {
const { flyout } = useLogExplorerCustomizationsContext();
const renderPreviousContent = useCallback(
() => (
<>
{/* Apply custom Log Explorer detail */}
<EuiFlexItem>
<FlyoutDetail actions={actions} dataView={dataView} doc={doc} />
</EuiFlexItem>
</>
),
[actions, dataView, doc]
);
const content = flyout?.renderContent
? flyout?.renderContent(renderPreviousContent, { doc })
: renderPreviousContent();
return (
<EuiFlexGroup direction="column">
{/* Apply custom Log Explorer detail */}
<EuiFlexItem>
<FlyoutDetail actions={actions} dataView={dataView} doc={doc} />
</EuiFlexItem>
{content}
{/* Restore default content */}
<EuiFlexItem>{renderDefaultContent()}</EuiFlexItem>
</EuiFlexGroup>

View file

@ -14,6 +14,8 @@ import { LogExplorerProfileStateService } from '../state_machines/log_explorer_p
import { LogExplorerStateContainer } from '../components/log_explorer';
import { LogExplorerStartDeps } from '../types';
import { useKibanaContextForPluginProvider } from '../utils/use_kibana';
import { LogExplorerCustomizations } from '../components/log_explorer/types';
import { LogExplorerCustomizationsProvider } from '../hooks/use_log_explorer_customizations';
const LazyCustomDatasetFilters = dynamic(() => import('./custom_dataset_filters'));
const LazyCustomDatasetSelector = dynamic(() => import('./custom_dataset_selector'));
@ -21,12 +23,18 @@ const LazyCustomFlyoutContent = dynamic(() => import('./custom_flyout_content'))
export interface CreateLogExplorerProfileCustomizationsDeps {
core: CoreStart;
customizations: LogExplorerCustomizations;
plugins: LogExplorerStartDeps;
state$?: BehaviorSubject<LogExplorerStateContainer>;
}
export const createLogExplorerProfileCustomizations =
({ core, plugins, state$ }: CreateLogExplorerProfileCustomizationsDeps): CustomizationCallback =>
({
core,
customizations: logExplorerCustomizations,
plugins,
state$,
}: CreateLogExplorerProfileCustomizationsDeps): CustomizationCallback =>
async ({ customizations, stateContainer }) => {
const { data, dataViews, discover } = plugins;
// Lazy load dependencies
@ -127,7 +135,9 @@ export const createLogExplorerProfileCustomizations =
return (
<KibanaContextProviderForPlugin>
<LazyCustomFlyoutContent {...props} dataView={internalState.dataView} />
<LogExplorerCustomizationsProvider value={logExplorerCustomizations}>
<LazyCustomFlyoutContent {...props} dataView={internalState.dataView} />
</LogExplorerCustomizationsProvider>
</KibanaContextProviderForPlugin>
);
},

View file

@ -0,0 +1,17 @@
/*
* 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 createContainer from 'constate';
import { LogExplorerCustomizations } from '../components/log_explorer/types';
interface UseLogExplorerCustomizationsDeps {
value: LogExplorerCustomizations;
}
const useLogExplorerCustomizations = ({ value }: UseLogExplorerCustomizationsDeps) => value;
export const [LogExplorerCustomizationsProvider, useLogExplorerCustomizationsContext] =
createContainer(useLogExplorerCustomizations);

View file

@ -10,6 +10,10 @@ import type { LogExplorerConfig } from '../common/plugin_config';
import { LogExplorerPlugin } from './plugin';
export type { LogExplorerPluginSetup, LogExplorerPluginStart } from './types';
export type { LogExplorerStateContainer } from './components/log_explorer';
export type {
LogExplorerCustomizations,
LogExplorerFlyoutContentProps,
} from './components/log_explorer/types';
export function plugin(context: PluginInitializerContext<LogExplorerConfig>) {
return new LogExplorerPlugin(context);

View file

@ -4,22 +4,25 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import React, { ComponentType } from 'react';
import { Optional } from '@kbn/utility-types';
import { dynamic } from '../../../common/dynamic';
import type { LogAIAssistantProps } from './log_ai_assistant';
import type { LogAIAssistantDeps } from './log_ai_assistant';
export const LogAIAssistant = dynamic(() => import('./log_ai_assistant'));
interface LogAIAssistantFactoryDeps {
observabilityAIAssistant: LogAIAssistantProps['aiAssistant'];
observabilityAIAssistant: LogAIAssistantDeps['observabilityAIAssistant'];
}
export function createLogAIAssistant({ observabilityAIAssistant }: LogAIAssistantFactoryDeps) {
return ({
aiAssistant = observabilityAIAssistant,
...props
}: Optional<LogAIAssistantProps, 'aiAssistant'>) => (
<LogAIAssistant aiAssistant={aiAssistant} {...props} />
export type LogAIAssistantComponent = ComponentType<
Optional<LogAIAssistantDeps, 'observabilityAIAssistant'>
>;
export function createLogAIAssistant({
observabilityAIAssistant: aiAssistant,
}: LogAIAssistantFactoryDeps): LogAIAssistantComponent {
return ({ observabilityAIAssistant = aiAssistant, ...props }) => (
<LogAIAssistant observabilityAIAssistant={observabilityAIAssistant} {...props} />
);
}

View file

@ -0,0 +1,10 @@
/*
* 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 from 'react';
export const createLogAIAssistantMock = () => jest.fn().mockReturnValue(<div />);

View file

@ -12,6 +12,8 @@ import {
type Message,
ObservabilityAIAssistantPluginStart,
MessageRole,
ObservabilityAIAssistantProvider,
useObservabilityAIAssistant,
} from '@kbn/observability-ai-assistant-plugin/public';
import { LogEntryField } from '../../../common';
import { explainLogMessageTitle, similarLogMessagesTitle } from './translations';
@ -21,11 +23,16 @@ export interface LogAIAssistantDocument {
}
export interface LogAIAssistantProps {
aiAssistant: ObservabilityAIAssistantPluginStart;
doc: LogAIAssistantDocument | undefined;
}
export function LogAIAssistant({ aiAssistant, doc }: LogAIAssistantProps) {
export interface LogAIAssistantDeps extends LogAIAssistantProps {
observabilityAIAssistant: ObservabilityAIAssistantPluginStart;
}
export const LogAIAssistant = withProviders(({ doc }: LogAIAssistantProps) => {
const aiAssistant = useObservabilityAIAssistant();
const explainLogMessageMessages = useMemo<Message[] | undefined>(() => {
if (!doc) {
return undefined;
@ -80,7 +87,20 @@ export function LogAIAssistant({ aiAssistant, doc }: LogAIAssistantProps) {
) : null}
</EuiFlexGroup>
);
}
});
// eslint-disable-next-line import/no-default-export
export default LogAIAssistant;
function withProviders(Component: React.FunctionComponent<LogAIAssistantProps>) {
return function ComponentWithProviders({
observabilityAIAssistant,
...props
}: LogAIAssistantDeps) {
return (
<ObservabilityAIAssistantProvider value={observabilityAIAssistant}>
<Component {...props} />
</ObservabilityAIAssistantProvider>
);
};
}

View file

@ -184,7 +184,7 @@ export const LogEntryFlyout = ({
>
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem grow={false}>
<LogAIAssistant aiAssistant={observabilityAIAssistant} doc={logEntry} />
<LogAIAssistant observabilityAIAssistant={observabilityAIAssistant} doc={logEntry} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LogEntryFieldsTable logEntry={logEntry} onSetFieldFilter={onSetFieldFilter} />

View file

@ -37,6 +37,7 @@ export { useLogSummary, WithSummary } from './containers/logs/log_summary';
export { useLogEntryFlyout } from './components/logging/log_entry_flyout';
// Shared components
export type { LogAIAssistantDocument } from './components/log_ai_assistant/log_ai_assistant';
export type {
LogEntryStreamItem,
LogEntryColumnWidths,

View file

@ -5,11 +5,13 @@
* 2.0.
*/
import { createLogAIAssistantMock } from './components/log_ai_assistant/log_ai_assistant.mock';
import { createLogViewsServiceStartMock } from './services/log_views/log_views_service.mock';
import { LogsSharedClientStartExports } from './types';
export const createLogsSharedPluginStartMock = (): jest.Mocked<LogsSharedClientStartExports> => ({
logViews: createLogViewsServiceStartMock(),
LogAIAssistant: createLogAIAssistantMock(),
});
export const _ensureTypeCompatibility = (): LogsSharedClientStartExports =>

View file

@ -17,6 +17,7 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { ObservabilityAIAssistantPluginStart } from '@kbn/observability-ai-assistant-plugin/public';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { LogAIAssistantComponent } from './components/log_ai_assistant';
// import type { OsqueryPluginStart } from '../../osquery/public';
import { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views';
@ -27,6 +28,7 @@ export interface LogsSharedClientSetupExports {
export interface LogsSharedClientStartExports {
logViews: LogViewsServiceStart;
LogAIAssistant: LogAIAssistantComponent;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface

View file

@ -15,6 +15,7 @@
"data",
"discover",
"logExplorer",
"logsShared",
"observabilityShared",
"share",
"kibanaUtils",

View file

@ -0,0 +1,51 @@
/*
* 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 { EuiFlexItem } from '@elastic/eui';
import {
LogExplorerCustomizations,
LogExplorerFlyoutContentProps,
} from '@kbn/log-explorer-plugin/public';
import type { LogAIAssistantDocument } from '@kbn/logs-shared-plugin/public';
import React, { useMemo } from 'react';
import { useKibanaContextForPlugin } from '../utils/use_kibana';
const ObservabilityLogAIAssistant = ({ doc }: LogExplorerFlyoutContentProps) => {
const { services } = useKibanaContextForPlugin();
const { LogAIAssistant } = services.logsShared;
const mappedDoc = useMemo(() => mapDocToAIAssistantFormat(doc), [doc]);
return <LogAIAssistant key={doc.id} doc={mappedDoc} />;
};
export const renderFlyoutContent: Required<LogExplorerCustomizations>['flyout']['renderContent'] = (
renderPreviousContent,
props
) => {
return (
<>
{renderPreviousContent()}
<EuiFlexItem>
<ObservabilityLogAIAssistant {...props} />
</EuiFlexItem>
</>
);
};
/**
* Utils
*/
const mapDocToAIAssistantFormat = (doc: LogExplorerFlyoutContentProps['doc']) => {
if (!doc) return;
return {
fields: Object.entries(doc.flattened).map(([field, value]) => ({
field,
value,
})) as LogAIAssistantDocument['fields'],
};
};

View file

@ -0,0 +1,15 @@
/*
* 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 { LogExplorerCustomizations } from '@kbn/log-explorer-plugin/public';
import { renderFlyoutContent } from './flyout_content';
export const createLogExplorerCustomizations = (): LogExplorerCustomizations => ({
flyout: {
renderContent: renderFlyoutContent,
},
});

View file

@ -6,7 +6,7 @@
*/
import { CoreStart } from '@kbn/core/public';
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { BehaviorSubject } from 'rxjs';
import { LogExplorerTopNavMenu } from '../../components/log_explorer_top_nav_menu';
import { ObservabilityLogExplorerPageTemplate } from '../../components/page_template';
@ -14,6 +14,7 @@ import { noBreadcrumbs, useBreadcrumbs } from '../../utils/breadcrumbs';
import { useKibanaContextForPlugin } from '../../utils/use_kibana';
import { ObservabilityLogExplorerAppMountParameters } from '../../types';
import { LazyOriginInterpreter } from '../../state_machines/origin_interpreter/src/lazy_component';
import { createLogExplorerCustomizations } from '../../log_explorer_customizations';
export interface ObservablityLogExplorerMainRouteProps {
appParams: ObservabilityLogExplorerAppMountParameters;
core: CoreStart;
@ -31,6 +32,8 @@ export const ObservablityLogExplorerMainRoute = ({
const [state$] = useState(() => new BehaviorSubject({}));
const customizations = useMemo(() => createLogExplorerCustomizations(), []);
return (
<>
<LogExplorerTopNavMenu
@ -41,7 +44,11 @@ export const ObservablityLogExplorerMainRoute = ({
/>
<LazyOriginInterpreter history={history} toasts={core.notifications.toasts} />
<ObservabilityLogExplorerPageTemplate observabilityShared={observabilityShared}>
<logExplorer.LogExplorer scopedHistory={history} state$={state$} />
<logExplorer.LogExplorer
customizations={customizations}
scopedHistory={history}
state$={state$}
/>
</ObservabilityLogExplorerPageTemplate>
</>
);

View file

@ -12,6 +12,7 @@ import { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin
import { ServerlessPluginStart } from '@kbn/serverless/public';
import { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
import { AppMountParameters, ScopedHistory } from '@kbn/core/public';
import { LogsSharedClientStartExports } from '@kbn/logs-shared-plugin/public';
import {
ObservabilityLogExplorerLocators,
ObservabilityLogExplorerLocationState,
@ -33,6 +34,7 @@ export interface ObservabilityLogExplorerStartDeps {
data: DataPublicPluginStart;
discover: DiscoverStart;
logExplorer: LogExplorerPluginStart;
logsShared: LogsSharedClientStartExports;
observabilityShared: ObservabilitySharedPluginStart;
serverless?: ServerlessPluginStart;
share: SharePluginStart;

View file

@ -33,7 +33,8 @@
"@kbn/core-mount-utils-browser-internal",
"@kbn/xstate-utils",
"@kbn/shared-ux-utility",
"@kbn/ui-theme"
"@kbn/ui-theme",
"@kbn/logs-shared-plugin"
],
"exclude": [
"target/**/*"