diff --git a/packages/kbn-lens-embeddable-utils/config_builder/config_builder.ts b/packages/kbn-lens-embeddable-utils/config_builder/config_builder.ts index d27d2b08abc1e..f793095aa3bff 100644 --- a/packages/kbn-lens-embeddable-utils/config_builder/config_builder.ts +++ b/packages/kbn-lens-embeddable-utils/config_builder/config_builder.ts @@ -46,7 +46,7 @@ export class LensConfigBuilder { async build( config: LensConfig, options: LensConfigOptions = {} - ): Promise { + ): Promise { const { chartType } = config; const chartConfig = await this.charts[chartType](config as any, { formulaAPI: this.formulaAPI, diff --git a/src/plugins/dashboard/kibana.jsonc b/src/plugins/dashboard/kibana.jsonc index 1c7689e09cf9f..2bf60cde55ef0 100644 --- a/src/plugins/dashboard/kibana.jsonc +++ b/src/plugins/dashboard/kibana.jsonc @@ -34,7 +34,8 @@ "usageCollection", "taskManager", "serverless", - "noDataPage" + "noDataPage", + "observabilityAIAssistant" ], "requiredBundles": [ "kibanaReact", diff --git a/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx b/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx index 1ff789ab61201..b24986b357ebd 100644 --- a/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx +++ b/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx @@ -14,7 +14,6 @@ import React, { createContext, useCallback, useContext, useEffect, useMemo, useS import { ViewMode } from '@kbn/embeddable-plugin/public'; import { useExecutionContext } from '@kbn/kibana-react-plugin/public'; import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; - import { DashboardAppNoDataPage, isDashboardAppInNoDataState, @@ -42,6 +41,7 @@ import { loadDashboardHistoryLocationState } from './locator/load_dashboard_hist import type { DashboardCreationOptions } from '../dashboard_container/embeddable/dashboard_container_factory'; import { DashboardTopNav } from '../dashboard_top_nav'; import { DashboardTabTitleSetter } from './tab_title_setter/dashboard_tab_title_setter'; +import { useObservabilityAIAssistantContext } from './hooks/use_observability_ai_assistant_context'; export interface DashboardAppProps { history: History; @@ -82,13 +82,21 @@ export function DashboardApp({ embeddable: { getStateTransfer }, notifications: { toasts }, settings: { uiSettings }, - data: { search }, + data: { search, dataViews }, customBranding, share: { url }, + observabilityAIAssistant, } = pluginServices.getServices(); const showPlainSpinner = useObservable(customBranding.hasCustomBranding$, false); const { scopedHistory: getScopedHistory } = useDashboardMountContext(); + useObservabilityAIAssistantContext({ + observabilityAIAssistant: observabilityAIAssistant.start, + dashboardAPI, + search, + dataViews, + }); + useExecutionContext(executionContext, { type: 'application', page: 'app', diff --git a/src/plugins/dashboard/public/dashboard_app/hooks/use_observability_ai_assistant_context.tsx b/src/plugins/dashboard/public/dashboard_app/hooks/use_observability_ai_assistant_context.tsx new file mode 100644 index 0000000000000..1e3a3d061a610 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_app/hooks/use_observability_ai_assistant_context.tsx @@ -0,0 +1,480 @@ +/* + * 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 type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; +import { useEffect } from 'react'; +import { getESQLAdHocDataview, getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; +import type { Embeddable } from '@kbn/embeddable-plugin/public'; +import type { ESQLSearchReponse } from '@kbn/es-types'; +import { esFieldTypeToKibanaFieldType } from '@kbn/field-types'; +import type { ISearchStart } from '@kbn/data-plugin/public'; +import { ESQL_SEARCH_STRATEGY } from '@kbn/data-plugin/common'; +import { + LensConfigBuilder, + type LensConfig, + type LensMetricConfig, + type LensPieConfig, + type LensGaugeConfig, + type LensXYConfig, + type LensHeatmapConfig, + type LensMosaicConfig, + type LensRegionMapConfig, + type LensTableConfig, + type LensTagCloudConfig, + type LensTreeMapConfig, + LensDataset, +} from '@kbn/lens-embeddable-utils/config_builder'; +import { lastValueFrom } from 'rxjs'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { LensEmbeddableInput } from '@kbn/lens-plugin/public'; +import type { AwaitingDashboardAPI } from '../../dashboard_container'; + +const chartTypes = [ + 'xy', + 'pie', + 'heatmap', + 'metric', + 'gauge', + 'donut', + 'mosaic', + 'regionmap', + 'table', + 'tagcloud', + 'treemap', +] as const; + +async function getColumns({ + esqlQuery, + search, + signal, +}: { + esqlQuery: string; + search: ISearchStart; + signal: AbortSignal; +}): Promise< + Array<{ + columnId: string; + fieldName: string; + meta: { + type: string; + }; + inMetricDimension: boolean; + }> +> { + const response = await lastValueFrom( + search.search( + { + params: { + query: `${esqlQuery} | LIMIT 0`, + }, + }, + { + abortSignal: signal, + strategy: ESQL_SEARCH_STRATEGY, + } + ) + ); + + const columns = + (response.rawResponse as unknown as ESQLSearchReponse).columns?.map(({ name, type }) => { + const kibanaType = esFieldTypeToKibanaFieldType(type); + const column = { + columnId: name, + fieldName: name, + meta: { type: kibanaType }, + inMetricDimension: kibanaType === 'number', + }; + + return column; + }) ?? []; + + return columns; +} + +export function useObservabilityAIAssistantContext({ + observabilityAIAssistant, + dashboardAPI, + search, + dataViews, +}: { + observabilityAIAssistant: ObservabilityAIAssistantPublicStart | undefined; + dashboardAPI: AwaitingDashboardAPI; + search: ISearchStart; + dataViews: DataViewsPublicPluginStart; +}) { + useEffect(() => { + if (!observabilityAIAssistant) { + return; + } + + const { + service: { setScreenContext }, + createScreenContextAction, + } = observabilityAIAssistant; + + const axisType = { + type: 'string', + enum: ['dateHistogram', 'topValues', 'filters', 'intervals'], + } as const; + + const xAxis = { + type: 'object', + properties: { + column: { + type: 'string', + }, + type: axisType, + }, + } as const; + + const breakdown = { + type: 'object', + properties: { + column: { + type: 'string', + }, + type: axisType, + }, + required: ['column'], + } as const; + + return setScreenContext({ + screenDescription: + 'The user is looking at the dashboard app. Here they can add visualizations to a dashboard and save them', + actions: dashboardAPI + ? [ + createScreenContextAction( + { + name: 'add_to_dashboard', + description: + 'Add an ES|QL visualization to the current dashboard. Pick a single chart type, and based on the chart type, the corresponding key for `layers`. E.g., when you select type:metric, fill in only layers.metric.', + parameters: { + type: 'object', + properties: { + esql: { + type: 'object', + properties: { + query: { + type: 'string', + description: + 'The ES|QL query for this visualization. Use the "query" function to generate ES|QL first and then add it here.', + }, + }, + required: ['query'], + }, + type: { + type: 'string', + description: 'The type of chart', + enum: chartTypes, + }, + layers: { + type: 'object', + properties: { + xy: { + type: 'object', + properties: { + xAxis, + yAxis: { + type: 'object', + properties: { + column: { + type: 'string', + }, + }, + }, + type: { + type: 'string', + enum: ['line', 'bar', 'area'], + }, + }, + }, + donut: { + type: 'object', + properties: { + breakdown: { + type: 'object', + properties: { + type: axisType, + }, + }, + }, + }, + metric: { + type: 'object', + }, + gauge: { + type: 'object', + }, + pie: { + type: 'object', + }, + heatmap: { + type: 'object', + properties: { + timestampField: { + type: 'string', + }, + breakdown, + }, + required: ['breakdown'], + }, + mosaic: { + type: 'object', + properties: { + timestampField: { + type: 'string', + }, + breakdown, + }, + required: ['breakdown'], + }, + regionmap: { + type: 'object', + properties: { + breakdown, + }, + required: ['breakdown'], + }, + table: { + type: 'object', + }, + tagcloud: { + type: 'object', + properties: { + breakdown, + }, + required: ['breakdown'], + }, + treemap: { + type: 'object', + }, + }, + }, + title: { + type: 'string', + description: 'An optional title for the visualization.', + }, + }, + required: ['esql', 'type'], + } as const, + }, + async ({ args, signal }) => { + const { + title = '', + type: chartType = 'xy', + layers, + esql: { query }, + } = args; + + const indexPattern = getIndexPatternFromESQLQuery(query); + + const [columns, adhocDataView] = await Promise.all([ + getColumns({ + search, + esqlQuery: query, + signal, + }), + getESQLAdHocDataview(indexPattern, dataViews), + ]); + + const configBuilder = new LensConfigBuilder(dataViews); + + let config: LensConfig; + + const firstMetricColumn = columns.find( + (column) => column.inMetricDimension + )?.columnId; + + const dataset: LensDataset = { + esql: query, + }; + + switch (chartType) { + default: + case 'xy': + const xyConfig: LensXYConfig = { + chartType: 'xy', + layers: [ + { + seriesType: layers?.xy?.type || 'line', + type: 'series', + xAxis: layers?.xy?.xAxis?.column || '@timestamp', + yAxis: [ + { + value: layers?.xy?.yAxis?.column || firstMetricColumn!, + }, + ], + }, + ], + dataset, + title, + }; + config = xyConfig; + break; + + case 'donut': + const donutConfig: LensPieConfig = { + chartType, + title, + value: firstMetricColumn!, + breakdown: [], + dataset, + }; + config = donutConfig; + break; + + case 'metric': + const metricConfig: LensMetricConfig = { + chartType, + title, + value: firstMetricColumn!, + dataset, + }; + config = metricConfig; + break; + + case 'gauge': + const gaugeConfig: LensGaugeConfig = { + chartType, + title, + value: firstMetricColumn!, + dataset, + }; + config = gaugeConfig; + + break; + + case 'heatmap': + const heatmapConfig: LensHeatmapConfig = { + chartType, + title, + value: firstMetricColumn!, + breakdown: { + field: layers?.heatmap?.breakdown?.column!, + type: layers?.heatmap?.breakdown?.type || 'topValues', + filters: [], + }, + xAxis: { + field: layers?.heatmap?.timestampField || '@timestamp', + type: 'dateHistogram', + }, + dataset, + }; + config = heatmapConfig; + break; + + case 'mosaic': + const mosaicConfig: LensMosaicConfig = { + chartType, + title, + value: firstMetricColumn!, + xAxis: { + field: layers?.mosaic?.timestampField || '@timestamp', + type: 'dateHistogram', + }, + breakdown: { + field: layers?.mosaic?.breakdown.column!, + filters: [], + type: layers?.mosaic?.breakdown.type || 'topValues', + }, + dataset, + }; + config = mosaicConfig; + break; + + case 'pie': + const pieConfig: LensPieConfig = { + chartType, + title, + value: firstMetricColumn!, + breakdown: [], + dataset, + }; + config = pieConfig; + break; + + case 'regionmap': + const regionMapConfig: LensRegionMapConfig = { + chartType, + title, + value: firstMetricColumn!, + breakdown: { + field: layers?.regionmap?.breakdown.column!, + filters: [], + type: layers?.regionmap?.breakdown.type! || 'topValues', + }, + dataset, + }; + config = regionMapConfig; + break; + + case 'table': + const tableConfig: LensTableConfig = { + chartType, + title, + value: firstMetricColumn!, + dataset, + }; + config = tableConfig; + break; + + case 'tagcloud': + const tagCloudConfig: LensTagCloudConfig = { + chartType, + title, + value: firstMetricColumn!, + breakdown: { + field: layers?.tagcloud?.breakdown.column!, + filters: [], + type: layers?.tagcloud?.breakdown.type || 'topValues', + }, + dataset, + }; + config = tagCloudConfig; + break; + + case 'treemap': + const treeMapConfig: LensTreeMapConfig = { + chartType, + title, + value: firstMetricColumn!, + breakdown: [], + dataset, + }; + config = treeMapConfig; + break; + } + + const embeddableInput = (await configBuilder.build(config, { + embeddable: true, + })) as LensEmbeddableInput; + + return dashboardAPI + .addNewPanel({ + panelType: 'lens', + initialState: embeddableInput, + }) + .then(() => { + return { + content: 'Visualization successfully added to dashboard', + }; + }) + .catch((error) => { + return { + content: { + error, + }, + }; + }); + } + ), + ] + : [], + }); + }, [observabilityAIAssistant, dashboardAPI, search, dataViews]); +} diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 8fb1260eb327d..e1d1f6fe895f2 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -52,6 +52,10 @@ import type { UrlForwardingSetup, UrlForwardingStart } from '@kbn/url-forwarding import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public'; import type { ServerlessPluginStart } from '@kbn/serverless/public'; import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public'; +import type { + ObservabilityAIAssistantPublicSetup, + ObservabilityAIAssistantPublicStart, +} from '@kbn/observability-ai-assistant-plugin/public'; import { CustomBrandingStart } from '@kbn/core-custom-branding-browser'; import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; @@ -86,6 +90,7 @@ export interface DashboardSetupDependencies { uiActions: UiActionsSetup; urlForwarding: UrlForwardingSetup; unifiedSearch: UnifiedSearchPublicPluginStart; + observabilityAIAssistant?: ObservabilityAIAssistantPublicSetup; } export interface DashboardStartDependencies { @@ -109,6 +114,7 @@ export interface DashboardStartDependencies { customBranding: CustomBrandingStart; serverless?: ServerlessPluginStart; noDataPage?: NoDataPagePluginStart; + observabilityAIAssistant?: ObservabilityAIAssistantPublicStart; } export interface DashboardSetup { diff --git a/src/plugins/dashboard/public/services/observability_ai_assistant/observability_ai_assistant_service.stub.ts b/src/plugins/dashboard/public/services/observability_ai_assistant/observability_ai_assistant_service.stub.ts new file mode 100644 index 0000000000000..f2593e5d8f449 --- /dev/null +++ b/src/plugins/dashboard/public/services/observability_ai_assistant/observability_ai_assistant_service.stub.ts @@ -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 { ObservabilityAIAssistantServiceFactory } from './observability_ai_assistant_service'; + +export const observabilityAIAssistantServiceStubFactory: ObservabilityAIAssistantServiceFactory = + () => { + return {}; + }; diff --git a/src/plugins/dashboard/public/services/observability_ai_assistant/observability_ai_assistant_service.ts b/src/plugins/dashboard/public/services/observability_ai_assistant/observability_ai_assistant_service.ts new file mode 100644 index 0000000000000..81d1a23854638 --- /dev/null +++ b/src/plugins/dashboard/public/services/observability_ai_assistant/observability_ai_assistant_service.ts @@ -0,0 +1,23 @@ +/* + * 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 type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; +import type { DashboardStartDependencies } from '../../plugin'; +import type { ObservabilityAIAssistantService } from './types'; + +export type ObservabilityAIAssistantServiceFactory = KibanaPluginServiceFactory< + ObservabilityAIAssistantService, + DashboardStartDependencies +>; +export const observabilityAIAssistantServiceFactory: ObservabilityAIAssistantServiceFactory = ({ + startPlugins, +}) => { + return startPlugins.observabilityAIAssistant + ? { start: startPlugins.observabilityAIAssistant } + : {}; +}; diff --git a/src/plugins/dashboard/public/services/observability_ai_assistant/types.ts b/src/plugins/dashboard/public/services/observability_ai_assistant/types.ts new file mode 100644 index 0000000000000..342f461024066 --- /dev/null +++ b/src/plugins/dashboard/public/services/observability_ai_assistant/types.ts @@ -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 type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; + +export interface ObservabilityAIAssistantService { + start?: ObservabilityAIAssistantPublicStart; +} diff --git a/src/plugins/dashboard/public/services/plugin_services.ts b/src/plugins/dashboard/public/services/plugin_services.ts index 2c9c1d95828e8..823ac2fa09641 100644 --- a/src/plugins/dashboard/public/services/plugin_services.ts +++ b/src/plugins/dashboard/public/services/plugin_services.ts @@ -45,6 +45,7 @@ import { contentManagementServiceFactory } from './content_management/content_ma import { serverlessServiceFactory } from './serverless/serverless_service'; import { noDataPageServiceFactory } from './no_data_page/no_data_page_service'; import { uiActionsServiceFactory } from './ui_actions/ui_actions_service'; +import { observabilityAIAssistantServiceFactory } from './observability_ai_assistant/observability_ai_assistant_service'; const providers: PluginServiceProviders = { dashboardContentManagement: new PluginServiceProvider(dashboardContentManagementServiceFactory, [ @@ -90,6 +91,7 @@ const providers: PluginServiceProviders(); diff --git a/src/plugins/dashboard/public/services/types.ts b/src/plugins/dashboard/public/services/types.ts index 420ba257b6a6a..1d32a41922189 100644 --- a/src/plugins/dashboard/public/services/types.ts +++ b/src/plugins/dashboard/public/services/types.ts @@ -40,6 +40,7 @@ import { DashboardVisualizationsService } from './visualizations/types'; import { DashboardServerlessService } from './serverless/types'; import { NoDataPageService } from './no_data_page/types'; import { DashboardUiActionsService } from './ui_actions/types'; +import { ObservabilityAIAssistantService } from './observability_ai_assistant/types'; export type DashboardPluginServiceParams = KibanaPluginServiceParams & { initContext: PluginInitializerContext; // need a custom type so that initContext is a required parameter for initializerContext @@ -76,4 +77,5 @@ export interface DashboardServices { serverless: DashboardServerlessService; // TODO: make this optional in follow up noDataPage: NoDataPageService; uiActions: DashboardUiActionsService; + observabilityAIAssistant: ObservabilityAIAssistantService; // TODO: make this optional in follow up } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/common/types.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/common/types.ts index e30f425d5dd9a..cd43d9eefaab9 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/common/types.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/common/types.ts @@ -98,7 +98,7 @@ export interface ObservabilityAIAssistantScreenContextRequest { actions?: Array<{ name: string; description: string; parameters?: CompatibleJSONSchema }>; } -export type ScreenContextActionRespondFunction = ({}: { +export type ScreenContextActionRespondFunction = ({}: { args: TArguments; signal: AbortSignal; connectorId: string; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/utils/create_screen_context_action.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/public/utils/create_screen_context_action.ts index 3dbc4dbaf36f0..bb012f863c7c1 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/utils/create_screen_context_action.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/utils/create_screen_context_action.ts @@ -17,14 +17,13 @@ type ReturnOf, - TResponse = ReturnOf + TActionDefinition extends Omit >( definition: TActionDefinition, - respond: ScreenContextActionRespondFunction -): ScreenContextActionDefinition { + respond: ScreenContextActionRespondFunction> +): ScreenContextActionDefinition { return { ...definition, respond, - }; + } as ScreenContextActionDefinition; }