From c97d92f87ccf103ee0feb2459258fcd829f6a80c Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 22 Mar 2024 10:59:05 +0100 Subject: [PATCH 1/5] [AI Assistant] Add setting for preferred type --- config/serverless.oblt.yml | 3 + .../selection/common/ai_assistant_type.ts | 14 +++ .../selection/common/ui_setting_keys.ts | 9 ++ .../selection/kibana.jsonc | 9 +- .../selection/public/index.ts | 23 +++-- .../management_section/mount_section.tsx | 8 +- .../selection/public/plugin.ts | 46 ++++++---- .../selection/public/types.ts | 13 +++ .../selection/server/config.ts | 33 +++++++ .../selection/server/index.ts | 16 ++++ .../selection/server/plugin.ts | 90 +++++++++++++++++++ .../selection/server/types.ts | 18 ++++ .../selection/tsconfig.json | 4 +- .../kibana.jsonc | 1 + .../public/components/nav_control/index.tsx | 4 +- .../public/hooks/is_nav_control_visible.tsx | 41 +++++++-- .../public/types.ts | 6 ++ .../tsconfig.json | 3 +- 18 files changed, 299 insertions(+), 42 deletions(-) create mode 100644 src/plugins/ai_assistant_management/selection/common/ai_assistant_type.ts create mode 100644 src/plugins/ai_assistant_management/selection/common/ui_setting_keys.ts create mode 100644 src/plugins/ai_assistant_management/selection/public/types.ts create mode 100644 src/plugins/ai_assistant_management/selection/server/config.ts create mode 100644 src/plugins/ai_assistant_management/selection/server/index.ts create mode 100644 src/plugins/ai_assistant_management/selection/server/plugin.ts create mode 100644 src/plugins/ai_assistant_management/selection/server/types.ts diff --git a/config/serverless.oblt.yml b/config/serverless.oblt.yml index a2e7d8d4eeb97..35d63ac5a49ef 100644 --- a/config/serverless.oblt.yml +++ b/config/serverless.oblt.yml @@ -81,6 +81,9 @@ xpack.apm.featureFlags.migrationToFleetAvailable: false xpack.apm.featureFlags.sourcemapApiAvailable: false xpack.apm.featureFlags.storageExplorerAvailable: false +## Set the AI Assistant type +aiAssistantManagementSelection.preferredAIAssistantType: "observability" + # Specify in telemetry the project type telemetry.labels.serverless: observability diff --git a/src/plugins/ai_assistant_management/selection/common/ai_assistant_type.ts b/src/plugins/ai_assistant_management/selection/common/ai_assistant_type.ts new file mode 100644 index 0000000000000..9ac02123e9297 --- /dev/null +++ b/src/plugins/ai_assistant_management/selection/common/ai_assistant_type.ts @@ -0,0 +1,14 @@ +/* + * 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. + */ + +export enum AIAssistantType { + Security = 'security', + Observability = 'observability', + Default = 'default', + Never = 'never', +} diff --git a/src/plugins/ai_assistant_management/selection/common/ui_setting_keys.ts b/src/plugins/ai_assistant_management/selection/common/ui_setting_keys.ts new file mode 100644 index 0000000000000..99a1a5859c06c --- /dev/null +++ b/src/plugins/ai_assistant_management/selection/common/ui_setting_keys.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 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. + */ + +export const PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY = 'aiAssistant:preferredAIAssistantType'; diff --git a/src/plugins/ai_assistant_management/selection/kibana.jsonc b/src/plugins/ai_assistant_management/selection/kibana.jsonc index 7db0b04b877d6..369cb0c626f04 100644 --- a/src/plugins/ai_assistant_management/selection/kibana.jsonc +++ b/src/plugins/ai_assistant_management/selection/kibana.jsonc @@ -4,10 +4,13 @@ "owner": "@elastic/obs-knowledge-team", "plugin": { "id": "aiAssistantManagementSelection", - "server": false, + "server": true, "browser": true, "requiredPlugins": ["management"], "optionalPlugins": ["home", "serverless"], - "requiredBundles": ["kibanaReact"] - } + "requiredBundles": ["kibanaReact"], + "configPath": [ + "aiAssistantManagementSelection" + ], + }, } diff --git a/src/plugins/ai_assistant_management/selection/public/index.ts b/src/plugins/ai_assistant_management/selection/public/index.ts index 54d13961ca017..46a20ceb7ffc8 100644 --- a/src/plugins/ai_assistant_management/selection/public/index.ts +++ b/src/plugins/ai_assistant_management/selection/public/index.ts @@ -6,13 +6,22 @@ * Side Public License, v 1. */ -import { AiAssistantManagementPlugin } from './plugin'; +import type { PluginInitializer } from '@kbn/core/public'; +import { AIAssistantManagementPlugin } from './plugin'; -export type { - AiAssistantManagementSelectionPluginSetup, - AiAssistantManagementSelectionPluginStart, +import type { + AIAssistantManagementSelectionPluginPublicSetup, + AIAssistantManagementSelectionPluginPublicStart, } from './plugin'; -export function plugin() { - return new AiAssistantManagementPlugin(); -} +export { AIAssistantType } from '../common/ai_assistant_type'; + +export type { + AIAssistantManagementSelectionPluginPublicSetup, + AIAssistantManagementSelectionPluginPublicStart, +}; + +export const plugin: PluginInitializer< + AIAssistantManagementSelectionPluginPublicSetup, + AIAssistantManagementSelectionPluginPublicStart +> = () => new AIAssistantManagementPlugin(); diff --git a/src/plugins/ai_assistant_management/selection/public/management_section/mount_section.tsx b/src/plugins/ai_assistant_management/selection/public/management_section/mount_section.tsx index 9a957d862bd71..a1bb4e19f2265 100644 --- a/src/plugins/ai_assistant_management/selection/public/management_section/mount_section.tsx +++ b/src/plugins/ai_assistant_management/selection/public/management_section/mount_section.tsx @@ -11,16 +11,16 @@ import ReactDOM from 'react-dom'; import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config'; import { I18nProvider } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { CoreSetup } from '@kbn/core/public'; +import type { CoreSetup } from '@kbn/core/public'; import { wrapWithTheme } from '@kbn/kibana-react-plugin/public'; -import { ManagementAppMountParams } from '@kbn/management-plugin/public'; -import { StartDependencies, AiAssistantManagementSelectionPluginStart } from '../plugin'; +import type { ManagementAppMountParams } from '@kbn/management-plugin/public'; +import type { StartDependencies, AIAssistantManagementSelectionPluginPublicStart } from '../plugin'; import { aIAssistantManagementSelectionRouter } from '../routes/config'; import { RedirectToHomeIfUnauthorized } from '../routes/components/redirect_to_home_if_unauthorized'; import { AppContextProvider } from '../app_context'; interface MountParams { - core: CoreSetup; + core: CoreSetup; mountParams: ManagementAppMountParams; } diff --git a/src/plugins/ai_assistant_management/selection/public/plugin.ts b/src/plugins/ai_assistant_management/selection/public/plugin.ts index 4658c9ec8795d..612f03b94423b 100644 --- a/src/plugins/ai_assistant_management/selection/public/plugin.ts +++ b/src/plugins/ai_assistant_management/selection/public/plugin.ts @@ -7,16 +7,20 @@ */ import { i18n } from '@kbn/i18n'; -import { CoreSetup, Plugin } from '@kbn/core/public'; -import { ManagementSetup } from '@kbn/management-plugin/public'; -import { HomePublicPluginSetup } from '@kbn/home-plugin/public'; -import { ServerlessPluginSetup } from '@kbn/serverless/public'; +import { type CoreSetup, Plugin, type CoreStart } from '@kbn/core/public'; +import type { ManagementSetup } from '@kbn/management-plugin/public'; +import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; +import type { ServerlessPluginSetup } from '@kbn/serverless/public'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { AIAssistantType } from '../common/ai_assistant_type'; +import { PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY } from '../common/ui_setting_keys'; // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface AiAssistantManagementSelectionPluginSetup {} +export interface AIAssistantManagementSelectionPluginPublicSetup {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface AiAssistantManagementSelectionPluginStart {} +export interface AIAssistantManagementSelectionPluginPublicStart { + aiAssistantType$: Observable; +} export interface SetupDependencies { management: ManagementSetup; @@ -27,20 +31,24 @@ export interface SetupDependencies { // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface StartDependencies {} -export class AiAssistantManagementPlugin +export class AIAssistantManagementPlugin implements Plugin< - AiAssistantManagementSelectionPluginSetup, - AiAssistantManagementSelectionPluginStart, + AIAssistantManagementSelectionPluginPublicSetup, + AIAssistantManagementSelectionPluginPublicStart, SetupDependencies, StartDependencies > { + constructor() {} + public setup( - core: CoreSetup, + core: CoreSetup, { home, management, serverless }: SetupDependencies - ): AiAssistantManagementSelectionPluginSetup { - if (serverless) return {}; + ): AIAssistantManagementSelectionPluginPublicSetup { + if (serverless) { + return {}; + } if (home) { home.featureCatalogue.register({ @@ -77,7 +85,15 @@ export class AiAssistantManagementPlugin return {}; } - public start() { - return {}; + public start(coreStart: CoreStart) { + const preferredAIAssistantType: AIAssistantType = coreStart.uiSettings.get( + PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY + ); + + const aiAssistantType$ = new BehaviorSubject(preferredAIAssistantType); + + return { + aiAssistantType$: aiAssistantType$.asObservable(), + }; } } diff --git a/src/plugins/ai_assistant_management/selection/public/types.ts b/src/plugins/ai_assistant_management/selection/public/types.ts new file mode 100644 index 0000000000000..94f7b5dde0bb8 --- /dev/null +++ b/src/plugins/ai_assistant_management/selection/public/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 { AIAssistantType } from '../common/ai_assistant_type'; + +export interface ConfigSchema { + preferredAIAssistantType: AIAssistantType; +} diff --git a/src/plugins/ai_assistant_management/selection/server/config.ts b/src/plugins/ai_assistant_management/selection/server/config.ts new file mode 100644 index 0000000000000..6b21b191fdf05 --- /dev/null +++ b/src/plugins/ai_assistant_management/selection/server/config.ts @@ -0,0 +1,33 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from '@kbn/core-plugins-server'; +import { AIAssistantType } from '../common/ai_assistant_type'; + +const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + preferredAIAssistantType: schema.oneOf( + [ + schema.literal(AIAssistantType.Default), + schema.literal(AIAssistantType.Never), + schema.literal(AIAssistantType.Observability), + schema.literal(AIAssistantType.Security), + ], + { defaultValue: AIAssistantType.Default } + ), +}); + +export type AIAssistantManagementSelectionConfig = TypeOf; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + exposeToBrowser: { + preferredAIAssistantType: true, + }, +}; diff --git a/src/plugins/ai_assistant_management/selection/server/index.ts b/src/plugins/ai_assistant_management/selection/server/index.ts new file mode 100644 index 0000000000000..3dbcc15473731 --- /dev/null +++ b/src/plugins/ai_assistant_management/selection/server/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { PluginInitializerContext } from '@kbn/core/server'; + +export { config } from './config'; + +export const plugin = async (initContext: PluginInitializerContext) => { + const { AIAssistantManagementSelectionPlugin } = await import('./plugin'); + return new AIAssistantManagementSelectionPlugin(initContext); +}; diff --git a/src/plugins/ai_assistant_management/selection/server/plugin.ts b/src/plugins/ai_assistant_management/selection/server/plugin.ts new file mode 100644 index 0000000000000..fe381bc4bf69e --- /dev/null +++ b/src/plugins/ai_assistant_management/selection/server/plugin.ts @@ -0,0 +1,90 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import type { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '@kbn/core/server'; +import { schema } from '@kbn/config-schema'; +import type { AIAssistantManagementSelectionConfig } from './config'; +import type { + AIAssistantManagementSelectionPluginServerSetup, + AIAssistantManagementSelectionPluginServerStart, +} from './types'; +import { AIAssistantType } from '../common/ai_assistant_type'; +import { PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY } from '../common/ui_setting_keys'; + +export class AIAssistantManagementSelectionPlugin + implements + Plugin< + AIAssistantManagementSelectionPluginServerSetup, + AIAssistantManagementSelectionPluginServerStart + > +{ + private readonly config: AIAssistantManagementSelectionConfig; + + constructor(initializerContext: PluginInitializerContext) { + this.config = initializerContext.config.get(); + } + + public setup(core: CoreSetup) { + core.uiSettings.register({ + [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { + name: i18n.translate('aiAssistantManagementSelection.preferredAIAssistantTypeSettingName', { + defaultMessage: 'AI Assistant type', + }), + value: this.config.preferredAIAssistantType, + description: i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingDescription', + { defaultMessage: 'Select an AI Assistant to use, or disable it entirely' } + ), + schema: schema.oneOf( + [ + schema.literal(AIAssistantType.Default), + schema.literal(AIAssistantType.Observability), + schema.literal(AIAssistantType.Security), + schema.literal(AIAssistantType.Never), + ], + { defaultValue: this.config.preferredAIAssistantType } + ), + options: [ + AIAssistantType.Default, + AIAssistantType.Observability, + AIAssistantType.Security, + AIAssistantType.Never, + ], + type: 'select', + optionLabels: { + [AIAssistantType.Default]: i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueDefault', + { defaultMessage: 'Default' } + ), + [AIAssistantType.Observability]: i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueObservability', + { defaultMessage: 'Observability' } + ), + [AIAssistantType.Security]: i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueSecurity', + { defaultMessage: 'Security' } + ), + [AIAssistantType.Never]: i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever', + { defaultMessage: 'Disable everywhere' } + ), + }, + requiresPageReload: true, + }, + }); + + return {}; + } + + public start(core: CoreStart) { + return {}; + } + + public stop() {} +} diff --git a/src/plugins/ai_assistant_management/selection/server/types.ts b/src/plugins/ai_assistant_management/selection/server/types.ts new file mode 100644 index 0000000000000..c956023781d75 --- /dev/null +++ b/src/plugins/ai_assistant_management/selection/server/types.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface AIAssistantManagementSelectionPluginServerDependenciesStart {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface AIAssistantManagementSelectionPluginServerDependenciesSetup {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface AIAssistantManagementSelectionPluginServerStart {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface AIAssistantManagementSelectionPluginServerSetup {} diff --git a/src/plugins/ai_assistant_management/selection/tsconfig.json b/src/plugins/ai_assistant_management/selection/tsconfig.json index 2c287b2ee77de..e6658bdfc6efb 100644 --- a/src/plugins/ai_assistant_management/selection/tsconfig.json +++ b/src/plugins/ai_assistant_management/selection/tsconfig.json @@ -13,7 +13,9 @@ "@kbn/i18n-react", "@kbn/core-chrome-browser", "@kbn/typed-react-router-config", - "@kbn/serverless" + "@kbn/serverless", + "@kbn/config-schema", + "@kbn/core-plugins-server" ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/kibana.jsonc b/x-pack/plugins/observability_solution/observability_ai_assistant_app/kibana.jsonc index 1a172d51a514b..713132ab2f7a7 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/kibana.jsonc +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/kibana.jsonc @@ -8,6 +8,7 @@ "browser": true, "configPath": ["xpack", "observabilityAIAssistantApp"], "requiredPlugins": [ + "aiAssistantManagementSelection", "observabilityAIAssistant", "observabilityShared", "actions", diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx index 2210f50d7a6b7..4c391c4b2f27e 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx @@ -6,7 +6,7 @@ */ import React, { useEffect, useRef, useState } from 'react'; import { AssistantAvatar, useAbortableAsync } from '@kbn/observability-ai-assistant-plugin/public'; -import { EuiButton } from '@elastic/eui'; +import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; import { css } from '@emotion/react'; import { v4 } from 'uuid'; import useObservable from 'react-use/lib/useObservable'; @@ -92,7 +92,7 @@ export function NavControl({}: {}) { fullWidth={false} minWidth={0} > - + {chatService.loading ? : } {chatService.value ? ( diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx index 1ceed342f37ae..c8386991eebbf 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx @@ -7,31 +7,54 @@ import { useEffect, useState } from 'react'; import { combineLatest } from 'rxjs'; -import { DEFAULT_APP_CATEGORIES } from '@kbn/core/public'; +import { DEFAULT_APP_CATEGORIES, type PublicAppInfo } from '@kbn/core/public'; +import { AIAssistantType } from '@kbn/ai-assistant-management-plugin/public'; import { useKibana } from './use_kibana'; +function getVisibility( + appId: string | undefined, + applications: ReadonlyMap, + preferredAssistantType: AIAssistantType +) { + if (preferredAssistantType === AIAssistantType.Never) { + return false; + } + + const categoryId = + (appId && applications.get(appId)?.category?.id) || DEFAULT_APP_CATEGORIES.kibana.id; + + if (preferredAssistantType === AIAssistantType.Observability) { + return categoryId !== DEFAULT_APP_CATEGORIES.security.id; + } + + return categoryId === DEFAULT_APP_CATEGORIES.observability.id; +} + export function useIsNavControlVisible() { const [isVisible, setIsVisible] = useState(false); const { services: { application: { currentAppId$, applications$ }, + plugins: { + start: { aiAssistantManagementSelection }, + }, }, } = useKibana(); useEffect(() => { - const appSubscription = combineLatest([currentAppId$, applications$]).subscribe({ - next: ([appId, applications]) => { - const isObservabilityApp = - appId && - applications.get(appId)?.category?.id === DEFAULT_APP_CATEGORIES.observability.id; - - setIsVisible(!!isObservabilityApp); + const appSubscription = combineLatest([ + currentAppId$, + applications$, + aiAssistantManagementSelection.aiAssistantType$, + ]).subscribe({ + next: ([appId, applications, preferredAssistantType]) => { + setIsVisible(getVisibility(appId, applications, preferredAssistantType)); }, }); return appSubscription.unsubscribe; - }, [currentAppId$, applications$]); + }, [currentAppId$, applications$, aiAssistantManagementSelection.aiAssistantType$]); return { isVisible, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/types.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/types.ts index 2876ddaf3332d..d6235da273839 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/types.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/types.ts @@ -28,6 +28,10 @@ import type { TriggersAndActionsUIPublicPluginStart, } from '@kbn/triggers-actions-ui-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { + AIAssistantManagementSelectionPluginPublicStart, + AIAssistantManagementSelectionPluginPublicSetup, +} from '@kbn/ai-assistant-management-plugin/public'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ObservabilityAIAssistantAppPublicStart {} @@ -46,6 +50,7 @@ export interface ObservabilityAIAssistantAppPluginStartDependencies { ml: MlPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; data: DataPublicPluginStart; + aiAssistantManagementSelection: AIAssistantManagementSelectionPluginPublicStart; } export interface ObservabilityAIAssistantAppPluginSetupDependencies { @@ -59,4 +64,5 @@ export interface ObservabilityAIAssistantAppPluginSetupDependencies { observabilityShared: ObservabilitySharedPluginSetup; ml: MlPluginSetup; triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; + aiAssistantManagementSelection: AIAssistantManagementSelectionPluginPublicSetup; } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json b/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json index 65fe909cd7e04..07cff8fa81614 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json @@ -49,7 +49,8 @@ "@kbn/react-kibana-context-theme", "@kbn/shared-ux-link-redirect-app", "@kbn/shared-ux-utility", - "@kbn/data-plugin" + "@kbn/data-plugin", + "@kbn/ai-assistant-management-plugin" ], "exclude": ["target/**/*"] } From 13a024db15dc710589874b4031f3a7644e8234c0 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 25 Mar 2024 14:27:02 +0100 Subject: [PATCH 2/5] Update telemetry/tests --- .../server/collectors/management/schema.ts | 4 ++++ .../server/collectors/management/types.ts | 1 + src/plugins/telemetry/schema/oss_plugins.json | 6 ++++++ .../plugin_functional/test_suites/core_plugins/rendering.ts | 1 + 4 files changed, 12 insertions(+) diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 925bf021baf92..c3d913141c886 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -655,4 +655,8 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'aiAssistant:preferredAIAssistantType': { + type: 'keyword', + _meta: { description: 'Non-default value of setting.' }, + }, }; diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index bb5cc9382f42b..4da08246e8632 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -172,4 +172,5 @@ export interface UsageStats { 'data_views:fields_excluded_data_tiers': string; 'observability:apmEnableTransactionProfiling': boolean; 'devTools:enablePersistentConsole': boolean; + 'aiAssistant:preferredAIAssistantType': string; } diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index ead61475284a7..8e56099d14a07 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -10427,6 +10427,12 @@ "_meta": { "description": "Non-default value of setting." } + }, + "aiAssistant:preferredAIAssistantType": { + "type": "keyword", + "_meta": { + "description": "Non-default value of setting." + } } } }, diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 5b06a48809c4c..b1721bc7fbfdf 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -340,6 +340,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.observability_onboarding.ui.enabled (boolean)', 'xpack.observabilityLogsExplorer.navigation.showAppLink (any)', // conditional, is actually a boolean 'share.new_version.enabled (boolean)', + 'aiAssistantManagementSelection.preferredAIAssistantType (alternatives)', ]; // We don't assert that actualExposedConfigKeys and expectedExposedConfigKeys are equal, because test failure messages with large // arrays are hard to grok. Instead, we take the difference between the two arrays and assert them separately, that way it's From 79fcedb43a0d44ef587691274ee8b1e89722fba5 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 25 Mar 2024 11:26:14 +0100 Subject: [PATCH 3/5] [Obs AI Assistant] Add to dashboard in Discover --- .../config_builder/config_builder.ts | 2 +- src/plugins/dashboard/kibana.jsonc | 3 +- .../public/dashboard_app/dashboard_app.tsx | 12 +- ...use_observability_ai_assistant_context.tsx | 480 ++++++++++++++++++ src/plugins/dashboard/public/plugin.tsx | 6 + ...observability_ai_assistant_service.stub.ts | 13 + .../observability_ai_assistant_service.ts | 23 + .../observability_ai_assistant/types.ts | 13 + .../public/services/plugin_services.ts | 2 + .../dashboard/public/services/types.ts | 2 + .../common/types.ts | 2 +- .../utils/create_screen_context_action.ts | 9 +- 12 files changed, 557 insertions(+), 10 deletions(-) create mode 100644 src/plugins/dashboard/public/dashboard_app/hooks/use_observability_ai_assistant_context.tsx create mode 100644 src/plugins/dashboard/public/services/observability_ai_assistant/observability_ai_assistant_service.stub.ts create mode 100644 src/plugins/dashboard/public/services/observability_ai_assistant/observability_ai_assistant_service.ts create mode 100644 src/plugins/dashboard/public/services/observability_ai_assistant/types.ts 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; } From 854bcd443ea1a5fc60e01bc001f2a43e9835ea61 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 27 Mar 2024 13:11:07 +0100 Subject: [PATCH 4/5] Simplifications and fixes --- .../config_builder/charts/tag_cloud.test.ts | 4 +- .../config_builder/charts/tag_cloud.ts | 6 +- .../config_builder/types.ts | 6 +- ...use_observability_ai_assistant_context.tsx | 140 ++++++------------ 4 files changed, 55 insertions(+), 101 deletions(-) diff --git a/packages/kbn-lens-embeddable-utils/config_builder/charts/tag_cloud.test.ts b/packages/kbn-lens-embeddable-utils/config_builder/charts/tag_cloud.test.ts index d7db572bb286a..bfa9b48c46d2c 100644 --- a/packages/kbn-lens-embeddable-utils/config_builder/charts/tag_cloud.test.ts +++ b/packages/kbn-lens-embeddable-utils/config_builder/charts/tag_cloud.test.ts @@ -123,8 +123,8 @@ test('generates tag cloud chart config', async () => { "minFontSize": 12, "orientation": "single", "showLabel": true, - "tagAccessor": "category", - "valueAccessor": "count", + "tagAccessor": "metric_formula_accessor_breakdown", + "valueAccessor": "metric_formula_accessor", }, }, "title": "test", diff --git a/packages/kbn-lens-embeddable-utils/config_builder/charts/tag_cloud.ts b/packages/kbn-lens-embeddable-utils/config_builder/charts/tag_cloud.ts index 551dcfe973d3a..55ce8f1fe1a02 100644 --- a/packages/kbn-lens-embeddable-utils/config_builder/charts/tag_cloud.ts +++ b/packages/kbn-lens-embeddable-utils/config_builder/charts/tag_cloud.ts @@ -18,7 +18,6 @@ import { buildDatasourceStates, buildReferences, getAdhocDataviews, - isFormulaDataset, mapToFormula, } from '../utils'; import { getBreakdownColumn, getFormulaColumn, getValueColumn } from '../columns'; @@ -31,18 +30,17 @@ function getAccessorName(type: 'breakdown') { function buildVisualizationState(config: LensTagCloudConfig): TagcloudState { const layer = config; - const isFormula = isFormulaDataset(config.dataset) || isFormulaDataset(layer.dataset); return { layerId: DEFAULT_LAYER_ID, - valueAccessor: !isFormula ? layer.value : ACCESSOR, + valueAccessor: ACCESSOR, maxFontSize: 72, minFontSize: 12, orientation: 'single', showLabel: true, ...(layer.breakdown ? { - tagAccessor: !isFormula ? (layer.breakdown as string) : getAccessorName('breakdown'), + tagAccessor: getAccessorName('breakdown'), } : {}), }; diff --git a/packages/kbn-lens-embeddable-utils/config_builder/types.ts b/packages/kbn-lens-embeddable-utils/config_builder/types.ts index 83595dbd7581c..9239409242787 100644 --- a/packages/kbn-lens-embeddable-utils/config_builder/types.ts +++ b/packages/kbn-lens-embeddable-utils/config_builder/types.ts @@ -208,9 +208,9 @@ export type LensRegionMapConfig = Identity< export interface LensMosaicConfigBase { chartType: 'mosaic'; /** field name to apply breakdown based on field type or full breakdown configuration */ - breakdown: LensBreakdownConfig; + breakdown: LensBreakdownConfig[]; /** field name to apply breakdown based on field type or full breakdown configuration */ - xAxis: LensBreakdownConfig; + xAxis?: LensBreakdownConfig; } export type LensMosaicConfig = Identity; @@ -228,7 +228,7 @@ export type LensTableConfig = Identity; } 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 index 1e3a3d061a610..83a55cbdd591a 100644 --- 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 @@ -8,7 +8,6 @@ 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'; @@ -117,32 +116,6 @@ export function useObservabilityAIAssistantContext({ 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', @@ -178,14 +151,11 @@ export function useObservabilityAIAssistantContext({ xy: { type: 'object', properties: { - xAxis, + xAxis: { + type: 'string', + }, yAxis: { - type: 'object', - properties: { - column: { - type: 'string', - }, - }, + type: 'string', }, type: { type: 'string', @@ -197,10 +167,7 @@ export function useObservabilityAIAssistantContext({ type: 'object', properties: { breakdown: { - type: 'object', - properties: { - type: axisType, - }, + type: 'string', }, }, }, @@ -212,31 +179,39 @@ export function useObservabilityAIAssistantContext({ }, pie: { type: 'object', + properties: { + breakdown: { + type: 'string', + }, + }, }, heatmap: { type: 'object', properties: { - timestampField: { + xAxis: { + type: 'string', + }, + breakdown: { type: 'string', }, - breakdown, }, - required: ['breakdown'], + required: ['xAxis'], }, mosaic: { type: 'object', properties: { - timestampField: { + breakdown: { type: 'string', }, - breakdown, }, required: ['breakdown'], }, regionmap: { type: 'object', properties: { - breakdown, + breakdown: { + type: 'string', + }, }, required: ['breakdown'], }, @@ -246,12 +221,19 @@ export function useObservabilityAIAssistantContext({ tagcloud: { type: 'object', properties: { - breakdown, + breakdown: { + type: 'string', + }, }, required: ['breakdown'], }, treemap: { type: 'object', + properties: { + breakdown: { + type: 'string', + }, + }, }, }, }, @@ -271,15 +253,12 @@ export function useObservabilityAIAssistantContext({ esql: { query }, } = args; - const indexPattern = getIndexPatternFromESQLQuery(query); - - const [columns, adhocDataView] = await Promise.all([ + const [columns] = await Promise.all([ getColumns({ search, esqlQuery: query, signal, }), - getESQLAdHocDataview(indexPattern, dataViews), ]); const configBuilder = new LensConfigBuilder(dataViews); @@ -303,10 +282,10 @@ export function useObservabilityAIAssistantContext({ { seriesType: layers?.xy?.type || 'line', type: 'series', - xAxis: layers?.xy?.xAxis?.column || '@timestamp', + xAxis: layers?.xy?.xAxis || '@timestamp', yAxis: [ { - value: layers?.xy?.yAxis?.column || firstMetricColumn!, + value: layers?.xy?.yAxis || firstMetricColumn!, }, ], }, @@ -322,12 +301,23 @@ export function useObservabilityAIAssistantContext({ chartType, title, value: firstMetricColumn!, - breakdown: [], + breakdown: [layers?.donut?.breakdown!], dataset, }; config = donutConfig; break; + case 'pie': + const pieConfig: LensPieConfig = { + chartType, + title, + value: firstMetricColumn!, + breakdown: [layers?.pie?.breakdown!], + dataset, + }; + config = pieConfig; + break; + case 'metric': const metricConfig: LensMetricConfig = { chartType, @@ -354,15 +344,8 @@ export function useObservabilityAIAssistantContext({ 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', - }, + breakdown: layers?.heatmap?.breakdown, + xAxis: layers?.heatmap?.xAxis || '@timestamp', dataset, }; config = heatmapConfig; @@ -373,41 +356,18 @@ export function useObservabilityAIAssistantContext({ 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', - }, + breakdown: [layers?.mosaic?.breakdown || '@timestamp'], 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', - }, + breakdown: layers?.regionmap?.breakdown!, dataset, }; config = regionMapConfig; @@ -428,11 +388,7 @@ export function useObservabilityAIAssistantContext({ chartType, title, value: firstMetricColumn!, - breakdown: { - field: layers?.tagcloud?.breakdown.column!, - filters: [], - type: layers?.tagcloud?.breakdown.type || 'topValues', - }, + breakdown: layers?.tagcloud?.breakdown!, dataset, }; config = tagCloudConfig; @@ -443,7 +399,7 @@ export function useObservabilityAIAssistantContext({ chartType, title, value: firstMetricColumn!, - breakdown: [], + breakdown: [layers?.treemap?.breakdown || '@timestamp'], dataset, }; config = treeMapConfig; From d13a86ffc0b7c576c1a1aff167f182a0ea3c34b6 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 8 Apr 2024 15:29:16 +0200 Subject: [PATCH 5/5] Fix ES|QL type lens embeddable --- packages/kbn-lens-embeddable-utils/config_builder/types.ts | 4 ++-- .../hooks/use_observability_ai_assistant_context.tsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/kbn-lens-embeddable-utils/config_builder/types.ts b/packages/kbn-lens-embeddable-utils/config_builder/types.ts index 9239409242787..124512c368ba1 100644 --- a/packages/kbn-lens-embeddable-utils/config_builder/types.ts +++ b/packages/kbn-lens-embeddable-utils/config_builder/types.ts @@ -7,7 +7,7 @@ */ import type { FormulaPublicApi, TypedLensByValueInput } from '@kbn/lens-plugin/public'; -import type { Filter, Query } from '@kbn/es-query'; +import type { AggregateQuery, Filter, Query } from '@kbn/es-query'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { Datatable } from '@kbn/expressions-plugin/common'; @@ -95,7 +95,7 @@ export interface LensConfigOptions { /** optional time range override */ timeRange?: TimeRange; filters?: Filter[]; - query?: Query; + query?: Query | AggregateQuery; } export interface LensAxisTitleVisibilityConfig { 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 index 83a55cbdd591a..c41112e646c7b 100644 --- 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 @@ -408,6 +408,7 @@ export function useObservabilityAIAssistantContext({ const embeddableInput = (await configBuilder.build(config, { embeddable: true, + query: dataset, })) as LensEmbeddableInput; return dashboardAPI